diff --git a/.changeset/great-starfishes-shake.md b/.changeset/great-starfishes-shake.md new file mode 100644 index 0000000000..853171ee19 --- /dev/null +++ b/.changeset/great-starfishes-shake.md @@ -0,0 +1,6 @@ +--- +'@udecode/plate-font': patch +--- + +Add api `api.fontSize.setMark`. +Add utils `toUnitLess`. \ No newline at end of file diff --git a/apps/www/content/docs/cn/components/changelog.mdx b/apps/www/content/docs/cn/components/changelog.mdx index dc4c05df4d..f4318fa66c 100644 --- a/apps/www/content/docs/cn/components/changelog.mdx +++ b/apps/www/content/docs/cn/components/changelog.mdx @@ -10,6 +10,13 @@ toc: true ## 2024年12月 #17 +### 12月27日 #17.7 + +- `fixed-toolbar-buttons`: 添加 `font-size-toolbar-button` +- `floating-toolbar`: 添加 `inline-equation-toolbar-button` +- `turn-into-dropdown-menu`: 修复:转换为其他块后,编辑器应重新获得焦点 +- `insert-dropdown-menu`: 添加 `行内公式` 和 `公式` 并修复焦点问题 +- `slash-input-element`: 添加 `公式` 和 `行内公式` ### 12月25日 #17.6 diff --git a/apps/www/content/docs/cn/font.mdx b/apps/www/content/docs/cn/font.mdx index dc89f06e69..bc3308ff77 100644 --- a/apps/www/content/docs/cn/font.mdx +++ b/apps/www/content/docs/cn/font.mdx @@ -57,6 +57,36 @@ const plugins = [ ### FontSizePlugin +#### API + +##### editor.api.fontSize.setMark + +设置字体大小标记。 + + + + 要设置的字体大小值。 + + + +#### Utils + +##### toUnitLess + +将字体大小值转换为无单位的值。 + + + + 要转换的字体大小值。 + + + + + + 无单位字体大小值。 + + + ### FontWeightPlugin ## API Components diff --git a/apps/www/content/docs/en/components/changelog.mdx b/apps/www/content/docs/en/components/changelog.mdx index 09c9285980..11172ec8ef 100644 --- a/apps/www/content/docs/en/components/changelog.mdx +++ b/apps/www/content/docs/en/components/changelog.mdx @@ -12,6 +12,7 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver ### December 27 #17.7 +- `fixed-toolbar-buttons`: add `font-size-toolbar-button` - `floating-toolbar`: add `inline-equation-toolbar-button` - `turn-into-dropdown-menu`: Fix: after turn into other block, the editor should regain focus. - `insert-dropdown-menu`: add `inline equation` and `equation` & fix the focus issue diff --git a/apps/www/content/docs/en/font.mdx b/apps/www/content/docs/en/font.mdx index d4001cf990..8b3e7e87a3 100644 --- a/apps/www/content/docs/en/font.mdx +++ b/apps/www/content/docs/en/font.mdx @@ -57,6 +57,36 @@ const plugins = [ ### FontSizePlugin +#### API + +##### editor.api.fontSize.setMark + +Set the font size mark. + + + + The font size value to set. + + + +#### Utils + +##### toUnitLess + +Convert a font size value to a unitless value. + + + + The font size value to convert. + + + + + + The font size value without units. + + + ### FontWeightPlugin ## API Components diff --git a/apps/www/public/r/index.json b/apps/www/public/r/index.json index 82f3af3a92..5fafae7ff1 100644 --- a/apps/www/public/r/index.json +++ b/apps/www/public/r/index.json @@ -462,6 +462,33 @@ ], "type": "registry:ui" }, + { + "dependencies": [ + "@udecode/plate-font" + ], + "doc": { + "description": "A toolbar control for adjusting font size.", + "docs": [ + { + "route": "/docs/font" + } + ], + "examples": [ + "list-demo" + ] + }, + "files": [ + { + "path": "plate-ui/font-size-toolbar-button.tsx", + "type": "registry:ui" + } + ], + "name": "font-size-toolbar-button", + "registryDependencies": [ + "toolbar" + ], + "type": "registry:ui" + }, { "dependencies": [], "doc": { @@ -2259,6 +2286,7 @@ "color-dropdown-menu", "comment-toolbar-button", "emoji-dropdown-menu", + "font-size-toolbar-button", "history-toolbar-button", "indent-list-toolbar-button", "indent-todo-toolbar-button", diff --git a/apps/www/public/r/styles/default/fixed-toolbar-buttons.json b/apps/www/public/r/styles/default/fixed-toolbar-buttons.json index 74fb3d2bb8..e0a61a5ef7 100644 --- a/apps/www/public/r/styles/default/fixed-toolbar-buttons.json +++ b/apps/www/public/r/styles/default/fixed-toolbar-buttons.json @@ -14,7 +14,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport {\n BoldPlugin,\n CodePlugin,\n ItalicPlugin,\n StrikethroughPlugin,\n UnderlinePlugin,\n} from '@udecode/plate-basic-marks/react';\nimport { useEditorReadOnly } from '@udecode/plate-common/react';\nimport {\n FontBackgroundColorPlugin,\n FontColorPlugin,\n} from '@udecode/plate-font/react';\nimport { HighlightPlugin } from '@udecode/plate-highlight/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n ArrowUpToLineIcon,\n BaselineIcon,\n BoldIcon,\n Code2Icon,\n HighlighterIcon,\n ItalicIcon,\n PaintBucketIcon,\n StrikethroughIcon,\n UnderlineIcon,\n WandSparklesIcon,\n} from 'lucide-react';\n\nimport { MoreDropdownMenu } from '@/components/plate-ui/more-dropdown-menu';\n\nimport { AIToolbarButton } from './ai-toolbar-button';\nimport { AlignDropdownMenu } from './align-dropdown-menu';\nimport { ColorDropdownMenu } from './color-dropdown-menu';\nimport { CommentToolbarButton } from './comment-toolbar-button';\nimport { EmojiDropdownMenu } from './emoji-dropdown-menu';\nimport { ExportToolbarButton } from './export-toolbar-button';\nimport { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';\nimport {\n BulletedIndentListToolbarButton,\n NumberedIndentListToolbarButton,\n} from './indent-list-toolbar-button';\nimport { IndentTodoToolbarButton } from './indent-todo-toolbar-button';\nimport { IndentToolbarButton } from './indent-toolbar-button';\nimport { InsertDropdownMenu } from './insert-dropdown-menu';\nimport { LineHeightDropdownMenu } from './line-height-dropdown-menu';\nimport { LinkToolbarButton } from './link-toolbar-button';\nimport { MarkToolbarButton } from './mark-toolbar-button';\nimport { MediaToolbarButton } from './media-toolbar-button';\nimport { ModeDropdownMenu } from './mode-dropdown-menu';\nimport { OutdentToolbarButton } from './outdent-toolbar-button';\nimport { TableDropdownMenu } from './table-dropdown-menu';\nimport { ToggleToolbarButton } from './toggle-toolbar-button';\nimport { ToolbarGroup } from './toolbar';\nimport { TurnIntoDropdownMenu } from './turn-into-dropdown-menu';\n\nexport function FixedToolbarButtons() {\n const readOnly = useEditorReadOnly();\n\n return (\n
\n {!readOnly && (\n <>\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n\n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n )}\n\n
\n\n \n \n \n \n \n \n\n \n \n \n
\n );\n}\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport {\n BoldPlugin,\n CodePlugin,\n ItalicPlugin,\n StrikethroughPlugin,\n UnderlinePlugin,\n} from '@udecode/plate-basic-marks/react';\nimport { useEditorReadOnly } from '@udecode/plate-common/react';\nimport {\n FontBackgroundColorPlugin,\n FontColorPlugin,\n} from '@udecode/plate-font/react';\nimport { HighlightPlugin } from '@udecode/plate-highlight/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n ArrowUpToLineIcon,\n BaselineIcon,\n BoldIcon,\n Code2Icon,\n HighlighterIcon,\n ItalicIcon,\n PaintBucketIcon,\n StrikethroughIcon,\n UnderlineIcon,\n WandSparklesIcon,\n} from 'lucide-react';\n\nimport { MoreDropdownMenu } from '@/components/plate-ui/more-dropdown-menu';\n\nimport { AIToolbarButton } from './ai-toolbar-button';\nimport { AlignDropdownMenu } from './align-dropdown-menu';\nimport { ColorDropdownMenu } from './color-dropdown-menu';\nimport { CommentToolbarButton } from './comment-toolbar-button';\nimport { EmojiDropdownMenu } from './emoji-dropdown-menu';\nimport { ExportToolbarButton } from './export-toolbar-button';\nimport { FontSizeToolbarButton } from './font-size-toolbar-button';\nimport { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';\nimport {\n BulletedIndentListToolbarButton,\n NumberedIndentListToolbarButton,\n} from './indent-list-toolbar-button';\nimport { IndentTodoToolbarButton } from './indent-todo-toolbar-button';\nimport { IndentToolbarButton } from './indent-toolbar-button';\nimport { InsertDropdownMenu } from './insert-dropdown-menu';\nimport { LineHeightDropdownMenu } from './line-height-dropdown-menu';\nimport { LinkToolbarButton } from './link-toolbar-button';\nimport { MarkToolbarButton } from './mark-toolbar-button';\nimport { MediaToolbarButton } from './media-toolbar-button';\nimport { ModeDropdownMenu } from './mode-dropdown-menu';\nimport { OutdentToolbarButton } from './outdent-toolbar-button';\nimport { TableDropdownMenu } from './table-dropdown-menu';\nimport { ToggleToolbarButton } from './toggle-toolbar-button';\nimport { ToolbarGroup } from './toolbar';\nimport { TurnIntoDropdownMenu } from './turn-into-dropdown-menu';\n\nexport function FixedToolbarButtons() {\n const readOnly = useEditorReadOnly();\n\n return (\n
\n {!readOnly && (\n <>\n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n\n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n )}\n\n
\n\n \n \n \n \n \n \n\n \n \n \n
\n );\n}\n", "path": "plate-ui/fixed-toolbar-buttons.tsx", "target": "components/plate-ui/fixed-toolbar-buttons.tsx", "type": "registry:ui" @@ -28,6 +28,7 @@ "color-dropdown-menu", "comment-toolbar-button", "emoji-dropdown-menu", + "font-size-toolbar-button", "history-toolbar-button", "indent-list-toolbar-button", "indent-todo-toolbar-button", diff --git a/apps/www/public/r/styles/default/font-size-toolbar-button.json b/apps/www/public/r/styles/default/font-size-toolbar-button.json new file mode 100644 index 0000000000..2e9486665b --- /dev/null +++ b/apps/www/public/r/styles/default/font-size-toolbar-button.json @@ -0,0 +1,29 @@ +{ + "dependencies": [ + "@udecode/plate-font" + ], + "doc": { + "description": "A toolbar control for adjusting font size.", + "docs": [ + { + "route": "/docs/font" + } + ], + "examples": [ + "list-demo" + ] + }, + "files": [ + { + "content": "'use client';\nimport { useState } from 'react';\n\nimport { cn } from '@udecode/cn';\nimport { type TElement, getAboveNode, getMarks } from '@udecode/plate-common';\nimport {\n focusEditor,\n useEditorPlugin,\n useEditorSelector,\n} from '@udecode/plate-common/react';\nimport { BaseFontSizePlugin, toUnitLess } from '@udecode/plate-font';\nimport { FontSizePlugin } from '@udecode/plate-font/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { Minus, Plus } from 'lucide-react';\n\nimport { Popover, PopoverContent, PopoverTrigger } from './popover';\nimport { ToolbarButton } from './toolbar';\n\nconst DEFAULT_FONT_SIZE = '16';\n\nconst FONT_SIZE_MAP = {\n [HEADING_KEYS.h1]: '36',\n [HEADING_KEYS.h2]: '24',\n [HEADING_KEYS.h3]: '20',\n} as const;\n\nconst FONT_SIZES = [\n '8',\n '9',\n '10',\n '12',\n '14',\n '16',\n '18',\n '24',\n '30',\n '36',\n '48',\n '60',\n '72',\n '96',\n] as const;\n\nexport function FontSizeToolbarButton() {\n const [inputValue, setInputValue] = useState(DEFAULT_FONT_SIZE);\n const [isFocused, setIsFocused] = useState(false);\n const { api, editor } = useEditorPlugin(BaseFontSizePlugin);\n\n const cursorFontSize = useEditorSelector((editor) => {\n const marks = getMarks(editor);\n const fontSize = marks?.[FontSizePlugin.key];\n\n if (fontSize) {\n return toUnitLess(fontSize as string);\n }\n\n const [node] =\n getAboveNode(editor, {\n at: editor.selection?.focus,\n }) || [];\n\n return node?.type && node.type in FONT_SIZE_MAP\n ? FONT_SIZE_MAP[node.type as keyof typeof FONT_SIZE_MAP]\n : DEFAULT_FONT_SIZE;\n }, []);\n\n const handleInputChange = () => {\n const newSize = toUnitLess(inputValue);\n\n if (Number.parseInt(newSize) < 1 || Number.parseInt(newSize) > 100) {\n focusEditor(editor);\n\n return;\n }\n if (newSize !== toUnitLess(cursorFontSize)) {\n api.fontSize.setMark(`${newSize}px`);\n }\n\n focusEditor(editor);\n };\n\n const handleFontSizeChange = (delta: number) => {\n const newSize = Number(displayValue) + delta;\n api.fontSize.setMark(`${newSize}px`);\n focusEditor(editor);\n };\n\n const displayValue = isFocused ? inputValue : cursorFontSize;\n\n return (\n
\n handleFontSizeChange(-1)}>\n \n \n\n \n \n {\n setIsFocused(false);\n handleInputChange();\n }}\n onChange={(e) => setInputValue(e.target.value)}\n onFocus={() => {\n setIsFocused(true);\n setInputValue(toUnitLess(cursorFontSize));\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n handleInputChange();\n }\n }}\n data-plate-focus=\"true\"\n type=\"text\"\n />\n \n e.preventDefault()}\n >\n {FONT_SIZES.map((size) => (\n {\n api.fontSize.setMark(`${size}px`);\n setIsFocused(false);\n }}\n data-highlighted={size === displayValue}\n type=\"button\"\n >\n {size}\n \n ))}\n \n \n\n handleFontSizeChange(1)}>\n \n \n
\n );\n}\n", + "path": "plate-ui/font-size-toolbar-button.tsx", + "target": "components/plate-ui/font-size-toolbar-button.tsx", + "type": "registry:ui" + } + ], + "name": "font-size-toolbar-button", + "registryDependencies": [ + "toolbar" + ], + "type": "registry:ui" +} \ No newline at end of file diff --git a/apps/www/public/r/styles/default/indent-list-toolbar-button.json b/apps/www/public/r/styles/default/indent-list-toolbar-button.json index 2f7765eb93..8ab7b8e06a 100644 --- a/apps/www/public/r/styles/default/indent-list-toolbar-button.json +++ b/apps/www/public/r/styles/default/indent-list-toolbar-button.json @@ -15,7 +15,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport { useEditorRef, useEditorSelector } from '@udecode/plate-common/react';\nimport {\n ListStyleType,\n someIndentList,\n toggleIndentList,\n} from '@udecode/plate-indent-list';\nimport { List, ListOrdered } from 'lucide-react';\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\n\nexport function NumberedIndentListToolbarButton() {\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const pressed = useEditorSelector(\n (editor) =>\n someIndentList(editor, [\n ListStyleType.Decimal,\n ListStyleType.LowerAlpha,\n ListStyleType.UpperAlpha,\n ListStyleType.LowerRoman,\n ListStyleType.UpperRoman,\n ]),\n []\n );\n\n return (\n \n {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Decimal,\n });\n }}\n data-state={pressed ? 'on' : 'off'}\n >\n \n \n\n \n \n \n \n\n \n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Decimal,\n })\n }\n >\n Decimal (1, 2, 3)\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.LowerAlpha,\n })\n }\n >\n Lower Alpha (a, b, c)\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.UpperAlpha,\n })\n }\n >\n Upper Alpha (A, B, C)\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.LowerRoman,\n })\n }\n >\n Lower Roman (i, ii, iii)\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.UpperRoman,\n })\n }\n >\n Upper Roman (I, II, III)\n \n \n \n \n \n );\n}\n\nexport function BulletedIndentListToolbarButton() {\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const pressed = useEditorSelector(\n (editor) =>\n someIndentList(editor, [\n ListStyleType.Disc,\n ListStyleType.Circle,\n ListStyleType.Square,\n ]),\n []\n );\n\n return (\n \n {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Disc,\n });\n }}\n data-state={pressed ? 'on' : 'off'}\n >\n \n \n\n \n \n \n \n\n \n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Disc,\n })\n }\n >\n
\n
\n Default\n
\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Circle,\n })\n }\n >\n
\n
\n Circle\n
\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Square,\n })\n }\n >\n
\n
\n Square\n
\n \n \n \n \n \n );\n}\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport { useEditorRef, useEditorSelector } from '@udecode/plate-common/react';\nimport {\n ListStyleType,\n someIndentList,\n toggleIndentList,\n} from '@udecode/plate-indent-list';\nimport { List, ListOrdered } from 'lucide-react';\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\n\nexport function NumberedIndentListToolbarButton() {\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const pressed = useEditorSelector(\n (editor) =>\n someIndentList(editor, [\n ListStyleType.Decimal,\n ListStyleType.LowerAlpha,\n ListStyleType.UpperAlpha,\n ListStyleType.LowerRoman,\n ListStyleType.UpperRoman,\n ]),\n []\n );\n\n return (\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Decimal,\n })\n }\n data-state={pressed ? 'on' : 'off'}\n tooltip=\"Numbered List\"\n >\n \n \n\n \n \n \n \n\n \n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Decimal,\n })\n }\n >\n Decimal (1, 2, 3)\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.LowerAlpha,\n })\n }\n >\n Lower Alpha (a, b, c)\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.UpperAlpha,\n })\n }\n >\n Upper Alpha (A, B, C)\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.LowerRoman,\n })\n }\n >\n Lower Roman (i, ii, iii)\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.UpperRoman,\n })\n }\n >\n Upper Roman (I, II, III)\n \n \n \n \n \n );\n}\n\nexport function BulletedIndentListToolbarButton() {\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const pressed = useEditorSelector(\n (editor) =>\n someIndentList(editor, [\n ListStyleType.Disc,\n ListStyleType.Circle,\n ListStyleType.Square,\n ]),\n []\n );\n\n return (\n \n {\n toggleIndentList(editor, {\n listStyleType: ListStyleType.Disc,\n });\n }}\n data-state={pressed ? 'on' : 'off'}\n tooltip=\"Bulleted List\"\n >\n \n \n\n \n \n \n \n\n \n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Disc,\n })\n }\n >\n
\n
\n Default\n
\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Circle,\n })\n }\n >\n
\n
\n Circle\n
\n \n \n toggleIndentList(editor, {\n listStyleType: ListStyleType.Square,\n })\n }\n >\n
\n
\n Square\n
\n \n \n \n \n \n );\n}\n", "path": "plate-ui/indent-list-toolbar-button.tsx", "target": "components/plate-ui/indent-list-toolbar-button.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/insert-dropdown-menu.json b/apps/www/public/r/styles/default/insert-dropdown-menu.json index 02bd9e2dec..005df56ec7 100644 --- a/apps/www/public/r/styles/default/insert-dropdown-menu.json +++ b/apps/www/public/r/styles/default/insert-dropdown-menu.json @@ -21,7 +21,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { BlockquotePlugin } from '@udecode/plate-block-quote/react';\nimport { CodeBlockPlugin } from '@udecode/plate-code-block/react';\nimport {\n type PlateEditor,\n ParagraphPlugin,\n focusEditor,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport { DatePlugin } from '@udecode/plate-date/react';\nimport { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { TocPlugin } from '@udecode/plate-heading/react';\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';\nimport { LinkPlugin } from '@udecode/plate-link/react';\nimport { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';\nimport { TablePlugin } from '@udecode/plate-table/react';\nimport { TogglePlugin } from '@udecode/plate-toggle/react';\nimport {\n CalendarIcon,\n ChevronRightIcon,\n Columns3Icon,\n FileCodeIcon,\n FilmIcon,\n Heading1Icon,\n Heading2Icon,\n Heading3Icon,\n ImageIcon,\n Link2Icon,\n ListIcon,\n ListOrderedIcon,\n MinusIcon,\n PenToolIcon,\n PilcrowIcon,\n PlusIcon,\n QuoteIcon,\n SquareIcon,\n TableIcon,\n TableOfContentsIcon,\n} from 'lucide-react';\n\nimport {\n insertBlock,\n insertInlineElement,\n} from '@/components/editor/transforms';\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { ToolbarButton } from './toolbar';\n\ntype Group = {\n group: string;\n items: Item[];\n};\n\ninterface Item {\n icon: React.ReactNode;\n onSelect: (editor: PlateEditor, value: string) => void;\n value: string;\n focusEditor?: boolean;\n label?: string;\n}\n\nconst groups: Group[] = [\n {\n group: 'Basic blocks',\n items: [\n {\n icon: ,\n label: 'Paragraph',\n value: ParagraphPlugin.key,\n },\n {\n icon: ,\n label: 'Heading 1',\n value: HEADING_KEYS.h1,\n },\n {\n icon: ,\n label: 'Heading 2',\n value: HEADING_KEYS.h2,\n },\n {\n icon: ,\n label: 'Heading 3',\n value: HEADING_KEYS.h3,\n },\n {\n icon: ,\n label: 'Table',\n value: TablePlugin.key,\n },\n {\n icon: ,\n label: 'Code',\n value: CodeBlockPlugin.key,\n },\n {\n icon: ,\n label: 'Quote',\n value: BlockquotePlugin.key,\n },\n {\n icon: ,\n label: 'Divider',\n value: HorizontalRulePlugin.key,\n },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertBlock(editor, value);\n },\n })),\n },\n {\n group: 'Lists',\n items: [\n {\n icon: ,\n label: 'Bulleted list',\n value: ListStyleType.Disc,\n },\n {\n icon: ,\n label: 'Numbered list',\n value: ListStyleType.Decimal,\n },\n {\n icon: ,\n label: 'To-do list',\n value: INDENT_LIST_KEYS.todo,\n },\n {\n icon: ,\n label: 'Toggle list',\n value: TogglePlugin.key,\n },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertBlock(editor, value);\n },\n })),\n },\n {\n group: 'Media',\n items: [\n {\n icon: ,\n label: 'Image',\n value: ImagePlugin.key,\n },\n {\n icon: ,\n label: 'Embed',\n value: MediaEmbedPlugin.key,\n },\n {\n icon: ,\n label: 'Excalidraw',\n value: ExcalidrawPlugin.key,\n },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertBlock(editor, value);\n },\n })),\n },\n {\n group: 'Advanced blocks',\n items: [\n {\n icon: ,\n label: 'Table of contents',\n value: TocPlugin.key,\n },\n {\n icon: ,\n label: '3 columns',\n value: 'action_three_columns',\n },\n // {\n // focusEditor: false,\n // icon: ,\n // label: 'Equation',\n // value: EquationPlugin.key,\n // },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertBlock(editor, value);\n },\n })),\n },\n {\n group: 'Inline',\n items: [\n {\n icon: ,\n label: 'Link',\n value: LinkPlugin.key,\n },\n {\n focusEditor: true,\n icon: ,\n label: 'Date',\n value: DatePlugin.key,\n },\n // {\n // focusEditor: false,\n // icon: ,\n // label: 'Inline Equation',\n // value: InlineEquationPlugin.key,\n // },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertInlineElement(editor, value);\n },\n })),\n },\n];\n\nexport function InsertDropdownMenu(props: DropdownMenuProps) {\n const editor = useEditorRef();\n const openState = useOpenState();\n\n return (\n \n \n \n \n \n \n\n \n {groups.map(({ group, items: nestedItems }) => (\n \n {nestedItems.map(({ icon, label, value, onSelect }) => (\n {\n onSelect(editor, value);\n focusEditor(editor);\n }}\n >\n {icon}\n {label}\n \n ))}\n \n ))}\n \n \n );\n}\n", + "content": "'use client';\n\nimport React from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { BlockquotePlugin } from '@udecode/plate-block-quote/react';\nimport { CodeBlockPlugin } from '@udecode/plate-code-block/react';\nimport {\n type PlateEditor,\n ParagraphPlugin,\n focusEditor,\n useEditorRef,\n} from '@udecode/plate-common/react';\nimport { DatePlugin } from '@udecode/plate-date/react';\nimport { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { TocPlugin } from '@udecode/plate-heading/react';\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list';\nimport { LinkPlugin } from '@udecode/plate-link/react';\nimport {\n EquationPlugin,\n InlineEquationPlugin,\n} from '@udecode/plate-math/react';\nimport { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';\nimport { TablePlugin } from '@udecode/plate-table/react';\nimport { TogglePlugin } from '@udecode/plate-toggle/react';\nimport {\n CalendarIcon,\n ChevronRightIcon,\n Columns3Icon,\n FileCodeIcon,\n FilmIcon,\n Heading1Icon,\n Heading2Icon,\n Heading3Icon,\n ImageIcon,\n Link2Icon,\n ListIcon,\n ListOrderedIcon,\n MinusIcon,\n PenToolIcon,\n PilcrowIcon,\n PlusIcon,\n QuoteIcon,\n RadicalIcon,\n SquareIcon,\n TableIcon,\n TableOfContentsIcon,\n} from 'lucide-react';\n\nimport {\n insertBlock,\n insertInlineElement,\n} from '@/components/editor/transforms';\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { ToolbarButton } from './toolbar';\n\ntype Group = {\n group: string;\n items: Item[];\n};\n\ninterface Item {\n icon: React.ReactNode;\n onSelect: (editor: PlateEditor, value: string) => void;\n value: string;\n focusEditor?: boolean;\n label?: string;\n}\n\nconst groups: Group[] = [\n {\n group: 'Basic blocks',\n items: [\n {\n icon: ,\n label: 'Paragraph',\n value: ParagraphPlugin.key,\n },\n {\n icon: ,\n label: 'Heading 1',\n value: HEADING_KEYS.h1,\n },\n {\n icon: ,\n label: 'Heading 2',\n value: HEADING_KEYS.h2,\n },\n {\n icon: ,\n label: 'Heading 3',\n value: HEADING_KEYS.h3,\n },\n {\n icon: ,\n label: 'Table',\n value: TablePlugin.key,\n },\n {\n icon: ,\n label: 'Code',\n value: CodeBlockPlugin.key,\n },\n {\n icon: ,\n label: 'Quote',\n value: BlockquotePlugin.key,\n },\n {\n icon: ,\n label: 'Divider',\n value: HorizontalRulePlugin.key,\n },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertBlock(editor, value);\n },\n })),\n },\n {\n group: 'Lists',\n items: [\n {\n icon: ,\n label: 'Bulleted list',\n value: ListStyleType.Disc,\n },\n {\n icon: ,\n label: 'Numbered list',\n value: ListStyleType.Decimal,\n },\n {\n icon: ,\n label: 'To-do list',\n value: INDENT_LIST_KEYS.todo,\n },\n {\n icon: ,\n label: 'Toggle list',\n value: TogglePlugin.key,\n },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertBlock(editor, value);\n },\n })),\n },\n {\n group: 'Media',\n items: [\n {\n icon: ,\n label: 'Image',\n value: ImagePlugin.key,\n },\n {\n icon: ,\n label: 'Embed',\n value: MediaEmbedPlugin.key,\n },\n {\n icon: ,\n label: 'Excalidraw',\n value: ExcalidrawPlugin.key,\n },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertBlock(editor, value);\n },\n })),\n },\n {\n group: 'Advanced blocks',\n items: [\n {\n icon: ,\n label: 'Table of contents',\n value: TocPlugin.key,\n },\n {\n icon: ,\n label: '3 columns',\n value: 'action_three_columns',\n },\n {\n focusEditor: false,\n icon: ,\n label: 'Equation',\n value: EquationPlugin.key,\n },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertBlock(editor, value);\n },\n })),\n },\n {\n group: 'Inline',\n items: [\n {\n icon: ,\n label: 'Link',\n value: LinkPlugin.key,\n },\n {\n focusEditor: true,\n icon: ,\n label: 'Date',\n value: DatePlugin.key,\n },\n {\n focusEditor: false,\n icon: ,\n label: 'Inline Equation',\n value: InlineEquationPlugin.key,\n },\n ].map((item) => ({\n ...item,\n onSelect: (editor, value) => {\n insertInlineElement(editor, value);\n },\n })),\n },\n];\n\nexport function InsertDropdownMenu(props: DropdownMenuProps) {\n const editor = useEditorRef();\n const openState = useOpenState();\n\n return (\n \n \n \n \n \n \n\n \n {groups.map(({ group, items: nestedItems }) => (\n \n {nestedItems.map(({ icon, label, value, onSelect }) => (\n {\n onSelect(editor, value);\n focusEditor(editor);\n }}\n >\n {icon}\n {label}\n \n ))}\n \n ))}\n \n \n );\n}\n", "path": "plate-ui/insert-dropdown-menu.tsx", "target": "components/plate-ui/insert-dropdown-menu.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/media-toolbar-button.json b/apps/www/public/r/styles/default/media-toolbar-button.json index b288486770..539ad99e6d 100644 --- a/apps/www/public/r/styles/default/media-toolbar-button.json +++ b/apps/www/public/r/styles/default/media-toolbar-button.json @@ -18,7 +18,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React, { useCallback, useState } from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { insertNodes, isUrl } from '@udecode/plate-common';\nimport { useEditorRef } from '@udecode/plate-common/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n AudioLinesIcon,\n FileUpIcon,\n FilmIcon,\n ImageIcon,\n LinkIcon,\n} from 'lucide-react';\nimport { toast } from 'sonner';\nimport { useFilePicker } from 'use-file-picker';\n\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from './alert-dialog';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { FloatingInput } from './input';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\n\nconst MEDIA_CONFIG: Record<\n string,\n {\n accept: string[];\n icon: React.ReactNode;\n title: string;\n tooltip: string;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n icon: ,\n title: 'Insert Audio',\n tooltip: 'Audio',\n },\n [FilePlugin.key]: {\n accept: ['*'],\n icon: ,\n title: 'Insert File',\n tooltip: 'File',\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n icon: ,\n title: 'Insert Image',\n tooltip: 'Image',\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n icon: ,\n title: 'Insert Video',\n tooltip: 'Video',\n },\n};\n\nexport function MediaToolbarButton({\n children,\n nodeType,\n ...props\n}: DropdownMenuProps & { nodeType: string }) {\n const currentConfig = MEDIA_CONFIG[nodeType];\n\n const editor = useEditorRef();\n const openState = useOpenState();\n const [dialogOpen, setDialogOpen] = useState(false);\n\n const { openFilePicker } = useFilePicker({\n accept: currentConfig.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n (editor as any).tf.insert.media(updatedFiles);\n },\n });\n\n return (\n <>\n {\n openFilePicker();\n }}\n onKeyDown={(e) => {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n openState.onOpenChange(true);\n }\n }}\n pressed={openState.open}\n tooltip={currentConfig.tooltip}\n >\n \n {currentConfig.icon}\n \n\n \n \n \n \n\n e.stopPropagation()}\n align=\"start\"\n alignOffset={-32}\n >\n \n openFilePicker()}>\n {currentConfig.icon}\n Upload from computer\n \n setDialogOpen(true)}>\n \n Insert via URL\n \n \n \n \n \n\n {\n setDialogOpen(value);\n }}\n >\n \n \n \n \n \n );\n}\n\nfunction MediaUrlDialogContent({\n currentConfig,\n nodeType,\n setOpen,\n}: {\n currentConfig: (typeof MEDIA_CONFIG)[string];\n nodeType: string;\n setOpen: (value: boolean) => void;\n}) {\n const editor = useEditorRef();\n const [url, setUrl] = useState('');\n\n const embedMedia = useCallback(() => {\n if (!isUrl(url)) return toast.error('Invalid URL');\n\n setOpen(false);\n insertNodes(editor, {\n children: [{ text: '' }],\n name: nodeType === FilePlugin.key ? url.split('/').pop() : undefined,\n type: nodeType,\n url,\n });\n }, [url, editor, nodeType, setOpen]);\n\n return (\n <>\n \n {currentConfig.title}\n \n\n \n setUrl(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') embedMedia();\n }}\n label=\"URL\"\n placeholder=\"\"\n type=\"url\"\n autoFocus\n />\n \n\n \n Cancel\n {\n e.preventDefault();\n embedMedia();\n }}\n >\n Accept\n \n \n \n );\n}\n", + "content": "'use client';\n\nimport React, { useCallback, useState } from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { insertNodes, isUrl } from '@udecode/plate-common';\nimport { useEditorRef } from '@udecode/plate-common/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n AudioLinesIcon,\n FileUpIcon,\n FilmIcon,\n ImageIcon,\n LinkIcon,\n} from 'lucide-react';\nimport { toast } from 'sonner';\nimport { useFilePicker } from 'use-file-picker';\n\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from './alert-dialog';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { FloatingInput } from './input';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\n\nconst MEDIA_CONFIG: Record<\n string,\n {\n accept: string[];\n icon: React.ReactNode;\n title: string;\n tooltip: string;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n icon: ,\n title: 'Insert Audio',\n tooltip: 'Audio',\n },\n [FilePlugin.key]: {\n accept: ['*'],\n icon: ,\n title: 'Insert File',\n tooltip: 'File',\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n icon: ,\n title: 'Insert Image',\n tooltip: 'Image',\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n icon: ,\n title: 'Insert Video',\n tooltip: 'Video',\n },\n};\n\nexport function MediaToolbarButton({\n children,\n nodeType,\n ...props\n}: DropdownMenuProps & { nodeType: string }) {\n const currentConfig = MEDIA_CONFIG[nodeType];\n\n const editor = useEditorRef();\n const openState = useOpenState();\n const [dialogOpen, setDialogOpen] = useState(false);\n\n const { openFilePicker } = useFilePicker({\n accept: currentConfig.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n (editor as any).tf.insert.media(updatedFiles);\n },\n });\n\n return (\n <>\n {\n openFilePicker();\n }}\n onKeyDown={(e) => {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n openState.onOpenChange(true);\n }\n }}\n pressed={openState.open}\n >\n \n {currentConfig.icon}\n \n\n \n \n \n \n\n e.stopPropagation()}\n align=\"start\"\n alignOffset={-32}\n >\n \n openFilePicker()}>\n {currentConfig.icon}\n Upload from computer\n \n setDialogOpen(true)}>\n \n Insert via URL\n \n \n \n \n \n\n {\n setDialogOpen(value);\n }}\n >\n \n \n \n \n \n );\n}\n\nfunction MediaUrlDialogContent({\n currentConfig,\n nodeType,\n setOpen,\n}: {\n currentConfig: (typeof MEDIA_CONFIG)[string];\n nodeType: string;\n setOpen: (value: boolean) => void;\n}) {\n const editor = useEditorRef();\n const [url, setUrl] = useState('');\n\n const embedMedia = useCallback(() => {\n if (!isUrl(url)) return toast.error('Invalid URL');\n\n setOpen(false);\n insertNodes(editor, {\n children: [{ text: '' }],\n name: nodeType === FilePlugin.key ? url.split('/').pop() : undefined,\n type: nodeType,\n url,\n });\n }, [url, editor, nodeType, setOpen]);\n\n return (\n <>\n \n {currentConfig.title}\n \n\n \n setUrl(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') embedMedia();\n }}\n label=\"URL\"\n placeholder=\"\"\n type=\"url\"\n autoFocus\n />\n \n\n \n Cancel\n {\n e.preventDefault();\n embedMedia();\n }}\n >\n Accept\n \n \n \n );\n}\n", "path": "plate-ui/media-toolbar-button.tsx", "target": "components/plate-ui/media-toolbar-button.tsx", "type": "registry:ui" diff --git a/apps/www/public/r/styles/default/toolbar.json b/apps/www/public/r/styles/default/toolbar.json index 60d117938c..07029fd1b8 100644 --- a/apps/www/public/r/styles/default/toolbar.json +++ b/apps/www/public/r/styles/default/toolbar.json @@ -7,7 +7,7 @@ }, "files": [ { - "content": "'use client';\n\nimport * as React from 'react';\n\nimport * as ToolbarPrimitive from '@radix-ui/react-toolbar';\nimport { cn, withCn, withRef, withVariants } from '@udecode/cn';\nimport { type VariantProps, cva } from 'class-variance-authority';\nimport { ChevronDown } from 'lucide-react';\n\nimport { Separator } from './separator';\nimport { withTooltip } from './tooltip';\n\nexport const Toolbar = withCn(\n ToolbarPrimitive.Root,\n 'relative flex select-none items-center'\n);\n\nexport const ToolbarToggleGroup = withCn(\n ToolbarPrimitive.ToolbarToggleGroup,\n 'flex items-center'\n);\n\nexport const ToolbarLink = withCn(\n ToolbarPrimitive.Link,\n 'font-medium underline underline-offset-4'\n);\n\nexport const ToolbarSeparator = withCn(\n ToolbarPrimitive.Separator,\n 'mx-2 my-1 w-px shrink-0 bg-border'\n);\n\nconst toolbarButtonVariants = cva(\n cn(\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium text-foreground ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg:not([data-icon])]:size-4'\n ),\n {\n defaultVariants: {\n size: 'sm',\n variant: 'default',\n },\n variants: {\n size: {\n default: 'h-10 px-3',\n lg: 'h-11 px-5',\n sm: 'h-7 px-2',\n },\n variant: {\n default:\n 'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',\n outline:\n 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',\n },\n },\n }\n);\n\nconst dropdownArrowVariants = cva(\n cn(\n 'inline-flex items-center justify-center rounded-r-md text-sm font-medium text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'\n ),\n {\n defaultVariants: {\n size: 'sm',\n variant: 'default',\n },\n variants: {\n size: {\n default: 'h-10 w-6',\n lg: 'h-11 w-8',\n sm: 'h-7 w-4',\n },\n variant: {\n default:\n 'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',\n outline:\n 'border border-l-0 border-input bg-transparent hover:bg-accent hover:text-accent-foreground',\n },\n },\n }\n);\n\nconst ToolbarButton = withTooltip(\n React.forwardRef<\n React.ElementRef,\n {\n isDropdown?: boolean;\n pressed?: boolean;\n } & Omit<\n React.ComponentPropsWithoutRef,\n 'asChild' | 'value'\n > &\n VariantProps\n >(\n (\n { children, className, isDropdown, pressed, size, variant, ...props },\n ref\n ) => {\n return typeof pressed === 'boolean' ? (\n \n \n {isDropdown ? (\n <>\n
\n {children}\n
\n
\n \n
\n \n ) : (\n children\n )}\n \n \n ) : (\n \n {children}\n \n );\n }\n )\n);\nToolbarButton.displayName = 'ToolbarButton';\n\nexport { ToolbarButton };\n\nexport const ToolbarSplitButton = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ children, className, ...props }, ref) => {\n return (\n \n {children}\n \n );\n});\n\nexport const ToolbarSplitButtonPrimary = React.forwardRef<\n React.ElementRef,\n Omit, 'value'>\n>(({ children, className, size, variant, ...props }, ref) => {\n return (\n \n {children}\n \n );\n});\n\nexport const ToolbarSplitButtonSecondary = React.forwardRef<\n HTMLButtonElement,\n React.ComponentPropsWithoutRef<'span'> &\n VariantProps\n>(({ className, size, variant, ...props }, ref) => {\n return (\n e.stopPropagation()}\n role=\"button\"\n {...props}\n >\n \n \n );\n});\n\nToolbarSplitButton.displayName = 'ToolbarButton';\n\nexport const ToolbarToggleItem = withVariants(\n ToolbarPrimitive.ToggleItem,\n toolbarButtonVariants,\n ['variant', 'size']\n);\n\nexport const ToolbarGroup = withRef<'div'>(({ children, className }, ref) => {\n return (\n
\n );\n});\n", + "content": "'use client';\n\nimport * as React from 'react';\n\nimport * as ToolbarPrimitive from '@radix-ui/react-toolbar';\nimport { cn, withCn, withRef, withVariants } from '@udecode/cn';\nimport { type VariantProps, cva } from 'class-variance-authority';\nimport { ChevronDown } from 'lucide-react';\n\nimport { Separator } from './separator';\nimport { withTooltip } from './tooltip';\n\nexport const Toolbar = withCn(\n ToolbarPrimitive.Root,\n 'relative flex select-none items-center'\n);\n\nexport const ToolbarToggleGroup = withCn(\n ToolbarPrimitive.ToolbarToggleGroup,\n 'flex items-center'\n);\n\nexport const ToolbarLink = withCn(\n ToolbarPrimitive.Link,\n 'font-medium underline underline-offset-4'\n);\n\nexport const ToolbarSeparator = withCn(\n ToolbarPrimitive.Separator,\n 'mx-2 my-1 w-px shrink-0 bg-border'\n);\n\nconst toolbarButtonVariants = cva(\n cn(\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium text-foreground ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg:not([data-icon])]:size-4'\n ),\n {\n defaultVariants: {\n size: 'sm',\n variant: 'default',\n },\n variants: {\n size: {\n default: 'h-10 px-3',\n lg: 'h-11 px-5',\n sm: 'h-7 px-2',\n },\n variant: {\n default:\n 'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',\n outline:\n 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',\n },\n },\n }\n);\n\nconst dropdownArrowVariants = cva(\n cn(\n 'inline-flex items-center justify-center rounded-r-md text-sm font-medium text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'\n ),\n {\n defaultVariants: {\n size: 'sm',\n variant: 'default',\n },\n variants: {\n size: {\n default: 'h-10 w-6',\n lg: 'h-11 w-8',\n sm: 'h-7 w-4',\n },\n variant: {\n default:\n 'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',\n outline:\n 'border border-l-0 border-input bg-transparent hover:bg-accent hover:text-accent-foreground',\n },\n },\n }\n);\n\nconst ToolbarButton = withTooltip(\n React.forwardRef<\n React.ElementRef,\n {\n isDropdown?: boolean;\n pressed?: boolean;\n } & Omit<\n React.ComponentPropsWithoutRef,\n 'asChild' | 'value'\n > &\n VariantProps\n >(\n (\n { children, className, isDropdown, pressed, size, variant, ...props },\n ref\n ) => {\n return typeof pressed === 'boolean' ? (\n \n \n {isDropdown ? (\n <>\n
\n {children}\n
\n
\n \n
\n \n ) : (\n children\n )}\n \n \n ) : (\n \n {children}\n \n );\n }\n )\n);\nToolbarButton.displayName = 'ToolbarButton';\n\nexport { ToolbarButton };\n\nexport const ToolbarSplitButton = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ children, className, ...props }, ref) => {\n return (\n \n {children}\n \n );\n});\n\nexport const ToolbarSplitButtonPrimary = withTooltip(\n React.forwardRef<\n React.ElementRef,\n Omit, 'value'>\n >(({ children, className, size, variant, ...props }, ref) => {\n return (\n \n {children}\n \n );\n })\n);\n\nexport const ToolbarSplitButtonSecondary = React.forwardRef<\n HTMLButtonElement,\n React.ComponentPropsWithoutRef<'span'> &\n VariantProps\n>(({ className, size, variant, ...props }, ref) => {\n return (\n e.stopPropagation()}\n role=\"button\"\n {...props}\n >\n \n \n );\n});\n\nToolbarSplitButton.displayName = 'ToolbarButton';\n\nexport const ToolbarToggleItem = withVariants(\n ToolbarPrimitive.ToggleItem,\n toolbarButtonVariants,\n ['variant', 'size']\n);\n\nexport const ToolbarGroup = withRef<'div'>(({ children, className }, ref) => {\n return (\n
\n );\n});\n", "path": "plate-ui/toolbar.tsx", "target": "components/plate-ui/toolbar.tsx", "type": "registry:ui" diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index f4dbe49ae1..0b2001dcce 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -277,6 +277,22 @@ export const Index: Record = { subcategory: "", chunks: [] }, + "font-size-toolbar-button": { + name: "font-size-toolbar-button", + description: "", + type: "registry:ui", + registryDependencies: ["toolbar"], + files: [{ + path: "src/registry/default/plate-ui/font-size-toolbar-button.tsx", + type: "registry:ui", + target: "" + }], + component: React.lazy(() => import("@/registry/default/plate-ui/font-size-toolbar-button.tsx")), + source: "", + category: "", + subcategory: "", + chunks: [] + }, "heading-element": { name: "heading-element", description: "", @@ -1353,7 +1369,7 @@ export const Index: Record = { name: "fixed-toolbar-buttons", description: "", type: "registry:ui", - registryDependencies: ["toolbar","ai-toolbar-button","align-dropdown-menu","color-dropdown-menu","comment-toolbar-button","emoji-dropdown-menu","history-toolbar-button","indent-list-toolbar-button","indent-todo-toolbar-button","indent-toolbar-button","insert-dropdown-menu","line-height-dropdown-menu","link-toolbar-button","mark-toolbar-button","media-toolbar-button","mode-dropdown-menu","more-dropdown-menu","outdent-toolbar-button","table-dropdown-menu","toggle-toolbar-button","turn-into-dropdown-menu","export-toolbar-button"], + registryDependencies: ["toolbar","ai-toolbar-button","align-dropdown-menu","color-dropdown-menu","comment-toolbar-button","emoji-dropdown-menu","font-size-toolbar-button","history-toolbar-button","indent-list-toolbar-button","indent-todo-toolbar-button","indent-toolbar-button","insert-dropdown-menu","line-height-dropdown-menu","link-toolbar-button","mark-toolbar-button","media-toolbar-button","mode-dropdown-menu","more-dropdown-menu","outdent-toolbar-button","table-dropdown-menu","toggle-toolbar-button","turn-into-dropdown-menu","export-toolbar-button"], files: [{ path: "src/registry/default/plate-ui/fixed-toolbar-buttons.tsx", type: "registry:ui", diff --git a/apps/www/src/registry/default/plate-ui/fixed-toolbar-buttons.tsx b/apps/www/src/registry/default/plate-ui/fixed-toolbar-buttons.tsx index b9c5a2d180..c1197a3bf6 100644 --- a/apps/www/src/registry/default/plate-ui/fixed-toolbar-buttons.tsx +++ b/apps/www/src/registry/default/plate-ui/fixed-toolbar-buttons.tsx @@ -42,6 +42,7 @@ import { ColorDropdownMenu } from './color-dropdown-menu'; import { CommentToolbarButton } from './comment-toolbar-button'; import { EmojiDropdownMenu } from './emoji-dropdown-menu'; import { ExportToolbarButton } from './export-toolbar-button'; +import { FontSizeToolbarButton } from './font-size-toolbar-button'; import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button'; import { BulletedIndentListToolbarButton, @@ -88,6 +89,7 @@ export function FixedToolbarButtons() { + diff --git a/apps/www/src/registry/default/plate-ui/font-size-toolbar-button.tsx b/apps/www/src/registry/default/plate-ui/font-size-toolbar-button.tsx new file mode 100644 index 0000000000..433895716c --- /dev/null +++ b/apps/www/src/registry/default/plate-ui/font-size-toolbar-button.tsx @@ -0,0 +1,150 @@ +'use client'; +import { useState } from 'react'; + +import { cn } from '@udecode/cn'; +import { type TElement, getAboveNode, getMarks } from '@udecode/plate-common'; +import { + focusEditor, + useEditorPlugin, + useEditorSelector, +} from '@udecode/plate-common/react'; +import { BaseFontSizePlugin, toUnitLess } from '@udecode/plate-font'; +import { FontSizePlugin } from '@udecode/plate-font/react'; +import { HEADING_KEYS } from '@udecode/plate-heading'; +import { Minus, Plus } from 'lucide-react'; + +import { Popover, PopoverContent, PopoverTrigger } from './popover'; +import { ToolbarButton } from './toolbar'; + +const DEFAULT_FONT_SIZE = '16'; + +const FONT_SIZE_MAP = { + [HEADING_KEYS.h1]: '36', + [HEADING_KEYS.h2]: '24', + [HEADING_KEYS.h3]: '20', +} as const; + +const FONT_SIZES = [ + '8', + '9', + '10', + '12', + '14', + '16', + '18', + '24', + '30', + '36', + '48', + '60', + '72', + '96', +] as const; + +export function FontSizeToolbarButton() { + const [inputValue, setInputValue] = useState(DEFAULT_FONT_SIZE); + const [isFocused, setIsFocused] = useState(false); + const { api, editor } = useEditorPlugin(BaseFontSizePlugin); + + const cursorFontSize = useEditorSelector((editor) => { + const marks = getMarks(editor); + const fontSize = marks?.[FontSizePlugin.key]; + + if (fontSize) { + return toUnitLess(fontSize as string); + } + + const [node] = + getAboveNode(editor, { + at: editor.selection?.focus, + }) || []; + + return node?.type && node.type in FONT_SIZE_MAP + ? FONT_SIZE_MAP[node.type as keyof typeof FONT_SIZE_MAP] + : DEFAULT_FONT_SIZE; + }, []); + + const handleInputChange = () => { + const newSize = toUnitLess(inputValue); + + if (Number.parseInt(newSize) < 1 || Number.parseInt(newSize) > 100) { + focusEditor(editor); + + return; + } + if (newSize !== toUnitLess(cursorFontSize)) { + api.fontSize.setMark(`${newSize}px`); + } + + focusEditor(editor); + }; + + const handleFontSizeChange = (delta: number) => { + const newSize = Number(displayValue) + delta; + api.fontSize.setMark(`${newSize}px`); + focusEditor(editor); + }; + + const displayValue = isFocused ? inputValue : cursorFontSize; + + return ( +
+ handleFontSizeChange(-1)}> + + + + + + { + setIsFocused(false); + handleInputChange(); + }} + onChange={(e) => setInputValue(e.target.value)} + onFocus={() => { + setIsFocused(true); + setInputValue(toUnitLess(cursorFontSize)); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleInputChange(); + } + }} + data-plate-focus="true" + type="text" + /> + + e.preventDefault()} + > + {FONT_SIZES.map((size) => ( + + ))} + + + + handleFontSizeChange(1)}> + + +
+ ); +} diff --git a/apps/www/src/registry/default/plate-ui/indent-list-toolbar-button.tsx b/apps/www/src/registry/default/plate-ui/indent-list-toolbar-button.tsx index c1d809ac33..e9ead8cc3b 100644 --- a/apps/www/src/registry/default/plate-ui/indent-list-toolbar-button.tsx +++ b/apps/www/src/registry/default/plate-ui/indent-list-toolbar-button.tsx @@ -41,15 +41,16 @@ export function NumberedIndentListToolbarButton() { ); return ( - + { + onClick={() => toggleIndentList(editor, { listStyleType: ListStyleType.Decimal, - }); - }} + }) + } data-state={pressed ? 'on' : 'off'} + tooltip="Numbered List" > @@ -128,7 +129,7 @@ export function BulletedIndentListToolbarButton() { ); return ( - + { @@ -137,6 +138,7 @@ export function BulletedIndentListToolbarButton() { }); }} data-state={pressed ? 'on' : 'off'} + tooltip="Bulleted List" > diff --git a/apps/www/src/registry/default/plate-ui/media-toolbar-button.tsx b/apps/www/src/registry/default/plate-ui/media-toolbar-button.tsx index fa9689a1bb..25a3e8c102 100644 --- a/apps/www/src/registry/default/plate-ui/media-toolbar-button.tsx +++ b/apps/www/src/registry/default/plate-ui/media-toolbar-button.tsx @@ -114,9 +114,8 @@ export function MediaToolbarButton({ } }} pressed={openState.open} - tooltip={currentConfig.tooltip} > - + {currentConfig.icon} diff --git a/apps/www/src/registry/default/plate-ui/toolbar.tsx b/apps/www/src/registry/default/plate-ui/toolbar.tsx index 1d27f55edd..38d2064918 100644 --- a/apps/www/src/registry/default/plate-ui/toolbar.tsx +++ b/apps/www/src/registry/default/plate-ui/toolbar.tsx @@ -170,28 +170,30 @@ export const ToolbarSplitButton = React.forwardRef< ); }); -export const ToolbarSplitButtonPrimary = React.forwardRef< - React.ElementRef, - Omit, 'value'> ->(({ children, className, size, variant, ...props }, ref) => { - return ( - - {children} - - ); -}); +export const ToolbarSplitButtonPrimary = withTooltip( + React.forwardRef< + React.ElementRef, + Omit, 'value'> + >(({ children, className, size, variant, ...props }, ref) => { + return ( + + {children} + + ); + }) +); export const ToolbarSplitButtonSecondary = React.forwardRef< HTMLButtonElement, diff --git a/apps/www/src/registry/registry-ui.ts b/apps/www/src/registry/registry-ui.ts index b4c78c8ebe..635abe1c72 100644 --- a/apps/www/src/registry/registry-ui.ts +++ b/apps/www/src/registry/registry-ui.ts @@ -360,6 +360,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';`, 'color-dropdown-menu', 'comment-toolbar-button', 'emoji-dropdown-menu', + 'font-size-toolbar-button', 'history-toolbar-button', 'indent-list-toolbar-button', 'indent-todo-toolbar-button', @@ -1152,6 +1153,20 @@ export const uiNodes: Registry = [ registryDependencies: ['plate-element'], type: 'registry:ui', }, + { + dependencies: ['@udecode/plate-font'], + doc: { + description: 'A toolbar control for adjusting font size.', + docs: [{ route: '/docs/font' }], + examples: ['list-demo'], + }, + files: [ + { path: 'plate-ui/font-size-toolbar-button.tsx', type: 'registry:ui' }, + ], + name: 'font-size-toolbar-button', + registryDependencies: ['toolbar'], + type: 'registry:ui', + }, { dependencies: [], doc: { diff --git a/packages/font/src/lib/BaseFontSizePlugin.ts b/packages/font/src/lib/BaseFontSizePlugin.ts index b9ee69f873..b940d36ace 100644 --- a/packages/font/src/lib/BaseFontSizePlugin.ts +++ b/packages/font/src/lib/BaseFontSizePlugin.ts @@ -1,26 +1,46 @@ -import { createSlatePlugin } from '@udecode/plate-common'; +import { + type PluginConfig, + bindFirst, + createTSlatePlugin, +} from '@udecode/plate-common'; -export const BaseFontSizePlugin = createSlatePlugin({ +import { setFontSize } from './utils'; + +export type BaseFontSizeConfig = PluginConfig< + 'fontSize', + {}, + { + fontSize: { + setMark: (fontSize: string) => void; + }; + } +>; + +export const BaseFontSizePlugin = createTSlatePlugin({ key: 'fontSize', inject: { nodeProps: { nodeKey: 'fontSize', }, }, -}).extend(({ type }) => ({ - parsers: { - html: { - deserializer: { - isLeaf: true, - parse: ({ element }) => ({ [type]: element.style.fontSize }), - rules: [ - { - validStyle: { - fontSize: '*', +}) + .extend(({ type }) => ({ + parsers: { + html: { + deserializer: { + isLeaf: true, + parse: ({ element }) => ({ [type]: element.style.fontSize }), + rules: [ + { + validStyle: { + fontSize: '*', + }, }, - }, - ], + ], + }, }, }, - }, -})); + })) + .extendApi(({ editor }) => ({ + setMark: bindFirst(setFontSize, editor), + })); diff --git a/packages/font/src/lib/index.ts b/packages/font/src/lib/index.ts index 3526ae15dc..93eac919ca 100644 --- a/packages/font/src/lib/index.ts +++ b/packages/font/src/lib/index.ts @@ -8,3 +8,4 @@ export * from './BaseFontFamilyPlugin'; export * from './BaseFontSizePlugin'; export * from './BaseFontWeightPlugin'; export * from './transforms/index'; +export * from './utils/index'; diff --git a/packages/font/src/lib/utils/index.ts b/packages/font/src/lib/utils/index.ts new file mode 100644 index 0000000000..f4b63d8f72 --- /dev/null +++ b/packages/font/src/lib/utils/index.ts @@ -0,0 +1,6 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './setFontSize'; +export * from './toUnitLess'; diff --git a/packages/font/src/lib/utils/setFontSize.ts b/packages/font/src/lib/utils/setFontSize.ts new file mode 100644 index 0000000000..76a59185c8 --- /dev/null +++ b/packages/font/src/lib/utils/setFontSize.ts @@ -0,0 +1,9 @@ +import { type SlateEditor, setMarks } from '@udecode/plate-common'; + +import { BaseFontSizePlugin } from '../BaseFontSizePlugin'; + +export const setFontSize = (editor: SlateEditor, fontSize: string): void => { + setMarks(editor, { + [BaseFontSizePlugin.key]: fontSize, + }); +}; diff --git a/packages/font/src/lib/utils/toUnitLess.ts b/packages/font/src/lib/utils/toUnitLess.ts new file mode 100644 index 0000000000..1cf3e8db70 --- /dev/null +++ b/packages/font/src/lib/utils/toUnitLess.ts @@ -0,0 +1,14 @@ +// return '0' if value not valid +export const toUnitLess = (value: string): string => { + const match = /\d+/.exec(value); + + if (!match) return '0'; + + const num = Number(match[0]); + + if (value.endsWith('rem')) { + return (num * 16).toString(); + } + + return num.toString(); +}; diff --git a/templates/plate-playground-template/pnpm-lock.yaml b/templates/plate-playground-template/pnpm-lock.yaml index ba579231ef..2ad82f9a31 100644 --- a/templates/plate-playground-template/pnpm-lock.yaml +++ b/templates/plate-playground-template/pnpm-lock.yaml @@ -7109,8 +7109,8 @@ snapshots: '@typescript-eslint/parser': 8.18.1(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.2(eslint@8.57.1) eslint-plugin-react-hooks: 5.1.0(eslint@8.57.1) @@ -7133,7 +7133,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 @@ -7145,22 +7145,22 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.18.1(eslint@8.57.1)(typescript@5.7.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7171,7 +7171,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.1(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.0 is-glob: 4.0.3 diff --git a/templates/plate-playground-template/src/components/plate-ui/fixed-toolbar-buttons.tsx b/templates/plate-playground-template/src/components/plate-ui/fixed-toolbar-buttons.tsx index 8ea968f6a4..8ad418eeb4 100644 --- a/templates/plate-playground-template/src/components/plate-ui/fixed-toolbar-buttons.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/fixed-toolbar-buttons.tsx @@ -42,6 +42,7 @@ import { ColorDropdownMenu } from './color-dropdown-menu'; import { CommentToolbarButton } from './comment-toolbar-button'; import { EmojiDropdownMenu } from './emoji-dropdown-menu'; import { ExportToolbarButton } from './export-toolbar-button'; +import { FontSizeToolbarButton } from './font-size-toolbar-button'; import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button'; import { BulletedIndentListToolbarButton, @@ -88,6 +89,7 @@ export function FixedToolbarButtons() { + diff --git a/templates/plate-playground-template/src/components/plate-ui/font-size-toolbar-button.tsx b/templates/plate-playground-template/src/components/plate-ui/font-size-toolbar-button.tsx new file mode 100644 index 0000000000..433895716c --- /dev/null +++ b/templates/plate-playground-template/src/components/plate-ui/font-size-toolbar-button.tsx @@ -0,0 +1,150 @@ +'use client'; +import { useState } from 'react'; + +import { cn } from '@udecode/cn'; +import { type TElement, getAboveNode, getMarks } from '@udecode/plate-common'; +import { + focusEditor, + useEditorPlugin, + useEditorSelector, +} from '@udecode/plate-common/react'; +import { BaseFontSizePlugin, toUnitLess } from '@udecode/plate-font'; +import { FontSizePlugin } from '@udecode/plate-font/react'; +import { HEADING_KEYS } from '@udecode/plate-heading'; +import { Minus, Plus } from 'lucide-react'; + +import { Popover, PopoverContent, PopoverTrigger } from './popover'; +import { ToolbarButton } from './toolbar'; + +const DEFAULT_FONT_SIZE = '16'; + +const FONT_SIZE_MAP = { + [HEADING_KEYS.h1]: '36', + [HEADING_KEYS.h2]: '24', + [HEADING_KEYS.h3]: '20', +} as const; + +const FONT_SIZES = [ + '8', + '9', + '10', + '12', + '14', + '16', + '18', + '24', + '30', + '36', + '48', + '60', + '72', + '96', +] as const; + +export function FontSizeToolbarButton() { + const [inputValue, setInputValue] = useState(DEFAULT_FONT_SIZE); + const [isFocused, setIsFocused] = useState(false); + const { api, editor } = useEditorPlugin(BaseFontSizePlugin); + + const cursorFontSize = useEditorSelector((editor) => { + const marks = getMarks(editor); + const fontSize = marks?.[FontSizePlugin.key]; + + if (fontSize) { + return toUnitLess(fontSize as string); + } + + const [node] = + getAboveNode(editor, { + at: editor.selection?.focus, + }) || []; + + return node?.type && node.type in FONT_SIZE_MAP + ? FONT_SIZE_MAP[node.type as keyof typeof FONT_SIZE_MAP] + : DEFAULT_FONT_SIZE; + }, []); + + const handleInputChange = () => { + const newSize = toUnitLess(inputValue); + + if (Number.parseInt(newSize) < 1 || Number.parseInt(newSize) > 100) { + focusEditor(editor); + + return; + } + if (newSize !== toUnitLess(cursorFontSize)) { + api.fontSize.setMark(`${newSize}px`); + } + + focusEditor(editor); + }; + + const handleFontSizeChange = (delta: number) => { + const newSize = Number(displayValue) + delta; + api.fontSize.setMark(`${newSize}px`); + focusEditor(editor); + }; + + const displayValue = isFocused ? inputValue : cursorFontSize; + + return ( +
+ handleFontSizeChange(-1)}> + + + + + + { + setIsFocused(false); + handleInputChange(); + }} + onChange={(e) => setInputValue(e.target.value)} + onFocus={() => { + setIsFocused(true); + setInputValue(toUnitLess(cursorFontSize)); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleInputChange(); + } + }} + data-plate-focus="true" + type="text" + /> + + e.preventDefault()} + > + {FONT_SIZES.map((size) => ( + + ))} + + + + handleFontSizeChange(1)}> + + +
+ ); +} diff --git a/templates/plate-playground-template/src/components/plate-ui/indent-list-toolbar-button.tsx b/templates/plate-playground-template/src/components/plate-ui/indent-list-toolbar-button.tsx index c1d809ac33..e9ead8cc3b 100644 --- a/templates/plate-playground-template/src/components/plate-ui/indent-list-toolbar-button.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/indent-list-toolbar-button.tsx @@ -41,15 +41,16 @@ export function NumberedIndentListToolbarButton() { ); return ( - + { + onClick={() => toggleIndentList(editor, { listStyleType: ListStyleType.Decimal, - }); - }} + }) + } data-state={pressed ? 'on' : 'off'} + tooltip="Numbered List" > @@ -128,7 +129,7 @@ export function BulletedIndentListToolbarButton() { ); return ( - + { @@ -137,6 +138,7 @@ export function BulletedIndentListToolbarButton() { }); }} data-state={pressed ? 'on' : 'off'} + tooltip="Bulleted List" > diff --git a/templates/plate-playground-template/src/components/plate-ui/insert-dropdown-menu.tsx b/templates/plate-playground-template/src/components/plate-ui/insert-dropdown-menu.tsx index b53ca0a4eb..e9ac6a5fa5 100644 --- a/templates/plate-playground-template/src/components/plate-ui/insert-dropdown-menu.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/insert-dropdown-menu.tsx @@ -19,6 +19,10 @@ import { TocPlugin } from '@udecode/plate-heading/react'; import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react'; import { INDENT_LIST_KEYS, ListStyleType } from '@udecode/plate-indent-list'; import { LinkPlugin } from '@udecode/plate-link/react'; +import { + EquationPlugin, + InlineEquationPlugin, +} from '@udecode/plate-math/react'; import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react'; import { TablePlugin } from '@udecode/plate-table/react'; import { TogglePlugin } from '@udecode/plate-toggle/react'; @@ -40,6 +44,7 @@ import { PilcrowIcon, PlusIcon, QuoteIcon, + RadicalIcon, SquareIcon, TableIcon, TableOfContentsIcon, @@ -192,12 +197,12 @@ const groups: Group[] = [ label: '3 columns', value: 'action_three_columns', }, - // { - // focusEditor: false, - // icon: , - // label: 'Equation', - // value: EquationPlugin.key, - // }, + { + focusEditor: false, + icon: , + label: 'Equation', + value: EquationPlugin.key, + }, ].map((item) => ({ ...item, onSelect: (editor, value) => { @@ -219,12 +224,12 @@ const groups: Group[] = [ label: 'Date', value: DatePlugin.key, }, - // { - // focusEditor: false, - // icon: , - // label: 'Inline Equation', - // value: InlineEquationPlugin.key, - // }, + { + focusEditor: false, + icon: , + label: 'Inline Equation', + value: InlineEquationPlugin.key, + }, ].map((item) => ({ ...item, onSelect: (editor, value) => { diff --git a/templates/plate-playground-template/src/components/plate-ui/media-toolbar-button.tsx b/templates/plate-playground-template/src/components/plate-ui/media-toolbar-button.tsx index fa9689a1bb..25a3e8c102 100644 --- a/templates/plate-playground-template/src/components/plate-ui/media-toolbar-button.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/media-toolbar-button.tsx @@ -114,9 +114,8 @@ export function MediaToolbarButton({ } }} pressed={openState.open} - tooltip={currentConfig.tooltip} > - + {currentConfig.icon} diff --git a/templates/plate-playground-template/src/components/plate-ui/toolbar.tsx b/templates/plate-playground-template/src/components/plate-ui/toolbar.tsx index 1d27f55edd..38d2064918 100644 --- a/templates/plate-playground-template/src/components/plate-ui/toolbar.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/toolbar.tsx @@ -170,28 +170,30 @@ export const ToolbarSplitButton = React.forwardRef< ); }); -export const ToolbarSplitButtonPrimary = React.forwardRef< - React.ElementRef, - Omit, 'value'> ->(({ children, className, size, variant, ...props }, ref) => { - return ( - - {children} - - ); -}); +export const ToolbarSplitButtonPrimary = withTooltip( + React.forwardRef< + React.ElementRef, + Omit, 'value'> + >(({ children, className, size, variant, ...props }, ref) => { + return ( + + {children} + + ); + }) +); export const ToolbarSplitButtonSecondary = React.forwardRef< HTMLButtonElement,