Skip to content

feat(text): support Link Mention format #609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/notion-types/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export type InlineEquationFormat = ['e', string]
export type DiscussionFormat = ['m', string]
export type ExternalLinkFormat = ['‣', [string, string]]
export type DateFormat = ['d', FormattedDate]
export type LinkMentionFormat = ['lm', string]

export interface FormattedDate {
type: 'date' | 'daterange' | 'datetime' | 'datetimerange'
Expand All @@ -145,6 +146,7 @@ export type SubDecoration =
| ExternalLinkFormat
| DiscussionFormat
| ExternalObjectInstanceFormat
| LinkMentionFormat

export type BaseDecoration = [string]
export type AdditionalDecoration = [string, SubDecoration[]]
Expand Down
72 changes: 72 additions & 0 deletions packages/react-notion-x/src/components/link-mention.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as React from 'react'

export type LinkMentionData = {
href: string
title: string
icon_url: string
description: string
link_provider: string
thumbnail_url: string
}

export function LinkMention({ metadata }: { metadata: LinkMentionData }) {
return (
<span className='notion-link-mention'>
<LinkMentionInline metadata={metadata} />
<LinkMentionPreview metadata={metadata} />
</span>
)
}

function LinkMentionInline({ metadata }: { metadata: LinkMentionData }) {
return (
<a
href={metadata.href}
target='_blank'
rel='noopener noreferrer'
className='notion-link-mention-link'
>
<img
className='notion-link-mention-icon'
src={metadata.icon_url}
alt={metadata.link_provider}
/>
{metadata.link_provider && (
<span className='notion-link-mention-provider'>
{metadata.link_provider}
</span>
)}
<span className='notion-link-mention-title'>{metadata.title}</span>
</a>
)
}

function LinkMentionPreview({ metadata }: { metadata: LinkMentionData }) {
return (
<div className='notion-link-mention-preview'>
<article className='notion-link-mention-card'>
<img
className='notion-link-mention-preview-thumbnail'
src={metadata.thumbnail_url}
alt={metadata.title}
/>
<div className='notion-link-mention-preview-content'>
<p className='notion-link-mention-preview-title'>{metadata.title}</p>
<p className='notion-link-mention-preview-description'>
{metadata.description}
</p>
<div className='notion-link-mention-preview-footer'>
<img
className='notion-link-mention-preview-icon'
src={metadata.icon_url}
alt={metadata.link_provider}
/>
<span className='notion-link-mention-preview-provider'>
{metadata.link_provider}
</span>
</div>
</div>
</article>
</div>
)
}
7 changes: 7 additions & 0 deletions packages/react-notion-x/src/components/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { formatDate, getHashFragmentValue } from '../utils'
import { EOI } from './eoi'
import { GracefulImage } from './graceful-image'
import { PageTitle } from './page-title'
import { LinkMention, type LinkMentionData } from './link-mention'

/**
* Renders a single piece of Notion text, including basic rich text formatting.
Expand All @@ -20,6 +21,7 @@ import { PageTitle } from './page-title'
* TODO: I think this implementation would be more correct if the reduce just added
* attributes to the final element's style.
*/

export function Text({
value,
block,
Expand Down Expand Up @@ -244,6 +246,11 @@ export function Text({
)
}

case 'lm': {
const metadata = decorator[1] as unknown as LinkMentionData
return <LinkMention metadata={metadata} />
}

case 'eoi': {
const blockId = decorator[1]
const externalObjectInstance = recordMap.block[blockId]
Expand Down
107 changes: 107 additions & 0 deletions packages/react-notion-x/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2976,3 +2976,110 @@ svg.notion-page-icon {
text-decoration: underline;
cursor: pointer;
}

/* Link Mention Styles */
.notion-link-mention {
position: relative;
display: inline-flex;
align-items: center;
vertical-align: middle;
}

.notion-link-mention-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}

.notion-link-mention-icon {
width: 1.5rem;
height: 1.5rem;
border-radius: 0.375rem;
}

.notion-link-mention-provider {
font-size: 0.875rem;
color: var(--fg-color);
}

.notion-link-mention-title {
font-size: 0.875rem;
font-weight: 600;
color: var(--fg-color);
}

/* Preview card (appears on hover) */
.notion-link-mention-preview {
display: none;
position: absolute;
top: 100%;
left: 0;
margin-top: 0.5rem;
z-index: 100000;
}

.notion-link-mention:hover .notion-link-mention-preview {
display: block;
}

.notion-link-mention-card {
width: 18rem;
height: 15rem;
background: var(--bg-color);
border-radius: 0.75rem;
border: 1px solid rgba(27, 31, 36, 0.15);
overflow: hidden;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

.notion-link-mention-preview-thumbnail {
width: 100%;
height: 55%;
object-fit: cover;
}

.notion-link-mention-preview-content {
display: flex;
flex-direction: column;
height: 45%;
padding: 0.5rem 1rem;
gap: 0.125rem;
}

.notion-link-mention-preview-title {
font-size: 0.875rem;
font-weight: 700;
color: var(--fg-color);
margin: 0;
}

.notion-link-mention-preview-description {
font-size: 0.875rem;
color: var(--fg-color);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
}

.notion-link-mention-preview-footer {
display: flex;
align-items: center;
gap: 0.375rem;
margin-top: auto;
padding-bottom: 0.5rem;
}

.notion-link-mention-preview-icon {
width: 1rem;
height: 1rem;
border-radius: 0.25rem;
}

.notion-link-mention-preview-provider {
font-size: 0.875rem;
color: var(--fg-color-2);
}