Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/** Shared floating toolbar chrome for text, table, and image selections. */
export const BUBBLE_MENU_CLASS =
'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'
import { cn, MENU_STYLES } from '@sim/emcn'

/** Shared floating toolbar chrome for text and table selections. */
export const BUBBLE_MENU_CLASS = cn(
MENU_STYLES.surface,
MENU_STYLES.padding,
'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'
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import {
Code,
Heading1,
Heading2,
Heading3,
Highlighter,
Italic,
Link as LinkIcon,
List,
ListChecks,
ListOrdered,
Pilcrow,
Strikethrough,
TextQuote,
Unlink,
Expand All @@ -37,8 +39,10 @@ import {
ToolbarButton,
ToolbarDivider,
} from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button'
import { ToolbarMenu } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-menu'
import { useBubbleMenuFloating } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/use-bubble-menu-floating'
import { useEditorToolbar } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/use-editor-toolbar'
import { selectionTouchesTable } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/table'

/**
* Whether the formatting toolbar may show for the given range: the editor is editable, the range
Expand Down Expand Up @@ -148,14 +152,23 @@ export function EditorBubbleMenu({
highlight: e.isActive('highlight'),
code: e.isActive('code'),
link: e.isActive('link'),
text:
e.isActive('paragraph') &&
!e.isActive('bulletList') &&
!e.isActive('orderedList') &&
!e.isActive('taskList') &&
!e.isActive('blockquote'),
heading1: e.isActive('heading', { level: 1 }),
heading2: e.isActive('heading', { level: 2 }),
heading3: e.isActive('heading', { level: 3 }),
bulletList: e.isActive('bulletList'),
orderedList: e.isActive('orderedList'),
taskList: e.isActive('taskList'),
blockquote: e.isActive('blockquote'),
canText: !selectionTouchesTable(e.state),
canHeading1: e.can().toggleHeading({ level: 1 }),
canHeading2: e.can().toggleHeading({ level: 2 }),
canHeading3: e.can().toggleHeading({ level: 3 }),
canBulletList: e.can().toggleBulletList(),
canOrderedList: e.can().toggleOrderedList(),
canTaskList: e.can().toggleTaskList(),
Expand Down Expand Up @@ -363,6 +376,80 @@ export function EditorBubbleMenu({
<ToolbarDivider />
</>
)}
<ToolbarMenu
editor={editor}
label='Text style'
value='Style'
items={[
{
icon: Pilcrow,
label: 'Text',
active: active.text,
disabled: !active.canText,
onSelect: () => {
if (!selectionTouchesTable(editor.state)) {
editor.chain().focus().clearNodes().run()
}
},
},
{
icon: Heading1,
label: 'Heading 1',
shortcut: '⌘⌥1',
active: active.heading1,
disabled: !active.canHeading1,
onSelect: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
},
{
icon: Heading2,
label: 'Heading 2',
shortcut: '⌘⌥2',
active: active.heading2,
disabled: !active.canHeading2,
onSelect: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
},
{
icon: Heading3,
label: 'Heading 3',
shortcut: '⌘⌥3',
active: active.heading3,
disabled: !active.canHeading3,
onSelect: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
},
{
icon: List,
label: 'Bulleted list',
shortcut: '⌘⇧8',
active: active.bulletList,
disabled: !active.canBulletList,
onSelect: () => editor.chain().focus().toggleBulletList().run(),
},
{
icon: ListOrdered,
label: 'Numbered list',
shortcut: '⌘⇧7',
active: active.orderedList,
disabled: !active.canOrderedList,
onSelect: () => editor.chain().focus().toggleOrderedList().run(),
},
{
icon: ListChecks,
label: 'Checklist',
shortcut: '⌘⇧9',
active: active.taskList,
disabled: !active.canTaskList,
onSelect: () => editor.chain().focus().toggleTaskList().run(),
},
{
icon: TextQuote,
label: 'Quote',
shortcut: '⌘⇧B',
active: active.blockquote,
disabled: !active.canBlockquote,
onSelect: () => editor.chain().focus().toggleBlockquote().run(),
},
]}
/>
<ToolbarButton
icon={Bold}
label='Bold'
Expand All @@ -377,83 +464,40 @@ export function EditorBubbleMenu({
isActive={active.italic}
onClick={() => editor.chain().focus().toggleItalic().run()}
/>
<ToolbarButton
icon={Strikethrough}
label='Strikethrough'
shortcut='⌘⇧S'
isActive={active.strike}
onClick={() => editor.chain().focus().toggleStrike().run()}
/>
<ToolbarButton
icon={Highlighter}
label='Highlight'
shortcut='⌘⇧H'
isActive={active.highlight}
onClick={() => editor.chain().focus().toggleMark('highlight').run()}
/>
<ToolbarButton
icon={Code}
label='Code'
shortcut='⌘E'
isActive={active.code}
onClick={() => editor.chain().focus().toggleCode().run()}
/>
<ToolbarButton
icon={LinkIcon}
label='Link'
shortcut='⌘K'
isActive={active.link}
onClick={openLinkEditor}
/>
<ToolbarDivider />
<ToolbarButton
icon={Heading1}
label='Heading 1'
shortcut='⌘⌥1'
isActive={active.heading1}
disabled={!active.canHeading1}
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
/>
<ToolbarButton
icon={Heading2}
label='Heading 2'
shortcut='⌘⌥2'
isActive={active.heading2}
disabled={!active.canHeading2}
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
/>
<ToolbarDivider />
<ToolbarButton
icon={List}
label='Bulleted list'
shortcut='⌘⇧8'
isActive={active.bulletList}
disabled={!active.canBulletList}
onClick={() => editor.chain().focus().toggleBulletList().run()}
/>
<ToolbarButton
icon={ListOrdered}
label='Numbered list'
shortcut='⌘⇧7'
isActive={active.orderedList}
disabled={!active.canOrderedList}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
/>
<ToolbarButton
icon={ListChecks}
label='Checklist'
shortcut='⌘⇧9'
isActive={active.taskList}
disabled={!active.canTaskList}
onClick={() => editor.chain().focus().toggleTaskList().run()}
/>
<ToolbarButton
icon={TextQuote}
label='Quote'
shortcut='⌘⇧B'
isActive={active.blockquote}
disabled={!active.canBlockquote}
onClick={() => editor.chain().focus().toggleBlockquote().run()}
<ToolbarMenu
editor={editor}
label='More formatting'
active={active.strike || active.highlight || active.code}
items={[
{
icon: Strikethrough,
label: 'Strikethrough',
shortcut: '⌘⇧S',
active: active.strike,
onSelect: () => editor.chain().focus().toggleStrike().run(),
},
{
icon: Highlighter,
label: 'Highlight',
shortcut: '⌘⇧H',
active: active.highlight,
onSelect: () => editor.chain().focus().toggleMark('highlight').run(),
},
{
icon: Code,
label: 'Code',
shortcut: '⌘E',
active: active.code,
onSelect: () => editor.chain().focus().toggleCode().run(),
},
]}
/>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
import { useCopyToClipboard } from '@sim/emcn'
import { cn, MENU_STYLES, useCopyToClipboard } from '@sim/emcn'
import { Check, Duplicate, Pencil, Unlink } from '@sim/emcn/icons'
import { getMarkRange } from '@tiptap/core'
import { type Editor, useEditorState } from '@tiptap/react'
Expand Down Expand Up @@ -187,7 +187,11 @@ export function LinkHoverCard({ editor }: LinkHoverCardProps) {
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) dismiss()
}}
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'
className={cn(
MENU_STYLES.surface,
MENU_STYLES.padding,
'z-[var(--z-popover)] flex items-center gap-0.5 transition-opacity duration-150 ease-out'
)}
>
{isEditing ? (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { ComponentType, SVGProps } from 'react'
import {
Button,
Chip,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemLabel,
DropdownMenuShortcut,
DropdownMenuTrigger,
Tooltip,
} from '@sim/emcn'
import { Check, ChevronDown, MoreHorizontal } from '@sim/emcn/icons'
import type { Editor } from '@tiptap/react'

interface ToolbarMenuProps {
editor: Editor
label: string
value?: string
active?: boolean
items: {
label: string
icon: ComponentType<SVGProps<SVGSVGElement>>
shortcut?: string
active: boolean
disabled?: boolean
onSelect: () => void
}[]
}

/**
* Formatting commands share the platform menu while retaining the editor's selection.
* Focusing the trigger before its portal opens keeps TipTap's blur handler inside the toolbar.
*/
export function ToolbarMenu({ editor, label, value, active, items }: ToolbarMenuProps) {
if (!editor.isEditable) return null

return (
<DropdownMenu modal={false}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<DropdownMenuTrigger asChild>
{value ? (
<Chip
rightIcon={ChevronDown}
aria-label={label}
className='h-10 w-[76px] sm:h-[28px]'
onPointerDown={(event) => event.currentTarget.focus()}
>
{value}
</Chip>
) : (
<Button
type='button'
variant={active ? 'active' : 'ghost'}
size='icon'
aria-label={label}
className={cn(
'size-10 focus-visible:bg-[var(--surface-hover)] sm:size-[28px]',
!active && 'hover-hover:bg-[var(--surface-hover)]'
)}
onPointerDown={(event) => event.currentTarget.focus()}
>
<MoreHorizontal className='size-[14px]' />
</Button>
)}
</DropdownMenuTrigger>
</Tooltip.Trigger>
<Tooltip.Content>{label}</Tooltip.Content>
</Tooltip.Root>
<DropdownMenuContent
Comment thread
waleedlatif1 marked this conversation as resolved.
align='start'
className='w-[220px]'
onCloseAutoFocus={(event) => {
if (editor.isFocused) event.preventDefault()
}}
onKeyDown={(event) => event.stopPropagation()}
>
{items.map(({ label: itemLabel, icon: Icon, shortcut, active, disabled, onSelect }) => (
<DropdownMenuItem
key={itemLabel}
role='menuitemcheckbox'
aria-checked={active}
active={active}
disabled={disabled}
onSelect={() => {
if (!editor.isDestroyed && editor.isEditable) onSelect()
}}
>
<Icon className='size-[14px]' />
<DropdownMenuItemLabel label={itemLabel} />
{shortcut && <DropdownMenuShortcut>{shortcut}</DropdownMenuShortcut>}
{active && <Check className='size-[14px]' />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
Loading
Loading