diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu-chrome.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu-chrome.ts index 3affe48b409..b4722862962 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu-chrome.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu-chrome.ts @@ -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' +) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx index 98321f451e4..a1f15a48f08 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx @@ -6,12 +6,14 @@ import { Code, Heading1, Heading2, + Heading3, Highlighter, Italic, Link as LinkIcon, List, ListChecks, ListOrdered, + Pilcrow, Strikethrough, TextQuote, Unlink, @@ -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 @@ -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(), @@ -363,6 +376,80 @@ export function EditorBubbleMenu({ )} + { + 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(), + }, + ]} + /> editor.chain().focus().toggleItalic().run()} /> - editor.chain().focus().toggleStrike().run()} - /> - editor.chain().focus().toggleMark('highlight').run()} - /> - editor.chain().focus().toggleCode().run()} - /> - - editor.chain().focus().toggleHeading({ level: 1 }).run()} - /> - editor.chain().focus().toggleHeading({ level: 2 }).run()} - /> - - editor.chain().focus().toggleBulletList().run()} - /> - editor.chain().focus().toggleOrderedList().run()} - /> - editor.chain().focus().toggleTaskList().run()} - /> - editor.chain().focus().toggleBlockquote().run()} + 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(), + }, + ]} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx index adc890eb2b0..a135a173099 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/link-hover-card.tsx @@ -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' @@ -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 ? ( <> diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-menu.tsx new file mode 100644 index 00000000000..9a87498f089 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-menu.tsx @@ -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> + 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 ( + + + + + {value ? ( + event.currentTarget.focus()} + > + {value} + + ) : ( + + )} + + + {label} + + { + if (editor.isFocused) event.preventDefault() + }} + onKeyDown={(event) => event.stopPropagation()} + > + {items.map(({ label: itemLabel, icon: Icon, shortcut, active, disabled, onSelect }) => ( + { + if (!editor.isDestroyed && editor.isEditable) onSelect() + }} + > + + + {shortcut && {shortcut}} + {active && } + + ))} + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-preview.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-preview.tsx index 661c84440bd..997c3988981 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-preview.tsx @@ -1,7 +1,7 @@ 'use client' import { type ReactElement, useEffect, useRef, useState } from 'react' -import { cn, OverflowText, Popover, PopoverAnchor, PopoverContent } from '@sim/emcn' +import { ChipLink, cn, OverflowText, Popover, PopoverAnchor, PopoverContent } from '@sim/emcn' import { ArrowUpRight } from '@sim/emcn/icons' import { inter } from '@/app/_styles/fonts/inter/inter' import { SourceIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-icon' @@ -73,9 +73,8 @@ export function SourcePreview({ source, children }: SourcePreviewProps) { {open && ( { @@ -118,36 +117,36 @@ function SourcePreviewContent({ source }: Pick) { const description = source.snippet?.trim() || (!source.connectorType && preview?.description?.trim()) return ( -
-
- +
+
+ - - Open - + Open +
{preview?.image && ( )} -
-

{title}

+
+

{title}

{description && ( -

- {description} -

+

{description}

)}
diff --git a/packages/emcn/src/components/chip/chip.tsx b/packages/emcn/src/components/chip/chip.tsx index 939e6345594..098604bd2b2 100644 --- a/packages/emcn/src/components/chip/chip.tsx +++ b/packages/emcn/src/components/chip/chip.tsx @@ -31,7 +31,7 @@ import { * - `chipVariants({...})` → any other element (`
`, `` inner, etc.) * * @remarks - * The implicit **default** variant is the bare pill — transparent, `--surface-hover` on hover. Omit `variant` + * The implicit **default** variant is the bare pill — transparent, `--surface-hover` on hover and keyboard focus. Omit `variant` * to get it (shadcn-style); never write `variant='default'`. Named variants: * `filled` (`--surface-5` light / `--surface-4` dark fill, `--surface-hover` hover) — a borderless surface reserved for * chip FIELDS/TRIGGERS ({@link ChipInput}/{@link ChipDropdown}/{@link ChipSelect}/{@link ChipDatePicker}), **never `Chip` @@ -79,7 +79,11 @@ const chipVariants = cva( fullWidth: { true: 'flex w-full', false: 'inline-flex' }, }, compoundVariants: [ - { variant: ['default', 'filled'], active: false, className: chipHoverSurfaceClass }, + { + variant: ['default', 'filled'], + active: false, + className: `${chipHoverSurfaceClass} focus-visible:bg-[var(--surface-hover)]`, + }, { variant: ['default', 'filled'], active: true, className: chipActiveSurfaceClass }, ], defaultVariants: { variant: 'default', shape: 'default', active: false, fullWidth: false }, diff --git a/packages/emcn/src/components/chip/segmented-control.ts b/packages/emcn/src/components/chip/segmented-control.ts index 183a4954d70..40d45dadae9 100644 --- a/packages/emcn/src/components/chip/segmented-control.ts +++ b/packages/emcn/src/components/chip/segmented-control.ts @@ -19,12 +19,8 @@ export const segmentedControlVariants = cva( } ) -/** The selected segment keeps its raised surface through hover. */ -export const segmentedControlItemVariants = cva( - cn( - chipVariants(), - 'h-full shrink-0 justify-center whitespace-nowrap rounded-[calc(var(--radius-lg)-2px)] outline-hidden focus:outline-hidden focus-visible:outline-hidden' - ), +const itemVariants = cva( + 'h-full shrink-0 justify-center whitespace-nowrap rounded-[calc(var(--radius-lg)-2px)] outline-hidden focus:outline-hidden focus-visible:outline-hidden', { variants: { size: { default: '', compact: 'px-1.5 text-xs' }, @@ -37,3 +33,10 @@ export const segmentedControlItemVariants = cva( defaultVariants: { active: false, size: 'default' }, } ) + +/** The selected segment keeps its raised surface through hover and keyboard focus. */ +export const segmentedControlItemVariants = ({ + active = false, + ...props +}: Parameters[0] = {}) => + cn(chipVariants({ active }), itemVariants({ active, ...props }))