Skip to content

Commit 542fb1b

Browse files
committed
improvement(editor): refine previews and compact formatting controls
1 parent 1897cbb commit 542fb1b

5 files changed

Lines changed: 241 additions & 92 deletions

File tree

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
/** Shared floating toolbar chrome for text, table, and image selections. */
2-
export const BUBBLE_MENU_CLASS =
3-
'scrollbar-none fade-in-0 z-[var(--z-popover)] flex max-w-[calc(100%_-_1rem)] animate-in items-center gap-0.5 overflow-x-auto rounded-lg border border-[var(--border)] bg-[var(--bg)] p-1 shadow-xs duration-150 ease-out motion-reduce:animate-none'
1+
import { cn, MENU_STYLES } from '@sim/emcn'
2+
3+
/** Shared floating toolbar chrome for text and table selections. */
4+
export const BUBBLE_MENU_CLASS = cn(
5+
MENU_STYLES.surface,
6+
MENU_STYLES.padding,
7+
'scrollbar-none fade-in-0 z-[var(--z-popover)] flex max-w-[calc(100%_-_1rem)] animate-in items-center gap-0.5 overflow-x-auto duration-150 ease-out motion-reduce:animate-none'
8+
)

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx‎

Lines changed: 114 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import {
66
Code,
77
Heading1,
88
Heading2,
9+
Heading3,
910
Highlighter,
1011
Italic,
1112
Link as LinkIcon,
1213
List,
1314
ListChecks,
1415
ListOrdered,
16+
Pilcrow,
1517
Strikethrough,
1618
TextQuote,
1719
Unlink,
@@ -37,8 +39,10 @@ import {
3739
ToolbarButton,
3840
ToolbarDivider,
3941
} from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button'
42+
import { ToolbarMenu } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-menu'
4043
import { useBubbleMenuFloating } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/use-bubble-menu-floating'
4144
import { useEditorToolbar } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/use-editor-toolbar'
45+
import { selectionTouchesTable } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/table'
4246

4347
/**
4448
* Whether the formatting toolbar may show for the given range: the editor is editable, the range
@@ -148,14 +152,23 @@ export function EditorBubbleMenu({
148152
highlight: e.isActive('highlight'),
149153
code: e.isActive('code'),
150154
link: e.isActive('link'),
155+
text:
156+
e.isActive('paragraph') &&
157+
!e.isActive('bulletList') &&
158+
!e.isActive('orderedList') &&
159+
!e.isActive('taskList') &&
160+
!e.isActive('blockquote'),
151161
heading1: e.isActive('heading', { level: 1 }),
152162
heading2: e.isActive('heading', { level: 2 }),
163+
heading3: e.isActive('heading', { level: 3 }),
153164
bulletList: e.isActive('bulletList'),
154165
orderedList: e.isActive('orderedList'),
155166
taskList: e.isActive('taskList'),
156167
blockquote: e.isActive('blockquote'),
168+
canText: !selectionTouchesTable(e.state),
157169
canHeading1: e.can().toggleHeading({ level: 1 }),
158170
canHeading2: e.can().toggleHeading({ level: 2 }),
171+
canHeading3: e.can().toggleHeading({ level: 3 }),
159172
canBulletList: e.can().toggleBulletList(),
160173
canOrderedList: e.can().toggleOrderedList(),
161174
canTaskList: e.can().toggleTaskList(),
@@ -363,6 +376,80 @@ export function EditorBubbleMenu({
363376
<ToolbarDivider />
364377
</>
365378
)}
379+
<ToolbarMenu
380+
editor={editor}
381+
label='Text style'
382+
value='Style'
383+
items={[
384+
{
385+
icon: Pilcrow,
386+
label: 'Text',
387+
active: active.text,
388+
disabled: !active.canText,
389+
onSelect: () => {
390+
if (!selectionTouchesTable(editor.state)) {
391+
editor.chain().focus().clearNodes().run()
392+
}
393+
},
394+
},
395+
{
396+
icon: Heading1,
397+
label: 'Heading 1',
398+
shortcut: '⌘⌥1',
399+
active: active.heading1,
400+
disabled: !active.canHeading1,
401+
onSelect: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
402+
},
403+
{
404+
icon: Heading2,
405+
label: 'Heading 2',
406+
shortcut: '⌘⌥2',
407+
active: active.heading2,
408+
disabled: !active.canHeading2,
409+
onSelect: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
410+
},
411+
{
412+
icon: Heading3,
413+
label: 'Heading 3',
414+
shortcut: '⌘⌥3',
415+
active: active.heading3,
416+
disabled: !active.canHeading3,
417+
onSelect: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
418+
},
419+
{
420+
icon: List,
421+
label: 'Bulleted list',
422+
shortcut: '⌘⇧8',
423+
active: active.bulletList,
424+
disabled: !active.canBulletList,
425+
onSelect: () => editor.chain().focus().toggleBulletList().run(),
426+
},
427+
{
428+
icon: ListOrdered,
429+
label: 'Numbered list',
430+
shortcut: '⌘⇧7',
431+
active: active.orderedList,
432+
disabled: !active.canOrderedList,
433+
onSelect: () => editor.chain().focus().toggleOrderedList().run(),
434+
},
435+
{
436+
icon: ListChecks,
437+
label: 'Checklist',
438+
shortcut: '⌘⇧9',
439+
active: active.taskList,
440+
disabled: !active.canTaskList,
441+
onSelect: () => editor.chain().focus().toggleTaskList().run(),
442+
},
443+
{
444+
icon: TextQuote,
445+
label: 'Quote',
446+
shortcut: '⌘⇧B',
447+
active: active.blockquote,
448+
disabled: !active.canBlockquote,
449+
onSelect: () => editor.chain().focus().toggleBlockquote().run(),
450+
},
451+
]}
452+
/>
366453
<ToolbarButton
367454
icon={Bold}
368455
label='Bold'
@@ -377,83 +464,40 @@ export function EditorBubbleMenu({
377464
isActive={active.italic}
378465
onClick={() => editor.chain().focus().toggleItalic().run()}
379466
/>
380-
<ToolbarButton
381-
icon={Strikethrough}
382-
label='Strikethrough'
383-
shortcut='⌘⇧S'
384-
isActive={active.strike}
385-
onClick={() => editor.chain().focus().toggleStrike().run()}
386-
/>
387-
<ToolbarButton
388-
icon={Highlighter}
389-
label='Highlight'
390-
shortcut='⌘⇧H'
391-
isActive={active.highlight}
392-
onClick={() => editor.chain().focus().toggleMark('highlight').run()}
393-
/>
394-
<ToolbarButton
395-
icon={Code}
396-
label='Code'
397-
shortcut='⌘E'
398-
isActive={active.code}
399-
onClick={() => editor.chain().focus().toggleCode().run()}
400-
/>
401467
<ToolbarButton
402468
icon={LinkIcon}
403469
label='Link'
404470
shortcut='⌘K'
405471
isActive={active.link}
406472
onClick={openLinkEditor}
407473
/>
408-
<ToolbarDivider />
409-
<ToolbarButton
410-
icon={Heading1}
411-
label='Heading 1'
412-
shortcut='⌘⌥1'
413-
isActive={active.heading1}
414-
disabled={!active.canHeading1}
415-
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
416-
/>
417-
<ToolbarButton
418-
icon={Heading2}
419-
label='Heading 2'
420-
shortcut='⌘⌥2'
421-
isActive={active.heading2}
422-
disabled={!active.canHeading2}
423-
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
424-
/>
425-
<ToolbarDivider />
426-
<ToolbarButton
427-
icon={List}
428-
label='Bulleted list'
429-
shortcut='⌘⇧8'
430-
isActive={active.bulletList}
431-
disabled={!active.canBulletList}
432-
onClick={() => editor.chain().focus().toggleBulletList().run()}
433-
/>
434-
<ToolbarButton
435-
icon={ListOrdered}
436-
label='Numbered list'
437-
shortcut='⌘⇧7'
438-
isActive={active.orderedList}
439-
disabled={!active.canOrderedList}
440-
onClick={() => editor.chain().focus().toggleOrderedList().run()}
441-
/>
442-
<ToolbarButton
443-
icon={ListChecks}
444-
label='Checklist'
445-
shortcut='⌘⇧9'
446-
isActive={active.taskList}
447-
disabled={!active.canTaskList}
448-
onClick={() => editor.chain().focus().toggleTaskList().run()}
449-
/>
450-
<ToolbarButton
451-
icon={TextQuote}
452-
label='Quote'
453-
shortcut='⌘⇧B'
454-
isActive={active.blockquote}
455-
disabled={!active.canBlockquote}
456-
onClick={() => editor.chain().focus().toggleBlockquote().run()}
474+
<ToolbarMenu
475+
editor={editor}
476+
label='More formatting'
477+
active={active.strike || active.highlight || active.code}
478+
items={[
479+
{
480+
icon: Strikethrough,
481+
label: 'Strikethrough',
482+
shortcut: '⌘⇧S',
483+
active: active.strike,
484+
onSelect: () => editor.chain().focus().toggleStrike().run(),
485+
},
486+
{
487+
icon: Highlighter,
488+
label: 'Highlight',
489+
shortcut: '⌘⇧H',
490+
active: active.highlight,
491+
onSelect: () => editor.chain().focus().toggleMark('highlight').run(),
492+
},
493+
{
494+
icon: Code,
495+
label: 'Code',
496+
shortcut: '⌘E',
497+
active: active.code,
498+
onSelect: () => editor.chain().focus().toggleCode().run(),
499+
},
500+
]}
457501
/>
458502
</>
459503
)}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback, useEffect, useRef, useState } from 'react'
22
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
3-
import { useCopyToClipboard } from '@sim/emcn'
3+
import { cn, MENU_STYLES, useCopyToClipboard } from '@sim/emcn'
44
import { Check, Duplicate, Pencil, Unlink } from '@sim/emcn/icons'
55
import { getMarkRange } from '@tiptap/core'
66
import { type Editor, useEditorState } from '@tiptap/react'
@@ -187,7 +187,11 @@ export function LinkHoverCard({ editor }: LinkHoverCardProps) {
187187
onBlur={(event) => {
188188
if (!event.currentTarget.contains(event.relatedTarget)) dismiss()
189189
}}
190-
className='z-[var(--z-popover)] flex items-center gap-0.5 rounded-lg border border-[var(--border)] bg-[var(--bg)] p-1 shadow-xs transition-opacity duration-150 ease-out'
190+
className={cn(
191+
MENU_STYLES.surface,
192+
MENU_STYLES.padding,
193+
'z-[var(--z-popover)] flex items-center gap-0.5 transition-opacity duration-150 ease-out'
194+
)}
191195
>
192196
{isEditing ? (
193197
<>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { ComponentType, SVGProps } from 'react'
2+
import {
3+
Button,
4+
Chip,
5+
cn,
6+
DropdownMenu,
7+
DropdownMenuContent,
8+
DropdownMenuItem,
9+
DropdownMenuItemLabel,
10+
DropdownMenuShortcut,
11+
DropdownMenuTrigger,
12+
Tooltip,
13+
} from '@sim/emcn'
14+
import { Check, ChevronDown, MoreHorizontal } from '@sim/emcn/icons'
15+
import type { Editor } from '@tiptap/react'
16+
17+
interface ToolbarMenuProps {
18+
editor: Editor
19+
label: string
20+
value?: string
21+
active?: boolean
22+
items: {
23+
label: string
24+
icon: ComponentType<SVGProps<SVGSVGElement>>
25+
shortcut?: string
26+
active: boolean
27+
disabled?: boolean
28+
onSelect: () => void
29+
}[]
30+
}
31+
32+
/**
33+
* Formatting commands share the platform menu while retaining the editor's selection.
34+
* Focusing the trigger before its portal opens keeps TipTap's blur handler inside the toolbar.
35+
*/
36+
export function ToolbarMenu({ editor, label, value, active, items }: ToolbarMenuProps) {
37+
if (!editor.isEditable) return null
38+
39+
return (
40+
<DropdownMenu modal={false}>
41+
<Tooltip.Root>
42+
<Tooltip.Trigger asChild>
43+
<DropdownMenuTrigger asChild>
44+
{value ? (
45+
<Chip
46+
rightIcon={ChevronDown}
47+
aria-label={label}
48+
className='h-10 w-[76px] focus-visible:bg-[var(--surface-hover)] sm:h-[28px]'
49+
onPointerDown={(event) => event.currentTarget.focus()}
50+
>
51+
{value}
52+
</Chip>
53+
) : (
54+
<Button
55+
type='button'
56+
variant={active ? 'active' : 'ghost'}
57+
size='icon'
58+
aria-label={label}
59+
className={cn(
60+
'size-10 focus-visible:bg-[var(--surface-hover)] sm:size-[28px]',
61+
!active && 'hover-hover:bg-[var(--surface-hover)]'
62+
)}
63+
onPointerDown={(event) => event.currentTarget.focus()}
64+
>
65+
<MoreHorizontal className='size-[14px]' />
66+
</Button>
67+
)}
68+
</DropdownMenuTrigger>
69+
</Tooltip.Trigger>
70+
<Tooltip.Content>{label}</Tooltip.Content>
71+
</Tooltip.Root>
72+
<DropdownMenuContent
73+
align='start'
74+
className='w-[220px]'
75+
onKeyDown={(event) => event.stopPropagation()}
76+
>
77+
{items.map(({ label: itemLabel, icon: Icon, shortcut, active, disabled, onSelect }) => (
78+
<DropdownMenuItem
79+
key={itemLabel}
80+
role='menuitemcheckbox'
81+
aria-checked={active}
82+
active={active}
83+
disabled={disabled}
84+
onSelect={() => {
85+
if (!editor.isDestroyed && editor.isEditable) onSelect()
86+
}}
87+
>
88+
<Icon className='size-[14px]' />
89+
<DropdownMenuItemLabel label={itemLabel} />
90+
{shortcut && <DropdownMenuShortcut>{shortcut}</DropdownMenuShortcut>}
91+
{active && <Check className='size-[14px]' />}
92+
</DropdownMenuItem>
93+
))}
94+
</DropdownMenuContent>
95+
</DropdownMenu>
96+
)
97+
}

0 commit comments

Comments
 (0)