prosekit-registry 0.0.9 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/r/react-example-change-tracking.json +2 -2
  2. package/dist/r/react-example-code-block-themes.json +2 -2
  3. package/dist/r/react-example-hard-break.json +1 -1
  4. package/dist/r/react-example-keymap.json +1 -1
  5. package/dist/r/react-example-link-mark-view.json +1 -1
  6. package/dist/r/react-example-loro.json +1 -1
  7. package/dist/r/react-example-notion.json +8 -8
  8. package/dist/r/react-example-page.json +1 -1
  9. package/dist/r/react-example-readonly.json +1 -1
  10. package/dist/r/react-example-strike.json +1 -1
  11. package/dist/r/react-example-text-align.json +1 -1
  12. package/dist/r/react-example-text-color.json +1 -1
  13. package/dist/r/react-example-tweet.json +2 -2
  14. package/dist/r/react-example-unmount.json +2 -2
  15. package/dist/r/react-example-user-menu-dynamic.json +1 -1
  16. package/dist/r/react-example-view-adapter.json +1 -1
  17. package/dist/r/react-example-yjs.json +1 -1
  18. package/dist/r/react-ui-block-handle.json +1 -1
  19. package/dist/r/react-ui-button.json +1 -1
  20. package/dist/r/react-ui-code-block-view.json +1 -1
  21. package/dist/r/react-ui-drop-indicator.json +1 -1
  22. package/dist/r/react-ui-image-upload-popover.json +1 -1
  23. package/dist/r/react-ui-image-view.json +1 -1
  24. package/dist/r/react-ui-inline-menu.json +1 -1
  25. package/dist/r/react-ui-search.json +1 -1
  26. package/dist/r/react-ui-slash-menu.json +3 -3
  27. package/dist/r/react-ui-table-handle.json +1 -1
  28. package/dist/r/react-ui-tag-menu.json +1 -1
  29. package/dist/r/react-ui-toolbar.json +1 -1
  30. package/dist/r/react-ui-user-menu.json +1 -1
  31. package/dist/r/react-ui-word-counter.json +1 -1
  32. package/package.json +1 -1
  33. package/dist/package.json +0 -7
@@ -12,13 +12,13 @@
12
12
  "path": "registry/src/react/examples/change-tracking/editor-diff.tsx",
13
13
  "type": "registry:component",
14
14
  "target": "components/editor/examples/change-tracking/editor-diff.tsx",
15
- "content": "'use client'\nimport { defineBasicExtension } from 'prosekit/basic'\nimport { createEditor, union } from 'prosekit/core'\nimport { defineCommitViewer, type Commit } from 'prosekit/extensions/commit'\nimport { defineReadonly } from 'prosekit/extensions/readonly'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nexport default function EditorDiff(props: { commit: Commit }) {\n const editor = useMemo(() => {\n const extension = union(\n defineBasicExtension(),\n defineReadonly(),\n defineCommitViewer(props.commit),\n )\n return createEditor({ extension })\n }, [props.commit])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n </div>\n </div>\n </ProseKit>\n )\n}\n"
15
+ "content": "'use client'\n\nimport { defineBasicExtension } from 'prosekit/basic'\nimport { createEditor, union } from 'prosekit/core'\nimport { defineCommitViewer, type Commit } from 'prosekit/extensions/commit'\nimport { defineReadonly } from 'prosekit/extensions/readonly'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nexport default function EditorDiff(props: { commit: Commit }) {\n const editor = useMemo(() => {\n const extension = union(\n defineBasicExtension(),\n defineReadonly(),\n defineCommitViewer(props.commit),\n )\n return createEditor({ extension })\n }, [props.commit])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n </div>\n </div>\n </ProseKit>\n )\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "registry/src/react/examples/change-tracking/editor-main.tsx",
19
19
  "type": "registry:component",
20
20
  "target": "components/editor/examples/change-tracking/editor-main.tsx",
21
- "content": "'use client'\nimport 'prosekit/basic/style.css'\nimport 'prosekit/basic/typography.css'\n\nimport { defineBasicExtension } from 'prosekit/basic'\nimport { createEditor, union, type NodeJSON } from 'prosekit/core'\nimport { defineCommitRecorder, type CommitRecorder } from 'prosekit/extensions/commit'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nexport default function EditorMain(props: {\n commitRecorder: CommitRecorder\n initialContent?: NodeJSON\n}) {\n const editor = useMemo(() => {\n const extension = union(\n defineBasicExtension(),\n defineCommitRecorder(props.commitRecorder),\n )\n return createEditor({ extension, defaultContent: props.initialContent })\n }, [props.commitRecorder, props.initialContent])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n </div>\n </div>\n </ProseKit>\n )\n}\n"
21
+ "content": "'use client'\n\nimport 'prosekit/basic/style.css'\nimport 'prosekit/basic/typography.css'\n\nimport { defineBasicExtension } from 'prosekit/basic'\nimport { createEditor, union, type NodeJSON } from 'prosekit/core'\nimport { defineCommitRecorder, type CommitRecorder } from 'prosekit/extensions/commit'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nexport default function EditorMain(props: {\n commitRecorder: CommitRecorder\n initialContent?: NodeJSON\n}) {\n const editor = useMemo(() => {\n const extension = union(\n defineBasicExtension(),\n defineCommitRecorder(props.commitRecorder),\n )\n return createEditor({ extension, defaultContent: props.initialContent })\n }, [props.commitRecorder, props.initialContent])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n </div>\n </div>\n </ProseKit>\n )\n}\n"
22
22
  },
23
23
  {
24
24
  "path": "registry/src/react/examples/change-tracking/editor.tsx",
@@ -33,13 +33,13 @@
33
33
  "path": "registry/src/react/examples/code-block-themes/theme-selector.tsx",
34
34
  "type": "registry:component",
35
35
  "target": "components/editor/examples/code-block-themes/theme-selector.tsx",
36
- "content": "'use client'\nimport { defineCodeBlockShiki, shikiBundledThemesInfo, type ShikiBundledTheme } from 'prosekit/extensions/code-block'\nimport { useExtension } from 'prosekit/react'\nimport { useMemo, useState } from 'react'\n\nexport function ThemeSelector() {\n const [theme, setTheme] = useState('github-dark')\n const extension = useMemo(() => {\n return defineCodeBlockShiki({ themes: [theme as ShikiBundledTheme] })\n }, [theme])\n useExtension(extension)\n\n return (\n <>\n <label htmlFor=\"code-block-theme-selector\">Theme</label>\n <select\n id=\"code-block-theme-selector\"\n value={theme}\n onChange={(event) => setTheme(event.target.value)}\n className=\"outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700\"\n >\n {shikiBundledThemesInfo.map((info) => (\n <option key={info.id} value={info.id}>\n {info.id}\n </option>\n ))}\n </select>\n </>\n )\n}\n"
36
+ "content": "'use client'\n\nimport { defineCodeBlockShiki, shikiBundledThemesInfo, type ShikiBundledTheme } from 'prosekit/extensions/code-block'\nimport { useExtension } from 'prosekit/react'\nimport { useMemo, useState } from 'react'\n\nexport function ThemeSelector() {\n const [theme, setTheme] = useState('github-dark')\n const extension = useMemo(() => {\n return defineCodeBlockShiki({ themes: [theme as ShikiBundledTheme] })\n }, [theme])\n useExtension(extension)\n\n return (\n <>\n <label htmlFor=\"code-block-theme-selector\">Theme</label>\n <select\n id=\"code-block-theme-selector\"\n value={theme}\n onChange={(event) => setTheme(event.target.value)}\n className=\"outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700\"\n >\n {shikiBundledThemesInfo.map((info) => (\n <option key={info.id} value={info.id}>\n {info.id}\n </option>\n ))}\n </select>\n </>\n )\n}\n"
37
37
  },
38
38
  {
39
39
  "path": "registry/src/react/examples/code-block-themes/toolbar.tsx",
40
40
  "type": "registry:component",
41
41
  "target": "components/editor/examples/code-block-themes/toolbar.tsx",
42
- "content": "'use client'\nimport { ThemeSelector } from './theme-selector'\n\nexport default function Toolbar() {\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <ThemeSelector />\n </div>\n )\n}\n"
42
+ "content": "'use client'\n\nimport { ThemeSelector } from './theme-selector'\n\nexport default function Toolbar() {\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <ThemeSelector />\n </div>\n )\n}\n"
43
43
  }
44
44
  ],
45
45
  "meta": {
@@ -33,7 +33,7 @@
33
33
  "path": "registry/src/react/examples/hard-break/toolbar.tsx",
34
34
  "type": "registry:component",
35
35
  "target": "components/editor/examples/hard-break/toolbar.tsx",
36
- "content": "'use client'\nimport { useEditor } from 'prosekit/react'\n\nimport { Button } from '../../ui/button'\n\nimport type { EditorExtension } from './extension'\n\nexport default function Toolbar() {\n const editor = useEditor<EditorExtension>({ update: true })\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button\n pressed={false}\n disabled={!editor.commands.insertHardBreak.canExec()}\n onClick={() => editor.commands.insertHardBreak()}\n >\n Insert Hard Break\n </Button>\n </div>\n )\n}\n"
36
+ "content": "'use client'\n\nimport { useEditor } from 'prosekit/react'\n\nimport { Button } from '../../ui/button'\n\nimport type { EditorExtension } from './extension'\n\nexport default function Toolbar() {\n const editor = useEditor<EditorExtension>({ update: true })\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button\n pressed={false}\n disabled={!editor.commands.insertHardBreak.canExec()}\n onClick={() => editor.commands.insertHardBreak()}\n >\n Insert Hard Break\n </Button>\n </div>\n )\n}\n"
37
37
  }
38
38
  ],
39
39
  "meta": {
@@ -32,7 +32,7 @@
32
32
  "path": "registry/src/react/examples/keymap/toolbar.tsx",
33
33
  "type": "registry:component",
34
34
  "target": "components/editor/examples/keymap/toolbar.tsx",
35
- "content": "'use client'\nimport { useState } from 'react'\n\nimport { Button } from '../../ui/button'\n\nimport { useSubmitKeymap } from './use-submit-keymap'\n\nexport default function Toolbar(props: {\n onSubmit: (hotkey: string) => void\n}) {\n const [hotkey, setHotkey] = useState<'Shift-Enter' | 'Enter'>('Shift-Enter')\n useSubmitKeymap(hotkey, props.onSubmit)\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button\n pressed={hotkey === 'Shift-Enter'}\n onClick={() => setHotkey('Shift-Enter')}\n >\n <span className=\"mr-1\">Submit with</span>\n <kbd>Shift + Enter</kbd>\n </Button>\n\n <Button pressed={hotkey === 'Enter'} onClick={() => setHotkey('Enter')}>\n <span className=\"mr-1\">Submit with</span>\n <kbd>Enter</kbd>\n </Button>\n </div>\n )\n}\n"
35
+ "content": "'use client'\n\nimport { useState } from 'react'\n\nimport { Button } from '../../ui/button'\n\nimport { useSubmitKeymap } from './use-submit-keymap'\n\nexport default function Toolbar(props: {\n onSubmit: (hotkey: string) => void\n}) {\n const [hotkey, setHotkey] = useState<'Shift-Enter' | 'Enter'>('Shift-Enter')\n useSubmitKeymap(hotkey, props.onSubmit)\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button\n pressed={hotkey === 'Shift-Enter'}\n onClick={() => setHotkey('Shift-Enter')}\n >\n <span className=\"mr-1\">Submit with</span>\n <kbd>Shift + Enter</kbd>\n </Button>\n\n <Button pressed={hotkey === 'Enter'} onClick={() => setHotkey('Enter')}>\n <span className=\"mr-1\">Submit with</span>\n <kbd>Enter</kbd>\n </Button>\n </div>\n )\n}\n"
36
36
  },
37
37
  {
38
38
  "path": "registry/src/react/examples/keymap/use-submit-keymap.ts",
@@ -32,7 +32,7 @@
32
32
  "path": "registry/src/react/examples/link-mark-view/link-view.tsx",
33
33
  "type": "registry:component",
34
34
  "target": "components/editor/examples/link-mark-view/link-view.tsx",
35
- "content": "'use client'\nimport type { ReactMarkViewProps } from 'prosekit/react'\nimport { useEffect, useState } from 'react'\n\nconst colors = [\n '#f06292',\n '#ba68c8',\n '#9575cd',\n '#7986cb',\n '#64b5f6',\n '#4fc3f7',\n '#4dd0e1',\n '#4db6ac',\n '#81c784',\n '#aed581',\n '#ffb74d',\n '#ffa726',\n '#ff8a65',\n '#d4e157',\n '#ffd54f',\n '#ffecb3',\n]\n\nfunction pickRandomColor() {\n return colors[Math.floor(Math.random() * colors.length)]\n}\n\nexport default function Link(props: ReactMarkViewProps) {\n const [color, setColor] = useState(colors[0])\n const href = props.mark.attrs.href as string\n\n useEffect(() => {\n const interval = setInterval(() => {\n setColor(pickRandomColor())\n }, 1000)\n return () => clearInterval(interval)\n }, [])\n\n return (\n <a\n href={href}\n ref={props.contentRef}\n style={{ color, transition: 'color 1s ease-in-out' }}\n >\n </a>\n )\n}\n"
35
+ "content": "'use client'\n\nimport type { ReactMarkViewProps } from 'prosekit/react'\nimport { useEffect, useState } from 'react'\n\nconst colors = [\n '#f06292',\n '#ba68c8',\n '#9575cd',\n '#7986cb',\n '#64b5f6',\n '#4fc3f7',\n '#4dd0e1',\n '#4db6ac',\n '#81c784',\n '#aed581',\n '#ffb74d',\n '#ffa726',\n '#ff8a65',\n '#d4e157',\n '#ffd54f',\n '#ffecb3',\n]\n\nfunction pickRandomColor() {\n return colors[Math.floor(Math.random() * colors.length)]\n}\n\nexport default function Link(props: ReactMarkViewProps) {\n const [color, setColor] = useState(colors[0])\n const href = props.mark.attrs.href as string\n\n useEffect(() => {\n const interval = setInterval(() => {\n setColor(pickRandomColor())\n }, 1000)\n return () => clearInterval(interval)\n }, [])\n\n return (\n <a\n href={href}\n ref={props.contentRef}\n style={{ color, transition: 'color 1s ease-in-out' }}\n >\n </a>\n )\n}\n"
36
36
  }
37
37
  ],
38
38
  "meta": {
@@ -16,7 +16,7 @@
16
16
  "path": "registry/src/react/examples/loro/editor-component.tsx",
17
17
  "type": "registry:component",
18
18
  "target": "components/editor/examples/loro/editor-component.tsx",
19
- "content": "'use client'\nimport 'prosekit/basic/style.css'\nimport 'prosekit/basic/typography.css'\nimport 'prosekit/extensions/loro/style.css'\n\nimport type { CursorAwareness, LoroDocType } from 'loro-prosemirror'\nimport { createEditor } from 'prosekit/core'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nimport { Toolbar } from '../../ui/toolbar'\n\nimport { defineExtension } from './extension'\n\nexport default function EditorComponent(props: {\n loro: LoroDocType\n awareness: CursorAwareness\n}) {\n const editor = useMemo(() => {\n const extension = defineExtension(props.loro, props.awareness)\n return createEditor({ extension })\n }, [props.loro, props.awareness])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <Toolbar />\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n </div>\n </div>\n </ProseKit>\n )\n}\n"
19
+ "content": "'use client'\n\nimport 'prosekit/basic/style.css'\nimport 'prosekit/basic/typography.css'\nimport 'prosekit/extensions/loro/style.css'\n\nimport type { CursorAwareness, LoroDocType } from 'loro-prosemirror'\nimport { createEditor } from 'prosekit/core'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nimport { Toolbar } from '../../ui/toolbar'\n\nimport { defineExtension } from './extension'\n\nexport default function EditorComponent(props: {\n loro: LoroDocType\n awareness: CursorAwareness\n}) {\n const editor = useMemo(() => {\n const extension = defineExtension(props.loro, props.awareness)\n return createEditor({ extension })\n }, [props.loro, props.awareness])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <Toolbar />\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n </div>\n </div>\n </ProseKit>\n )\n}\n"
20
20
  },
21
21
  {
22
22
  "path": "registry/src/react/examples/loro/editor.tsx",
@@ -26,13 +26,13 @@
26
26
  "path": "registry/src/react/examples/notion/block-handle-menu.tsx",
27
27
  "type": "registry:component",
28
28
  "target": "components/editor/examples/notion/block-handle-menu.tsx",
29
- "content": "'use client'\nimport { Menu } from '@base-ui/react'\nimport type { Editor } from 'prosekit/core'\nimport { clsx } from 'prosekit/core'\nimport type { ListAttrs } from 'prosekit/extensions/list'\nimport { useEditorDerivedValue } from 'prosekit/react'\nimport { useState } from 'react'\n\nimport type { EditorExtension } from './extension'\n\nconst POPUP_CLASSNAME =\n 'origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300 w-50'\n\nconst ITEM_CLASSNAME =\n 'flex items-center justify-between gap-2 cursor-default py-2 px-3 text-sm leading-4 outline-none select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900'\n\nconst TEXT_COLOR_CLASSNAME = clsx(\n `border rounded-sm relative after:absolute after:inset-0 after:flex after:items-center after:justify-center after:content-['A']`,\n)\n\ninterface Props {\n children: React.ReactElement\n}\n\ninterface SubmenuInfo {\n key: string\n label: string\n iconClassName?: string\n isAvailable: boolean\n children: ItemInfo[]\n}\n\ninterface MenuItemInfo {\n key: string\n label: string\n isActive: boolean\n isAvailable: boolean\n iconClassName?: string\n shortcut?: string\n danger?: boolean\n onClick: () => void\n children?: never\n}\n\ntype ItemInfo = SubmenuInfo | MenuItemInfo\n\nfunction getActiveBlockType(editor: Editor<EditorExtension>) {\n if (editor.nodes.heading.isActive({ level: 1 })) {\n return 'h1'\n }\n\n if (editor.nodes.heading.isActive({ level: 2 })) {\n return 'h2'\n }\n\n if (editor.nodes.heading.isActive({ level: 3 })) {\n return 'h3'\n }\n\n if (editor.nodes.list.isActive({ kind: 'bullet' })) {\n return 'bullet-list'\n }\n\n if (editor.nodes.list.isActive({ kind: 'ordered' })) {\n return 'ordered-list'\n }\n\n if (editor.nodes.list.isActive({ kind: 'task' })) {\n return 'task-list'\n }\n\n if (editor.nodes.list.isActive({ kind: 'toggle' })) {\n return 'toggle-list'\n }\n\n if (editor.nodes.image.isActive()) {\n return 'image'\n }\n\n return 'text'\n}\n\nfunction turnIntoList(editor: Editor<EditorExtension>, attrs: ListAttrs) {\n editor.commands.setParagraph()\n editor.commands.wrapInList(attrs)\n}\n\nfunction getMenuItems(editor: Editor<EditorExtension>): ItemInfo[] {\n const activeBlockType = getActiveBlockType(editor)\n\n return [\n {\n key: 'turn-into',\n label: 'Turn into',\n iconClassName: 'i-lucide-refresh-cw',\n isAvailable: activeBlockType !== 'image',\n children: [\n {\n key: 'text',\n label: 'Text',\n iconClassName: 'i-lucide-type',\n isActive: activeBlockType === 'text',\n isAvailable: editor.commands.setParagraph.canExec(),\n onClick: () => editor.commands.setParagraph(),\n },\n {\n key: 'h1',\n label: 'Heading 1',\n iconClassName: 'i-lucide-heading-1',\n isActive: activeBlockType === 'h1',\n isAvailable: editor.commands.setHeading.canExec({ level: 1 }),\n onClick: () => editor.commands.setHeading({ level: 1 }),\n },\n {\n key: 'h2',\n label: 'Heading 2',\n iconClassName: 'i-lucide-heading-2',\n isActive: activeBlockType === 'h2',\n isAvailable: editor.commands.setHeading.canExec({ level: 2 }),\n onClick: () => editor.commands.setHeading({ level: 2 }),\n },\n {\n key: 'h3',\n label: 'Heading 3',\n iconClassName: 'i-lucide-heading-3',\n isActive: activeBlockType === 'h3',\n isAvailable: editor.commands.setHeading.canExec({ level: 3 }),\n onClick: () => editor.commands.setHeading({ level: 3 }),\n },\n {\n key: 'bullet-list',\n label: 'Bullet list',\n iconClassName: 'i-lucide-list',\n isActive: activeBlockType === 'bullet-list',\n isAvailable: editor.commands.wrapInList.canExec({ kind: 'bullet' }),\n onClick: () => turnIntoList(editor, { kind: 'bullet' }),\n },\n {\n key: 'ordered-list',\n label: 'Ordered list',\n iconClassName: 'i-lucide-list-ordered',\n isActive: activeBlockType === 'ordered-list',\n isAvailable: editor.commands.wrapInList.canExec({ kind: 'ordered' }),\n onClick: () => turnIntoList(editor, { kind: 'ordered' }),\n },\n {\n key: 'task-list',\n label: 'Task list',\n iconClassName: 'i-lucide-list-checks',\n isActive: activeBlockType === 'task-list',\n isAvailable: editor.commands.wrapInList.canExec({ kind: 'task' }),\n onClick: () => turnIntoList(editor, { kind: 'task' }),\n },\n {\n key: 'toggle-list',\n label: 'Toggle list',\n iconClassName: 'i-lucide-list-collapse',\n isActive: activeBlockType === 'toggle-list',\n isAvailable: editor.commands.wrapInList.canExec({ kind: 'toggle' }),\n onClick: () => turnIntoList(editor, { kind: 'toggle' }),\n },\n ],\n },\n {\n key: 'color',\n label: 'Color',\n iconClassName: 'i-lucide-paint-roller',\n isAvailable: activeBlockType !== 'image',\n children: [\n {\n key: 'default',\n label: 'Default Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-current text-current'),\n isActive: !editor.marks.textColor.isActive(),\n isAvailable: editor.commands.removeTextColor.canExec(),\n onClick: () => editor.commands.removeTextColor(),\n },\n {\n key: 'gray',\n label: 'Gray Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-gray-300 text-gray-500'),\n isActive: editor.marks.textColor.isActive({ color: 'gray' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'gray' }),\n onClick: () => editor.commands.addTextColor({ color: 'gray' }),\n },\n {\n key: 'orange',\n label: 'Orange Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-orange-300 text-orange-500'),\n isActive: editor.marks.textColor.isActive({ color: 'orange' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'orange' }),\n onClick: () => editor.commands.addTextColor({ color: 'orange' }),\n },\n {\n key: 'yellow',\n label: 'Yellow Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-yellow-300 text-yellow-500'),\n isActive: editor.marks.textColor.isActive({ color: 'yellow' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'yellow' }),\n onClick: () => editor.commands.addTextColor({ color: 'yellow' }),\n },\n {\n key: 'green',\n label: 'Green Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-green-300 text-green-500'),\n isActive: editor.marks.textColor.isActive({ color: 'green' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'green' }),\n onClick: () => editor.commands.addTextColor({ color: 'green' }),\n },\n {\n key: 'blue',\n label: 'Blue Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-blue-300 text-blue-500'),\n isActive: editor.marks.textColor.isActive({ color: 'blue' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'blue' }),\n onClick: () => editor.commands.addTextColor({ color: 'blue' }),\n },\n {\n key: 'purple',\n label: 'Purple Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-purple-300 text-purple-500'),\n isActive: editor.marks.textColor.isActive({ color: 'purple' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'purple' }),\n onClick: () => editor.commands.addTextColor({ color: 'purple' }),\n },\n {\n key: 'pink',\n label: 'Pink Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-pink-300 text-pink-500'),\n isActive: editor.marks.textColor.isActive({ color: 'pink' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'pink' }),\n onClick: () => editor.commands.addTextColor({ color: 'pink' }),\n },\n {\n key: 'red',\n label: 'Red Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-red-300 text-red-500'),\n isActive: editor.marks.textColor.isActive({ color: 'red' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'red' }),\n onClick: () => editor.commands.addTextColor({ color: 'red' }),\n },\n ],\n },\n {\n key: 'delete',\n label: 'Delete',\n iconClassName: 'i-lucide-trash-2',\n shortcut: 'Del',\n danger: true,\n isActive: false,\n isAvailable: true,\n onClick: () => editor.view.dispatch(editor.view.state.tr.deleteSelection()),\n },\n ]\n}\n\nfunction BlockHandleItem(props: { item: ItemInfo }) {\n if (!props.item.isAvailable) {\n return null\n } else if (props.item.children) {\n return (\n <Menu.SubmenuRoot>\n <Menu.SubmenuTrigger className={ITEM_CLASSNAME}>\n {props.item.iconClassName && <span className={clsx('inline-block size-4', props.item.iconClassName)} />}\n <span className=\"flex-1\">{props.item.label}</span>\n <span className=\"inline-block size-4 i-lucide-chevron-right opacity-50\">\n </span>\n </Menu.SubmenuTrigger>\n <Menu.Portal>\n <Menu.Positioner align=\"center\">\n <Menu.Popup className={POPUP_CLASSNAME}>\n {props.item.children.map(item => <BlockHandleItem key={item.key} item={item} />)}\n </Menu.Popup>\n </Menu.Positioner>\n </Menu.Portal>\n </Menu.SubmenuRoot>\n )\n } else {\n return (\n <Menu.Item\n className={clsx(ITEM_CLASSNAME, 'group')}\n onClick={props.item.onClick}\n >\n {props.item.iconClassName && <span className={clsx('inline-block size-5', props.item.iconClassName)} />}\n <span className={clsx('flex-1', props.item.danger && 'group-data-highlighted:text-red-500')}>{props.item.label}</span>\n {props.item.isActive && <span className=\"inline-block size-4 i-lucide-check\"></span>}\n {!props.item.isActive && props.item.shortcut && <span className=\"opacity-50\">{props.item.shortcut}</span>}\n </Menu.Item>\n )\n }\n}\n\nexport default function BlockHandleMenu(props: Props) {\n const [open, setOpen] = useState(false)\n\n const items = useEditorDerivedValue(getMenuItems)\n\n return (\n <Menu.Root\n open={open}\n onOpenChange={(open, details) => {\n // ignore the event to open the menu because by default Menu is opened\n // by a `mousedown` event but we only want to open the menu by a `click`\n // event.\n if (open && details.reason === 'trigger-press') {\n return\n }\n setOpen(open)\n }}\n >\n <Menu.Trigger\n render={props.children}\n nativeButton={false}\n onClick={(event) => {\n event.preventDefault()\n setOpen(open => !open)\n }}\n >\n </Menu.Trigger>\n <Menu.Portal>\n <Menu.Backdrop className=\"size-dvw flex fixed inset-0 opacity-0\" />\n <Menu.Positioner className=\"outline-none\" side=\"right\" align=\"center\">\n <Menu.Popup className={POPUP_CLASSNAME}>\n {items.map(item => <BlockHandleItem key={item.key} item={item} />)}\n </Menu.Popup>\n </Menu.Positioner>\n </Menu.Portal>\n </Menu.Root>\n )\n}\n"
29
+ "content": "'use client'\n\nimport { Menu } from '@base-ui/react'\nimport type { Editor } from 'prosekit/core'\nimport { clsx } from 'prosekit/core'\nimport type { ListAttrs } from 'prosekit/extensions/list'\nimport { useEditorDerivedValue } from 'prosekit/react'\nimport { useState } from 'react'\n\nimport type { EditorExtension } from './extension'\n\nconst POPUP_CLASSNAME =\n 'origin-[var(--transform-origin)] rounded-md bg-[canvas] py-1 text-gray-900 shadow-lg shadow-gray-200 outline outline-1 outline-gray-200 transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0 dark:shadow-none dark:-outline-offset-1 dark:outline-gray-300 w-50'\n\nconst ITEM_CLASSNAME =\n 'flex items-center justify-between gap-2 cursor-default py-2 px-3 text-sm leading-4 outline-none select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900'\n\nconst TEXT_COLOR_CLASSNAME = clsx(\n `border rounded-sm relative after:absolute after:inset-0 after:flex after:items-center after:justify-center after:content-['A']`,\n)\n\ninterface Props {\n children: React.ReactElement\n}\n\ninterface SubmenuInfo {\n key: string\n label: string\n iconClassName?: string\n isAvailable: boolean\n children: ItemInfo[]\n}\n\ninterface MenuItemInfo {\n key: string\n label: string\n isActive: boolean\n isAvailable: boolean\n iconClassName?: string\n shortcut?: string\n danger?: boolean\n onClick: () => void\n children?: never\n}\n\ntype ItemInfo = SubmenuInfo | MenuItemInfo\n\nfunction getActiveBlockType(editor: Editor<EditorExtension>) {\n if (editor.nodes.heading.isActive({ level: 1 })) {\n return 'h1'\n }\n\n if (editor.nodes.heading.isActive({ level: 2 })) {\n return 'h2'\n }\n\n if (editor.nodes.heading.isActive({ level: 3 })) {\n return 'h3'\n }\n\n if (editor.nodes.list.isActive({ kind: 'bullet' })) {\n return 'bullet-list'\n }\n\n if (editor.nodes.list.isActive({ kind: 'ordered' })) {\n return 'ordered-list'\n }\n\n if (editor.nodes.list.isActive({ kind: 'task' })) {\n return 'task-list'\n }\n\n if (editor.nodes.list.isActive({ kind: 'toggle' })) {\n return 'toggle-list'\n }\n\n if (editor.nodes.image.isActive()) {\n return 'image'\n }\n\n return 'text'\n}\n\nfunction turnIntoList(editor: Editor<EditorExtension>, attrs: ListAttrs) {\n editor.commands.setParagraph()\n editor.commands.wrapInList(attrs)\n}\n\nfunction getMenuItems(editor: Editor<EditorExtension>): ItemInfo[] {\n const activeBlockType = getActiveBlockType(editor)\n\n return [\n {\n key: 'turn-into',\n label: 'Turn into',\n iconClassName: 'i-lucide-refresh-cw',\n isAvailable: activeBlockType !== 'image',\n children: [\n {\n key: 'text',\n label: 'Text',\n iconClassName: 'i-lucide-type',\n isActive: activeBlockType === 'text',\n isAvailable: editor.commands.setParagraph.canExec(),\n onClick: () => editor.commands.setParagraph(),\n },\n {\n key: 'h1',\n label: 'Heading 1',\n iconClassName: 'i-lucide-heading-1',\n isActive: activeBlockType === 'h1',\n isAvailable: editor.commands.setHeading.canExec({ level: 1 }),\n onClick: () => editor.commands.setHeading({ level: 1 }),\n },\n {\n key: 'h2',\n label: 'Heading 2',\n iconClassName: 'i-lucide-heading-2',\n isActive: activeBlockType === 'h2',\n isAvailable: editor.commands.setHeading.canExec({ level: 2 }),\n onClick: () => editor.commands.setHeading({ level: 2 }),\n },\n {\n key: 'h3',\n label: 'Heading 3',\n iconClassName: 'i-lucide-heading-3',\n isActive: activeBlockType === 'h3',\n isAvailable: editor.commands.setHeading.canExec({ level: 3 }),\n onClick: () => editor.commands.setHeading({ level: 3 }),\n },\n {\n key: 'bullet-list',\n label: 'Bullet list',\n iconClassName: 'i-lucide-list',\n isActive: activeBlockType === 'bullet-list',\n isAvailable: editor.commands.wrapInList.canExec({ kind: 'bullet' }),\n onClick: () => turnIntoList(editor, { kind: 'bullet' }),\n },\n {\n key: 'ordered-list',\n label: 'Ordered list',\n iconClassName: 'i-lucide-list-ordered',\n isActive: activeBlockType === 'ordered-list',\n isAvailable: editor.commands.wrapInList.canExec({ kind: 'ordered' }),\n onClick: () => turnIntoList(editor, { kind: 'ordered' }),\n },\n {\n key: 'task-list',\n label: 'Task list',\n iconClassName: 'i-lucide-list-checks',\n isActive: activeBlockType === 'task-list',\n isAvailable: editor.commands.wrapInList.canExec({ kind: 'task' }),\n onClick: () => turnIntoList(editor, { kind: 'task' }),\n },\n {\n key: 'toggle-list',\n label: 'Toggle list',\n iconClassName: 'i-lucide-list-collapse',\n isActive: activeBlockType === 'toggle-list',\n isAvailable: editor.commands.wrapInList.canExec({ kind: 'toggle' }),\n onClick: () => turnIntoList(editor, { kind: 'toggle' }),\n },\n ],\n },\n {\n key: 'color',\n label: 'Color',\n iconClassName: 'i-lucide-paint-roller',\n isAvailable: activeBlockType !== 'image',\n children: [\n {\n key: 'default',\n label: 'Default Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-current text-current'),\n isActive: !editor.marks.textColor.isActive(),\n isAvailable: editor.commands.removeTextColor.canExec(),\n onClick: () => editor.commands.removeTextColor(),\n },\n {\n key: 'gray',\n label: 'Gray Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-gray-300 text-gray-500'),\n isActive: editor.marks.textColor.isActive({ color: 'gray' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'gray' }),\n onClick: () => editor.commands.addTextColor({ color: 'gray' }),\n },\n {\n key: 'orange',\n label: 'Orange Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-orange-300 text-orange-500'),\n isActive: editor.marks.textColor.isActive({ color: 'orange' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'orange' }),\n onClick: () => editor.commands.addTextColor({ color: 'orange' }),\n },\n {\n key: 'yellow',\n label: 'Yellow Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-yellow-300 text-yellow-500'),\n isActive: editor.marks.textColor.isActive({ color: 'yellow' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'yellow' }),\n onClick: () => editor.commands.addTextColor({ color: 'yellow' }),\n },\n {\n key: 'green',\n label: 'Green Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-green-300 text-green-500'),\n isActive: editor.marks.textColor.isActive({ color: 'green' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'green' }),\n onClick: () => editor.commands.addTextColor({ color: 'green' }),\n },\n {\n key: 'blue',\n label: 'Blue Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-blue-300 text-blue-500'),\n isActive: editor.marks.textColor.isActive({ color: 'blue' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'blue' }),\n onClick: () => editor.commands.addTextColor({ color: 'blue' }),\n },\n {\n key: 'purple',\n label: 'Purple Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-purple-300 text-purple-500'),\n isActive: editor.marks.textColor.isActive({ color: 'purple' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'purple' }),\n onClick: () => editor.commands.addTextColor({ color: 'purple' }),\n },\n {\n key: 'pink',\n label: 'Pink Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-pink-300 text-pink-500'),\n isActive: editor.marks.textColor.isActive({ color: 'pink' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'pink' }),\n onClick: () => editor.commands.addTextColor({ color: 'pink' }),\n },\n {\n key: 'red',\n label: 'Red Text',\n iconClassName: clsx(TEXT_COLOR_CLASSNAME, 'border-red-300 text-red-500'),\n isActive: editor.marks.textColor.isActive({ color: 'red' }),\n isAvailable: editor.commands.addTextColor.canExec({ color: 'red' }),\n onClick: () => editor.commands.addTextColor({ color: 'red' }),\n },\n ],\n },\n {\n key: 'delete',\n label: 'Delete',\n iconClassName: 'i-lucide-trash-2',\n shortcut: 'Del',\n danger: true,\n isActive: false,\n isAvailable: true,\n onClick: () => editor.view.dispatch(editor.view.state.tr.deleteSelection()),\n },\n ]\n}\n\nfunction BlockHandleItem(props: { item: ItemInfo }) {\n if (!props.item.isAvailable) {\n return null\n } else if (props.item.children) {\n return (\n <Menu.SubmenuRoot>\n <Menu.SubmenuTrigger className={ITEM_CLASSNAME}>\n {props.item.iconClassName && <span className={clsx('inline-block size-4', props.item.iconClassName)} />}\n <span className=\"flex-1\">{props.item.label}</span>\n <span className=\"inline-block size-4 i-lucide-chevron-right opacity-50\">\n </span>\n </Menu.SubmenuTrigger>\n <Menu.Portal>\n <Menu.Positioner align=\"center\">\n <Menu.Popup className={POPUP_CLASSNAME}>\n {props.item.children.map(item => <BlockHandleItem key={item.key} item={item} />)}\n </Menu.Popup>\n </Menu.Positioner>\n </Menu.Portal>\n </Menu.SubmenuRoot>\n )\n } else {\n return (\n <Menu.Item\n className={clsx(ITEM_CLASSNAME, 'group')}\n onClick={props.item.onClick}\n >\n {props.item.iconClassName && <span className={clsx('inline-block size-5', props.item.iconClassName)} />}\n <span className={clsx('flex-1', props.item.danger && 'group-data-highlighted:text-red-500')}>{props.item.label}</span>\n {props.item.isActive && <span className=\"inline-block size-4 i-lucide-check\"></span>}\n {!props.item.isActive && props.item.shortcut && <span className=\"opacity-50\">{props.item.shortcut}</span>}\n </Menu.Item>\n )\n }\n}\n\nexport default function BlockHandleMenu(props: Props) {\n const [open, setOpen] = useState(false)\n\n const items = useEditorDerivedValue(getMenuItems)\n\n return (\n <Menu.Root\n open={open}\n onOpenChange={(open, details) => {\n // ignore the event to open the menu because by default Menu is opened\n // by a `mousedown` event but we only want to open the menu by a `click`\n // event.\n if (open && details.reason === 'trigger-press') {\n return\n }\n setOpen(open)\n }}\n >\n <Menu.Trigger\n render={props.children}\n nativeButton={false}\n onClick={(event) => {\n event.preventDefault()\n setOpen(open => !open)\n }}\n >\n </Menu.Trigger>\n <Menu.Portal>\n <Menu.Backdrop className=\"size-dvw flex fixed inset-0 opacity-0\" />\n <Menu.Positioner className=\"outline-none\" side=\"right\" align=\"center\">\n <Menu.Popup className={POPUP_CLASSNAME}>\n {items.map(item => <BlockHandleItem key={item.key} item={item} />)}\n </Menu.Popup>\n </Menu.Positioner>\n </Menu.Portal>\n </Menu.Root>\n )\n}\n"
30
30
  },
31
31
  {
32
32
  "path": "registry/src/react/examples/notion/block-handle.tsx",
33
33
  "type": "registry:component",
34
34
  "target": "components/editor/examples/notion/block-handle.tsx",
35
- "content": "'use client'\nimport { Tooltip } from '@base-ui/react/tooltip'\nimport { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopup, BlockHandlePositioner, BlockHandleRoot } from 'prosekit/react/block-handle'\n\nimport BlockHandleMenu from './block-handle-menu'\n\ninterface Props {\n enabled: boolean\n dir?: 'ltr' | 'rtl'\n}\n\nexport default function BlockHandle(props: Props) {\n if (!props.enabled) {\n return null\n }\n\n return (\n <Tooltip.Provider>\n <BlockHandleRoot>\n <BlockHandlePositioner\n placement={props.dir === 'rtl' ? 'right' : 'left'}\n className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\"\n >\n <BlockHandlePopup className=\"flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100\">\n <Tooltip.Root>\n <Tooltip.Trigger className=\"m-0 p-0\">\n <BlockHandleAdd className=\"h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50\">\n <div className=\"i-lucide-plus size-5 block\" />\n </BlockHandleAdd>\n </Tooltip.Trigger>\n <Tooltip.Portal>\n <Tooltip.Positioner sideOffset={10} side=\"bottom\">\n <Tooltip.Popup className=\"\n flex flex-col justify-center items-center\n px-2 py-1\n rounded-md\n bg-[canvas]\n text-sm\n z-100\n origin-(--transform-origin)\n shadow-lg shadow-gray-200 outline-1 outline-gray-200\n transition-[transform,scale,opacity]\n data-ending-style:opacity-0 data-ending-style:scale-90\n data-instant:transition-none\n data-starting-style:opacity-0 data-starting-style:scale-90\n dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1\">\n <span>\n <span>Click{' '}</span>\n <span className=\"opacity-50\">to add below</span>\n </span>\n <span>\n <span>Option-click{' '}</span>\n <span className=\"opacity-50\">to add above</span>\n </span>\n </Tooltip.Popup>\n </Tooltip.Positioner>\n </Tooltip.Portal>\n </Tooltip.Root>\n <Tooltip.Root>\n <Tooltip.Trigger className=\"m-0 p-0\">\n <BlockHandleMenu>\n <BlockHandleDraggable className=\"h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50\">\n <div className=\"i-lucide-grip-vertical size-5 block\" />\n </BlockHandleDraggable>\n </BlockHandleMenu>\n </Tooltip.Trigger>\n <Tooltip.Portal>\n <Tooltip.Positioner sideOffset={10} side=\"bottom\">\n <Tooltip.Popup className=\"\n flex flex-col justify-center items-center\n px-2 py-1\n rounded-md\n bg-[canvas]\n text-sm\n z-100\n origin-(--transform-origin)\n shadow-lg shadow-gray-200 outline-1 outline-gray-200\n transition-[transform,scale,opacity]\n data-ending-style:opacity-0 data-ending-style:scale-90\n data-instant:transition-none\n data-starting-style:opacity-0 data-starting-style:scale-90\n dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1\">\n <span>\n <span>Drag{' '}</span>\n <span className=\"opacity-50\">to move</span>\n </span>\n <span>\n <span>Click{' '}</span>\n <span className=\"opacity-50\">to open menu</span>\n </span>\n </Tooltip.Popup>\n </Tooltip.Positioner>\n </Tooltip.Portal>\n </Tooltip.Root>\n </BlockHandlePopup>\n </BlockHandlePositioner>\n </BlockHandleRoot>\n </Tooltip.Provider>\n )\n}\n"
35
+ "content": "'use client'\n\nimport { Tooltip } from '@base-ui/react/tooltip'\nimport { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopup, BlockHandlePositioner, BlockHandleRoot } from 'prosekit/react/block-handle'\n\nimport BlockHandleMenu from './block-handle-menu'\n\ninterface Props {\n enabled: boolean\n dir?: 'ltr' | 'rtl'\n}\n\nexport default function BlockHandle(props: Props) {\n if (!props.enabled) {\n return null\n }\n\n return (\n <Tooltip.Provider>\n <BlockHandleRoot>\n <BlockHandlePositioner\n placement={props.dir === 'rtl' ? 'right' : 'left'}\n className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\"\n >\n <BlockHandlePopup className=\"flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100\">\n <Tooltip.Root>\n <Tooltip.Trigger className=\"m-0 p-0\">\n <BlockHandleAdd className=\"h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50\">\n <div className=\"i-lucide-plus size-5 block\" />\n </BlockHandleAdd>\n </Tooltip.Trigger>\n <Tooltip.Portal>\n <Tooltip.Positioner sideOffset={10} side=\"bottom\">\n <Tooltip.Popup className=\"\n flex flex-col justify-center items-center\n px-2 py-1\n rounded-md\n bg-[canvas]\n text-sm\n z-100\n origin-(--transform-origin)\n shadow-lg shadow-gray-200 outline-1 outline-gray-200\n transition-[transform,scale,opacity]\n data-ending-style:opacity-0 data-ending-style:scale-90\n data-instant:transition-none\n data-starting-style:opacity-0 data-starting-style:scale-90\n dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1\">\n <span>\n <span>Click{' '}</span>\n <span className=\"opacity-50\">to add below</span>\n </span>\n <span>\n <span>Option-click{' '}</span>\n <span className=\"opacity-50\">to add above</span>\n </span>\n </Tooltip.Popup>\n </Tooltip.Positioner>\n </Tooltip.Portal>\n </Tooltip.Root>\n <Tooltip.Root>\n <Tooltip.Trigger className=\"m-0 p-0\">\n <BlockHandleMenu>\n <BlockHandleDraggable className=\"h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50\">\n <div className=\"i-lucide-grip-vertical size-5 block\" />\n </BlockHandleDraggable>\n </BlockHandleMenu>\n </Tooltip.Trigger>\n <Tooltip.Portal>\n <Tooltip.Positioner sideOffset={10} side=\"bottom\">\n <Tooltip.Popup className=\"\n flex flex-col justify-center items-center\n px-2 py-1\n rounded-md\n bg-[canvas]\n text-sm\n z-100\n origin-(--transform-origin)\n shadow-lg shadow-gray-200 outline-1 outline-gray-200\n transition-[transform,scale,opacity]\n data-ending-style:opacity-0 data-ending-style:scale-90\n data-instant:transition-none\n data-starting-style:opacity-0 data-starting-style:scale-90\n dark:shadow-none dark:outline-gray-300 dark:-outline-offset-1\">\n <span>\n <span>Drag{' '}</span>\n <span className=\"opacity-50\">to move</span>\n </span>\n <span>\n <span>Click{' '}</span>\n <span className=\"opacity-50\">to open menu</span>\n </span>\n </Tooltip.Popup>\n </Tooltip.Positioner>\n </Tooltip.Portal>\n </Tooltip.Root>\n </BlockHandlePopup>\n </BlockHandlePositioner>\n </BlockHandleRoot>\n </Tooltip.Provider>\n )\n}\n"
36
36
  },
37
37
  {
38
38
  "path": "registry/src/react/examples/notion/editor.tsx",
@@ -50,19 +50,19 @@
50
50
  "path": "registry/src/react/examples/notion/image-view/image-view-content.tsx",
51
51
  "type": "registry:component",
52
52
  "target": "components/editor/examples/notion/image-view/image-view-content.tsx",
53
- "content": "'use client'\nimport { UploadTask } from 'prosekit/extensions/file'\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { ReactNodeViewProps } from 'prosekit/react'\nimport { ResizableHandle, ResizableRoot } from 'prosekit/react/resizable'\nimport { useEffect, useState, type SyntheticEvent } from 'react'\n\nexport default function ImageViewContent(props: ReactNodeViewProps) {\n const attrs = props.node.attrs as ImageAttrs\n const url = attrs.src || ''\n const uploading = url.startsWith('blob:')\n\n const [aspectRatio, setAspectRatio] = useState<number | undefined>()\n const [error, setError] = useState<string | undefined>()\n const [progress, setProgress] = useState(0)\n\n useEffect(() => {\n if (!uploading) return\n\n const uploadTask = UploadTask.get<string>(url)\n if (!uploadTask) return\n\n let canceled = false\n\n uploadTask.finished.catch((error) => {\n if (canceled) return\n setError(String(error))\n })\n const unsubscribeProgress = uploadTask.subscribeProgress(({ loaded, total }) => {\n if (canceled) return\n setProgress(total ? loaded / total : 0)\n })\n\n return () => {\n canceled = true\n unsubscribeProgress()\n }\n }, [url, uploading])\n\n const handleImageLoad = (event: SyntheticEvent) => {\n const img = event.target as HTMLImageElement\n const { naturalWidth, naturalHeight } = img\n const ratio = naturalWidth / naturalHeight\n if (ratio && Number.isFinite(ratio)) {\n setAspectRatio(ratio)\n }\n if (naturalWidth && naturalHeight && (!attrs.width || !attrs.height)) {\n props.setAttrs({ width: naturalWidth, height: naturalHeight })\n }\n }\n\n return (\n <ResizableRoot\n width={attrs.width ?? undefined}\n height={attrs.height ?? undefined}\n aspectRatio={aspectRatio}\n onResizeEnd={(event) => props.setAttrs(event.detail)}\n data-selected={props.selected ? '' : undefined}\n className=\"relative flex items-center justify-center box-border overflow-hidden my-2 group max-h-150 max-w-full min-h-16 min-w-16 outline-2 outline-transparent data-selected:outline-blue-500 outline-solid\"\n >\n {url && !error && (\n <img\n src={url}\n onLoad={handleImageLoad}\n alt=\"upload preview\"\n className=\"h-full w-full max-w-full max-h-full object-contain\"\n />\n )}\n {uploading && !error && (\n <div className=\"absolute bottom-0 left-0 m-1 flex content-center items-center gap-2 rounded-sm bg-gray-800/60 p-1.5 text-xs text-white/80 transition\">\n <div className=\"i-lucide-loader-circle size-4 animate-spin block\"></div>\n <div>{Math.round(progress * 100)}%</div>\n </div>\n )}\n {error && (\n <div className=\"absolute bottom-0 left-0 right-0 top-0 flex flex-col items-center justify-center gap-4 bg-gray-200 p-2 text-sm dark:bg-gray-800 @container\">\n <div className=\"i-lucide-image-off size-8 block\"></div>\n <div className=\"hidden opacity-80 @xs:block\">\n Failed to upload image\n </div>\n </div>\n )}\n <ResizableHandle\n className=\"absolute bottom-0 right-0 rounded-sm m-1.5 p-1 transition bg-gray-900/30 active:bg-gray-800/60 hover:bg-gray-800/60 text-white/50 active:text-white/80 active:translate-x-0.5 active:translate-y-0.5 opacity-0 hover:opacity-100 group-hover:opacity-100 group-data-resizing:opacity-100\"\n position=\"bottom-right\"\n >\n <div className=\"i-lucide-arrow-down-right size-4 block\"></div>\n </ResizableHandle>\n </ResizableRoot>\n )\n}\n"
53
+ "content": "'use client'\n\nimport { UploadTask } from 'prosekit/extensions/file'\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { ReactNodeViewProps } from 'prosekit/react'\nimport { ResizableHandle, ResizableRoot } from 'prosekit/react/resizable'\nimport { useEffect, useState, type SyntheticEvent } from 'react'\n\nexport default function ImageViewContent(props: ReactNodeViewProps) {\n const attrs = props.node.attrs as ImageAttrs\n const url = attrs.src || ''\n const uploading = url.startsWith('blob:')\n\n const [aspectRatio, setAspectRatio] = useState<number | undefined>()\n const [error, setError] = useState<string | undefined>()\n const [progress, setProgress] = useState(0)\n\n useEffect(() => {\n if (!uploading) return\n\n const uploadTask = UploadTask.get<string>(url)\n if (!uploadTask) return\n\n let canceled = false\n\n uploadTask.finished.catch((error) => {\n if (canceled) return\n setError(String(error))\n })\n const unsubscribeProgress = uploadTask.subscribeProgress(({ loaded, total }) => {\n if (canceled) return\n setProgress(total ? loaded / total : 0)\n })\n\n return () => {\n canceled = true\n unsubscribeProgress()\n }\n }, [url, uploading])\n\n const handleImageLoad = (event: SyntheticEvent) => {\n const img = event.target as HTMLImageElement\n const { naturalWidth, naturalHeight } = img\n const ratio = naturalWidth / naturalHeight\n if (ratio && Number.isFinite(ratio)) {\n setAspectRatio(ratio)\n }\n if (naturalWidth && naturalHeight && (!attrs.width || !attrs.height)) {\n props.setAttrs({ width: naturalWidth, height: naturalHeight })\n }\n }\n\n return (\n <ResizableRoot\n width={attrs.width ?? undefined}\n height={attrs.height ?? undefined}\n aspectRatio={aspectRatio}\n onResizeEnd={(event) => props.setAttrs(event.detail)}\n data-selected={props.selected ? '' : undefined}\n className=\"relative flex items-center justify-center box-border overflow-hidden my-2 group max-h-150 max-w-full min-h-16 min-w-16 outline-2 outline-transparent data-selected:outline-blue-500 outline-solid\"\n >\n {url && !error && (\n <img\n src={url}\n onLoad={handleImageLoad}\n alt=\"upload preview\"\n className=\"h-full w-full max-w-full max-h-full object-contain\"\n />\n )}\n {uploading && !error && (\n <div className=\"absolute bottom-0 left-0 m-1 flex content-center items-center gap-2 rounded-sm bg-gray-800/60 p-1.5 text-xs text-white/80 transition\">\n <div className=\"i-lucide-loader-circle size-4 animate-spin block\"></div>\n <div>{Math.round(progress * 100)}%</div>\n </div>\n )}\n {error && (\n <div className=\"absolute bottom-0 left-0 right-0 top-0 flex flex-col items-center justify-center gap-4 bg-gray-200 p-2 text-sm dark:bg-gray-800 @container\">\n <div className=\"i-lucide-image-off size-8 block\"></div>\n <div className=\"hidden opacity-80 @xs:block\">\n Failed to upload image\n </div>\n </div>\n )}\n <ResizableHandle\n className=\"absolute bottom-0 right-0 rounded-sm m-1.5 p-1 transition bg-gray-900/30 active:bg-gray-800/60 hover:bg-gray-800/60 text-white/50 active:text-white/80 active:translate-x-0.5 active:translate-y-0.5 opacity-0 hover:opacity-100 group-hover:opacity-100 group-data-resizing:opacity-100\"\n position=\"bottom-right\"\n >\n <div className=\"i-lucide-arrow-down-right size-4 block\"></div>\n </ResizableHandle>\n </ResizableRoot>\n )\n}\n"
54
54
  },
55
55
  {
56
56
  "path": "registry/src/react/examples/notion/image-view/image-view-placeholder.tsx",
57
57
  "type": "registry:component",
58
58
  "target": "components/editor/examples/notion/image-view/image-view-placeholder.tsx",
59
- "content": "'use client'\nimport { useEditor } from 'prosekit/react'\n\nimport { sampleUploader } from '../../../sample/sample-uploader'\nimport type { EditorExtension } from '../extension'\n\ninterface Props {\n getPos: () => number | undefined\n selected: boolean\n}\n\nexport default function ImageViewPlaceholder(props: Props) {\n const editor = useEditor<EditorExtension>()\n\n const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {\n const file = event.target.files?.[0]\n if (!file) return\n\n const pos = props.getPos()\n if (typeof pos !== 'number') return\n\n editor.commands.uploadImage({ file, uploader: sampleUploader, pos, replace: true })\n\n // Reset input so the same file can be selected again\n event.target.value = ''\n }\n\n return (\n <label\n className=\"flex w-full cursor-pointer items-center rounded-lg gap-3 px-4 py-3 transition-colors data-selected:outline-blue-500 outline-1 outline-transparent bg-gray-500/10 text-current/40 hover:bg-gray-500/20 hover:text-current/60\"\n aria-label=\"Add an image\"\n data-selected={props.selected ? '' : undefined}\n >\n <input\n type=\"file\"\n accept=\"image/*\"\n className=\"hidden\"\n onChange={handleFileChange}\n />\n <span className=\"block i-lucide-image size-4\" />\n <span className=\"text-sm font-medium\">Add an image</span>\n </label>\n )\n}\n"
59
+ "content": "'use client'\n\nimport { useEditor } from 'prosekit/react'\n\nimport { sampleUploader } from '../../../sample/sample-uploader'\nimport type { EditorExtension } from '../extension'\n\ninterface Props {\n getPos: () => number | undefined\n selected: boolean\n}\n\nexport default function ImageViewPlaceholder(props: Props) {\n const editor = useEditor<EditorExtension>()\n\n const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {\n const file = event.target.files?.[0]\n if (!file) return\n\n const pos = props.getPos()\n if (typeof pos !== 'number') return\n\n editor.commands.uploadImage({ file, uploader: sampleUploader, pos, replace: true })\n\n // Reset input so the same file can be selected again\n event.target.value = ''\n }\n\n return (\n <label\n className=\"flex w-full cursor-pointer items-center rounded-lg gap-3 px-4 py-3 transition-colors data-selected:outline-blue-500 outline-1 outline-transparent bg-gray-500/10 text-current/40 hover:bg-gray-500/20 hover:text-current/60\"\n aria-label=\"Add an image\"\n data-selected={props.selected ? '' : undefined}\n >\n <input\n type=\"file\"\n accept=\"image/*\"\n className=\"hidden\"\n onChange={handleFileChange}\n />\n <span className=\"block i-lucide-image size-4\" />\n <span className=\"text-sm font-medium\">Add an image</span>\n </label>\n )\n}\n"
60
60
  },
61
61
  {
62
62
  "path": "registry/src/react/examples/notion/image-view/image-view.tsx",
63
63
  "type": "registry:component",
64
64
  "target": "components/editor/examples/notion/image-view/image-view.tsx",
65
- "content": "'use client'\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { ReactNodeViewProps } from 'prosekit/react'\n\nimport ImageViewContent from './image-view-content'\nimport ImageViewPlaceholder from './image-view-placeholder'\n\nexport default function ImageView(props: ReactNodeViewProps) {\n const attrs = props.node.attrs as ImageAttrs\n const url = attrs.src || ''\n\n if (url) {\n return <ImageViewContent {...props} />\n } else {\n return <ImageViewPlaceholder getPos={props.getPos} selected={props.selected} />\n }\n}\n"
65
+ "content": "'use client'\n\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { ReactNodeViewProps } from 'prosekit/react'\n\nimport ImageViewContent from './image-view-content'\nimport ImageViewPlaceholder from './image-view-placeholder'\n\nexport default function ImageView(props: ReactNodeViewProps) {\n const attrs = props.node.attrs as ImageAttrs\n const url = attrs.src || ''\n\n if (url) {\n return <ImageViewContent {...props} />\n } else {\n return <ImageViewPlaceholder getPos={props.getPos} selected={props.selected} />\n }\n}\n"
66
66
  },
67
67
  {
68
68
  "path": "registry/src/react/examples/notion/image-view/index.ts",
@@ -86,19 +86,19 @@
86
86
  "path": "registry/src/react/examples/notion/slash-menu/slash-menu-empty.tsx",
87
87
  "type": "registry:component",
88
88
  "target": "components/editor/examples/notion/slash-menu/slash-menu-empty.tsx",
89
- "content": "'use client'\nimport { AutocompleteEmpty } from 'prosekit/react/autocomplete'\n\nexport default function SlashMenuEmpty() {\n return (\n <AutocompleteEmpty className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n <span>No results</span>\n </AutocompleteEmpty>\n )\n}\n"
89
+ "content": "'use client'\n\nimport { AutocompleteEmpty } from 'prosekit/react/autocomplete'\n\nexport default function SlashMenuEmpty() {\n return (\n <AutocompleteEmpty className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n <span>No results</span>\n </AutocompleteEmpty>\n )\n}\n"
90
90
  },
91
91
  {
92
92
  "path": "registry/src/react/examples/notion/slash-menu/slash-menu-item.tsx",
93
93
  "type": "registry:component",
94
94
  "target": "components/editor/examples/notion/slash-menu/slash-menu-item.tsx",
95
- "content": "'use client'\nimport { AutocompleteItem } from 'prosekit/react/autocomplete'\n\nexport default function SlashMenuItem(props: {\n label: string\n kbd?: string\n onSelect: () => void\n}) {\n return (\n <AutocompleteItem onSelect={props.onSelect} className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n <span>{props.label}</span>\n {props.kbd && <kbd className=\"text-xs font-mono text-gray-400 dark:text-gray-500\">{props.kbd}</kbd>}\n </AutocompleteItem>\n )\n}\n"
95
+ "content": "'use client'\n\nimport { AutocompleteItem } from 'prosekit/react/autocomplete'\n\nexport default function SlashMenuItem(props: {\n label: string\n kbd?: string\n onSelect: () => void\n}) {\n return (\n <AutocompleteItem onSelect={props.onSelect} className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n <span>{props.label}</span>\n {props.kbd && <kbd className=\"text-xs font-mono text-gray-400 dark:text-gray-500\">{props.kbd}</kbd>}\n </AutocompleteItem>\n )\n}\n"
96
96
  },
97
97
  {
98
98
  "path": "registry/src/react/examples/notion/slash-menu/slash-menu.tsx",
99
99
  "type": "registry:component",
100
100
  "target": "components/editor/examples/notion/slash-menu/slash-menu.tsx",
101
- "content": "'use client'\nimport type { BasicExtension } from 'prosekit/basic'\nimport { canUseRegexLookbehind } from 'prosekit/core'\nimport { useEditor } from 'prosekit/react'\nimport { AutocompletePopup, AutocompletePositioner, AutocompleteRoot } from 'prosekit/react/autocomplete'\n\nimport SlashMenuEmpty from './slash-menu-empty'\nimport SlashMenuItem from './slash-menu-item'\n\n// Match inputs like \"/\", \"/table\", \"/heading 1\" etc. Do not match \"/ heading\".\nconst regex = canUseRegexLookbehind() ? /(?<!\\S)\\/(\\S.*)?$/u : /\\/(\\S.*)?$/u\n\ninterface Props {\n onOpenChange: (open: boolean) => void\n}\n\nexport default function SlashMenu(props: Props) {\n const editor = useEditor<BasicExtension>()\n\n return (\n <AutocompleteRoot\n regex={regex}\n onOpenChange={(event) => {\n props.onOpenChange(event.detail)\n }}\n >\n <AutocompletePositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <AutocompletePopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col relative max-h-100 min-w-60 select-none overflow-auto whitespace-nowrap p-1\">\n <SlashMenuItem\n label=\"Text\"\n onSelect={() => editor.commands.setParagraph()}\n />\n\n <SlashMenuItem\n label=\"Heading 1\"\n kbd=\"#\"\n onSelect={() => editor.commands.setHeading({ level: 1 })}\n />\n\n <SlashMenuItem\n label=\"Heading 2\"\n kbd=\"##\"\n onSelect={() => editor.commands.setHeading({ level: 2 })}\n />\n\n <SlashMenuItem\n label=\"Heading 3\"\n kbd=\"###\"\n onSelect={() => editor.commands.setHeading({ level: 3 })}\n />\n\n <SlashMenuItem\n label=\"Bullet list\"\n kbd=\"-\"\n onSelect={() => editor.commands.wrapInList({ kind: 'bullet' })}\n />\n\n <SlashMenuItem\n label=\"Ordered list\"\n kbd=\"1.\"\n onSelect={() => editor.commands.wrapInList({ kind: 'ordered' })}\n />\n\n <SlashMenuItem\n label=\"Task list\"\n kbd=\"[]\"\n onSelect={() => editor.commands.wrapInList({ kind: 'task' })}\n />\n\n <SlashMenuItem\n label=\"Toggle list\"\n kbd=\">>\"\n onSelect={() => editor.commands.wrapInList({ kind: 'toggle' })}\n />\n\n <SlashMenuItem\n label=\"Quote\"\n kbd=\">\"\n onSelect={() => editor.commands.setBlockquote()}\n />\n\n <SlashMenuItem\n label=\"Table\"\n onSelect={() => editor.commands.insertTable({ row: 3, col: 3 })}\n />\n\n <SlashMenuItem\n label=\"Divider\"\n kbd=\"---\"\n onSelect={() => editor.commands.insertHorizontalRule()}\n />\n\n <SlashMenuItem\n label=\"Code\"\n kbd=\"```\"\n onSelect={() => editor.commands.setCodeBlock()}\n />\n\n <SlashMenuItem\n label=\"Image\"\n onSelect={() => editor.commands.insertImage({ src: '' })}\n />\n\n <SlashMenuEmpty />\n </AutocompletePopup>\n </AutocompletePositioner>\n </AutocompleteRoot>\n )\n}\n"
101
+ "content": "'use client'\n\nimport type { BasicExtension } from 'prosekit/basic'\nimport { canUseRegexLookbehind } from 'prosekit/core'\nimport { useEditor } from 'prosekit/react'\nimport { AutocompletePopup, AutocompletePositioner, AutocompleteRoot } from 'prosekit/react/autocomplete'\n\nimport SlashMenuEmpty from './slash-menu-empty'\nimport SlashMenuItem from './slash-menu-item'\n\n// Match inputs like \"/\", \"/table\", \"/heading 1\" etc. Do not match \"/ heading\".\nconst regex = canUseRegexLookbehind() ? /(?<!\\S)\\/(\\S.*)?$/u : /\\/(\\S.*)?$/u\n\ninterface Props {\n onOpenChange: (open: boolean) => void\n}\n\nexport default function SlashMenu(props: Props) {\n const editor = useEditor<BasicExtension>()\n\n return (\n <AutocompleteRoot\n regex={regex}\n onOpenChange={(event) => {\n props.onOpenChange(event.detail)\n }}\n >\n <AutocompletePositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <AutocompletePopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col relative max-h-100 min-w-60 select-none overflow-auto whitespace-nowrap p-1\">\n <SlashMenuItem\n label=\"Text\"\n onSelect={() => editor.commands.setParagraph()}\n />\n\n <SlashMenuItem\n label=\"Heading 1\"\n kbd=\"#\"\n onSelect={() => editor.commands.setHeading({ level: 1 })}\n />\n\n <SlashMenuItem\n label=\"Heading 2\"\n kbd=\"##\"\n onSelect={() => editor.commands.setHeading({ level: 2 })}\n />\n\n <SlashMenuItem\n label=\"Heading 3\"\n kbd=\"###\"\n onSelect={() => editor.commands.setHeading({ level: 3 })}\n />\n\n <SlashMenuItem\n label=\"Bullet list\"\n kbd=\"-\"\n onSelect={() => editor.commands.wrapInList({ kind: 'bullet' })}\n />\n\n <SlashMenuItem\n label=\"Ordered list\"\n kbd=\"1.\"\n onSelect={() => editor.commands.wrapInList({ kind: 'ordered' })}\n />\n\n <SlashMenuItem\n label=\"Task list\"\n kbd=\"[]\"\n onSelect={() => editor.commands.wrapInList({ kind: 'task' })}\n />\n\n <SlashMenuItem\n label=\"Toggle list\"\n kbd=\">>\"\n onSelect={() => editor.commands.wrapInList({ kind: 'toggle' })}\n />\n\n <SlashMenuItem\n label=\"Quote\"\n kbd=\">\"\n onSelect={() => editor.commands.setBlockquote()}\n />\n\n <SlashMenuItem\n label=\"Table\"\n onSelect={() => editor.commands.insertTable({ row: 3, col: 3 })}\n />\n\n <SlashMenuItem\n label=\"Divider\"\n kbd=\"---\"\n onSelect={() => editor.commands.insertHorizontalRule()}\n />\n\n <SlashMenuItem\n label=\"Code\"\n kbd=\"```\"\n onSelect={() => editor.commands.setCodeBlock()}\n />\n\n <SlashMenuItem\n label=\"Image\"\n onSelect={() => editor.commands.insertImage({ src: '' })}\n />\n\n <SlashMenuEmpty />\n </AutocompletePopup>\n </AutocompletePositioner>\n </AutocompleteRoot>\n )\n}\n"
102
102
  },
103
103
  {
104
104
  "path": "registry/src/react/examples/notion/style.css",
@@ -32,7 +32,7 @@
32
32
  "path": "registry/src/react/examples/page/paper-controller.tsx",
33
33
  "type": "registry:component",
34
34
  "target": "components/editor/examples/page/paper-controller.tsx",
35
- "content": "'use client'\nimport { definePageRendering, type PageRenderingOptions } from 'prosekit/extensions/page'\nimport { useExtension } from 'prosekit/react'\nimport { useEffect, useId, useMemo, useState } from 'react'\n\n// Paper sizes in pixels at 96 DPI\nconst PAPER_SIZES = {\n A3: { short: 1123, long: 1587 },\n A4: { short: 794, long: 1123 },\n A5: { short: 559, long: 794 },\n B4: { short: 945, long: 1334 },\n B5: { short: 665, long: 945 },\n letter: { short: 816, long: 1056 },\n} as const\n\ntype PaperSize = keyof typeof PAPER_SIZES\ntype Orientation = 'portrait' | 'landscape'\n\nexport default function PaperController({\n zoom,\n setZoom,\n}: {\n zoom: number\n setZoom: (zoom: number) => void\n}) {\n const id = useId()\n const [paperSize, setPaperSize] = useState<PaperSize>('A4')\n const [orientation, setOrientation] = useState<Orientation>('portrait')\n const [margin, setMargin] = useState(70)\n const [enablePageLayout, setEnablePageLayout] = useState(true)\n\n const pageRenderingOptions: PageRenderingOptions = useMemo(() => {\n const { short, long } = PAPER_SIZES[paperSize]\n const pageWidth = orientation === 'portrait' ? short : long\n const pageHeight = orientation === 'portrait' ? long : short\n\n return {\n pageWidth,\n pageHeight,\n marginTop: margin,\n marginRight: margin,\n marginBottom: margin,\n marginLeft: margin,\n }\n }, [paperSize, orientation, margin])\n\n useEffect(() => {\n const styleId = 'print-page-style'\n let style = document.getElementById(styleId) as HTMLStyleElement | null\n if (!style) {\n style = document.createElement('style')\n style.id = styleId\n document.head.appendChild(style)\n }\n style.textContent = `@page { size: ${paperSize} ${orientation}; margin: 0; }`\n\n return () => {\n style.textContent = ''\n }\n }, [paperSize, orientation])\n\n const extension = useMemo(() => {\n return enablePageLayout ? definePageRendering(pageRenderingOptions) : null\n }, [pageRenderingOptions, enablePageLayout])\n\n useExtension(extension)\n\n return (\n <div\n data-paper-controller={paperSize}\n className=\"grid grid-cols-[auto_1fr] gap-2 w-min border p-2 bg-[Canvas] sticky top-2 left-2 z-10 print:hidden text-xs\"\n >\n <label htmlFor={`${id}-page`}>Page</label>\n <select\n id={`${id}-page`}\n value={enablePageLayout ? 'Enabled' : 'Disabled'}\n onChange={(e) => setEnablePageLayout(e.target.value === 'Enabled')}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"Enabled\">Enabled</option>\n <option value=\"Disabled\">Disabled</option>\n </select>\n <label htmlFor={`${id}-paper`}>Paper Size</label>\n <select\n id={`${id}-paper`}\n value={paperSize}\n onChange={(e) => setPaperSize(e.target.value as PaperSize)}\n disabled={!enablePageLayout}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"A3\">A3</option>\n <option value=\"A4\">A4</option>\n <option value=\"A5\">A5</option>\n <option value=\"B4\">B4</option>\n <option value=\"B5\">B5</option>\n <option value=\"letter\">Letter</option>\n </select>\n <label htmlFor={`${id}-orientation`}>Orientation</label>\n <select\n id={`${id}-orientation`}\n value={orientation}\n onChange={(e) => setOrientation(e.target.value as Orientation)}\n disabled={!enablePageLayout}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"portrait\">Portrait</option>\n <option value=\"landscape\">Landscape</option>\n </select>\n <label htmlFor={`${id}-margin`}>Margin</label>\n <select\n id={`${id}-margin`}\n value={String(margin)}\n onChange={(e) => setMargin(Number.parseInt(e.target.value, 10))}\n disabled={!enablePageLayout}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"30\">Narrow</option>\n <option value=\"70\">Normal</option>\n <option value=\"120\">Wide</option>\n </select>\n <label htmlFor={`${id}-zoom`}>Zoom</label>\n <select\n id={`${id}-zoom`}\n value={String(zoom)}\n onChange={(e) => setZoom(Number.parseInt(e.target.value, 10))}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"25\">25%</option>\n <option value=\"50\">50%</option>\n <option value=\"75\">75%</option>\n <option value=\"100\">100%</option>\n <option value=\"125\">125%</option>\n <option value=\"150\">150%</option>\n <option value=\"200\">200%</option>\n </select>\n </div>\n )\n}\n"
35
+ "content": "'use client'\n\nimport { definePageRendering, type PageRenderingOptions } from 'prosekit/extensions/page'\nimport { useExtension } from 'prosekit/react'\nimport { useEffect, useId, useMemo, useState } from 'react'\n\n// Paper sizes in pixels at 96 DPI\nconst PAPER_SIZES = {\n A3: { short: 1123, long: 1587 },\n A4: { short: 794, long: 1123 },\n A5: { short: 559, long: 794 },\n B4: { short: 945, long: 1334 },\n B5: { short: 665, long: 945 },\n letter: { short: 816, long: 1056 },\n} as const\n\ntype PaperSize = keyof typeof PAPER_SIZES\ntype Orientation = 'portrait' | 'landscape'\n\nexport default function PaperController({\n zoom,\n setZoom,\n}: {\n zoom: number\n setZoom: (zoom: number) => void\n}) {\n const id = useId()\n const [paperSize, setPaperSize] = useState<PaperSize>('A4')\n const [orientation, setOrientation] = useState<Orientation>('portrait')\n const [margin, setMargin] = useState(70)\n const [enablePageLayout, setEnablePageLayout] = useState(true)\n\n const pageRenderingOptions: PageRenderingOptions = useMemo(() => {\n const { short, long } = PAPER_SIZES[paperSize]\n const pageWidth = orientation === 'portrait' ? short : long\n const pageHeight = orientation === 'portrait' ? long : short\n\n return {\n pageWidth,\n pageHeight,\n marginTop: margin,\n marginRight: margin,\n marginBottom: margin,\n marginLeft: margin,\n }\n }, [paperSize, orientation, margin])\n\n useEffect(() => {\n const styleId = 'print-page-style'\n let style = document.getElementById(styleId) as HTMLStyleElement | null\n if (!style) {\n style = document.createElement('style')\n style.id = styleId\n document.head.appendChild(style)\n }\n style.textContent = `@page { size: ${paperSize} ${orientation}; margin: 0; }`\n\n return () => {\n style.textContent = ''\n }\n }, [paperSize, orientation])\n\n const extension = useMemo(() => {\n return enablePageLayout ? definePageRendering(pageRenderingOptions) : null\n }, [pageRenderingOptions, enablePageLayout])\n\n useExtension(extension)\n\n return (\n <div\n data-paper-controller={paperSize}\n className=\"grid grid-cols-[auto_1fr] gap-2 w-min border p-2 bg-[Canvas] sticky top-2 left-2 z-10 print:hidden text-xs\"\n >\n <label htmlFor={`${id}-page`}>Page</label>\n <select\n id={`${id}-page`}\n value={enablePageLayout ? 'Enabled' : 'Disabled'}\n onChange={(e) => setEnablePageLayout(e.target.value === 'Enabled')}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"Enabled\">Enabled</option>\n <option value=\"Disabled\">Disabled</option>\n </select>\n <label htmlFor={`${id}-paper`}>Paper Size</label>\n <select\n id={`${id}-paper`}\n value={paperSize}\n onChange={(e) => setPaperSize(e.target.value as PaperSize)}\n disabled={!enablePageLayout}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"A3\">A3</option>\n <option value=\"A4\">A4</option>\n <option value=\"A5\">A5</option>\n <option value=\"B4\">B4</option>\n <option value=\"B5\">B5</option>\n <option value=\"letter\">Letter</option>\n </select>\n <label htmlFor={`${id}-orientation`}>Orientation</label>\n <select\n id={`${id}-orientation`}\n value={orientation}\n onChange={(e) => setOrientation(e.target.value as Orientation)}\n disabled={!enablePageLayout}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"portrait\">Portrait</option>\n <option value=\"landscape\">Landscape</option>\n </select>\n <label htmlFor={`${id}-margin`}>Margin</label>\n <select\n id={`${id}-margin`}\n value={String(margin)}\n onChange={(e) => setMargin(Number.parseInt(e.target.value, 10))}\n disabled={!enablePageLayout}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"30\">Narrow</option>\n <option value=\"70\">Normal</option>\n <option value=\"120\">Wide</option>\n </select>\n <label htmlFor={`${id}-zoom`}>Zoom</label>\n <select\n id={`${id}-zoom`}\n value={String(zoom)}\n onChange={(e) => setZoom(Number.parseInt(e.target.value, 10))}\n className=\"rounded border disabled:opacity-50\"\n >\n <option value=\"25\">25%</option>\n <option value=\"50\">50%</option>\n <option value=\"75\">75%</option>\n <option value=\"100\">100%</option>\n <option value=\"125\">125%</option>\n <option value=\"150\">150%</option>\n <option value=\"200\">200%</option>\n </select>\n </div>\n )\n}\n"
36
36
  },
37
37
  {
38
38
  "path": "registry/src/react/examples/page/zoom.css",
@@ -33,7 +33,7 @@
33
33
  "path": "registry/src/react/examples/readonly/toolbar.tsx",
34
34
  "type": "registry:component",
35
35
  "target": "components/editor/examples/readonly/toolbar.tsx",
36
- "content": "'use client'\nimport { Button } from '../../ui/button'\n\nimport { useReadonly } from './use-readonly'\n\nexport default function Toolbar() {\n const { readonly, setReadonly } = useReadonly()\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button pressed={readonly} onClick={() => setReadonly(true)}>\n Readonly\n </Button>\n\n <Button pressed={!readonly} onClick={() => setReadonly(false)}>\n Editable\n </Button>\n </div>\n )\n}\n"
36
+ "content": "'use client'\n\nimport { Button } from '../../ui/button'\n\nimport { useReadonly } from './use-readonly'\n\nexport default function Toolbar() {\n const { readonly, setReadonly } = useReadonly()\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button pressed={readonly} onClick={() => setReadonly(true)}>\n Readonly\n </Button>\n\n <Button pressed={!readonly} onClick={() => setReadonly(false)}>\n Editable\n </Button>\n </div>\n )\n}\n"
37
37
  },
38
38
  {
39
39
  "path": "registry/src/react/examples/readonly/use-readonly.ts",
@@ -33,7 +33,7 @@
33
33
  "path": "registry/src/react/examples/strike/toolbar.tsx",
34
34
  "type": "registry:component",
35
35
  "target": "components/editor/examples/strike/toolbar.tsx",
36
- "content": "'use client'\nimport { useEditor } from 'prosekit/react'\n\nimport { Button } from '../../ui/button'\n\nimport type { EditorExtension } from './extension'\n\nexport default function Toolbar() {\n const editor = useEditor<EditorExtension>({ update: true })\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button\n pressed={false}\n disabled={!editor.commands.toggleStrike.canExec()}\n onClick={() => editor.commands.toggleStrike()}\n >\n Strikethrough\n </Button>\n </div>\n )\n}\n"
36
+ "content": "'use client'\n\nimport { useEditor } from 'prosekit/react'\n\nimport { Button } from '../../ui/button'\n\nimport type { EditorExtension } from './extension'\n\nexport default function Toolbar() {\n const editor = useEditor<EditorExtension>({ update: true })\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button\n pressed={false}\n disabled={!editor.commands.toggleStrike.canExec()}\n onClick={() => editor.commands.toggleStrike()}\n >\n Strikethrough\n </Button>\n </div>\n )\n}\n"
37
37
  }
38
38
  ],
39
39
  "meta": {
@@ -33,7 +33,7 @@
33
33
  "path": "registry/src/react/examples/text-align/toolbar.tsx",
34
34
  "type": "registry:component",
35
35
  "target": "components/editor/examples/text-align/toolbar.tsx",
36
- "content": "'use client'\nimport type { Editor } from 'prosekit/core'\nimport { useEditorDerivedValue } from 'prosekit/react'\n\nimport { Button } from '../../ui/button'\n\nimport type { EditorExtension } from './extension'\n\nfunction isTextAlignActive(editor: Editor<EditorExtension>, value: string) {\n return Object.values(editor.nodes).some((node) => {\n // @ts-expect-error textAlign may not be available on every node\n return node.isActive({ textAlign: value })\n })\n}\n\nfunction getToolbarItems(editor: Editor<EditorExtension>) {\n return {\n left: {\n isActive: isTextAlignActive(editor, 'left'),\n canExec: editor.commands.setTextAlign.canExec('left'),\n command: () => editor.commands.setTextAlign('left'),\n },\n center: {\n isActive: isTextAlignActive(editor, 'center'),\n canExec: editor.commands.setTextAlign.canExec('center'),\n command: () => editor.commands.setTextAlign('center'),\n },\n right: {\n isActive: isTextAlignActive(editor, 'right'),\n canExec: editor.commands.setTextAlign.canExec('right'),\n command: () => editor.commands.setTextAlign('right'),\n },\n justify: {\n isActive: isTextAlignActive(editor, 'justify'),\n canExec: editor.commands.setTextAlign.canExec('justify'),\n command: () => editor.commands.setTextAlign('justify'),\n },\n }\n}\n\nexport default function Toolbar() {\n const items = useEditorDerivedValue(getToolbarItems)\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button\n pressed={items.left.isActive}\n disabled={!items.left.canExec}\n onClick={items.left.command}\n >\n Left\n </Button>\n\n <Button\n pressed={items.center.isActive}\n disabled={!items.center.canExec}\n onClick={items.center.command}\n >\n Center\n </Button>\n\n <Button\n pressed={items.right.isActive}\n disabled={!items.right.canExec}\n onClick={items.right.command}\n >\n Right\n </Button>\n\n <Button\n pressed={items.justify.isActive}\n disabled={!items.justify.canExec}\n onClick={items.justify.command}\n >\n Justify\n </Button>\n </div>\n )\n}\n"
36
+ "content": "'use client'\n\nimport type { Editor } from 'prosekit/core'\nimport { useEditorDerivedValue } from 'prosekit/react'\n\nimport { Button } from '../../ui/button'\n\nimport type { EditorExtension } from './extension'\n\nfunction isTextAlignActive(editor: Editor<EditorExtension>, value: string) {\n return Object.values(editor.nodes).some((node) => {\n // @ts-expect-error textAlign may not be available on every node\n return node.isActive({ textAlign: value })\n })\n}\n\nfunction getToolbarItems(editor: Editor<EditorExtension>) {\n return {\n left: {\n isActive: isTextAlignActive(editor, 'left'),\n canExec: editor.commands.setTextAlign.canExec('left'),\n command: () => editor.commands.setTextAlign('left'),\n },\n center: {\n isActive: isTextAlignActive(editor, 'center'),\n canExec: editor.commands.setTextAlign.canExec('center'),\n command: () => editor.commands.setTextAlign('center'),\n },\n right: {\n isActive: isTextAlignActive(editor, 'right'),\n canExec: editor.commands.setTextAlign.canExec('right'),\n command: () => editor.commands.setTextAlign('right'),\n },\n justify: {\n isActive: isTextAlignActive(editor, 'justify'),\n canExec: editor.commands.setTextAlign.canExec('justify'),\n command: () => editor.commands.setTextAlign('justify'),\n },\n }\n}\n\nexport default function Toolbar() {\n const items = useEditorDerivedValue(getToolbarItems)\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n <Button\n pressed={items.left.isActive}\n disabled={!items.left.canExec}\n onClick={items.left.command}\n >\n Left\n </Button>\n\n <Button\n pressed={items.center.isActive}\n disabled={!items.center.canExec}\n onClick={items.center.command}\n >\n Center\n </Button>\n\n <Button\n pressed={items.right.isActive}\n disabled={!items.right.canExec}\n onClick={items.right.command}\n >\n Right\n </Button>\n\n <Button\n pressed={items.justify.isActive}\n disabled={!items.justify.canExec}\n onClick={items.justify.command}\n >\n Justify\n </Button>\n </div>\n )\n}\n"
37
37
  }
38
38
  ],
39
39
  "meta": {
@@ -33,7 +33,7 @@
33
33
  "path": "registry/src/react/examples/text-color/inline-menu.tsx",
34
34
  "type": "registry:component",
35
35
  "target": "components/editor/examples/text-color/inline-menu.tsx",
36
- "content": "'use client'\nimport type { Editor, Keymap } from 'prosekit/core'\nimport { useEditorDerivedValue, useKeymap } from 'prosekit/react'\nimport { InlinePopoverPopup, InlinePopoverPositioner, InlinePopoverRoot } from 'prosekit/react/inline-popover'\nimport { useMemo, useState } from 'react'\n\nimport { Button } from '../../ui/button'\n\nimport type { EditorExtension } from './extension'\n\nconst textColors = [\n { label: 'Gray', value: '#9ca3af' },\n { label: 'Brown', value: '#92400e' },\n { label: 'Orange', value: '#ea580c' },\n { label: 'Yellow', value: '#ca8a04' },\n { label: 'Green', value: '#16a34a' },\n { label: 'Blue', value: '#2563eb' },\n { label: 'Purple', value: '#9333ea' },\n { label: 'Magenta', value: '#c026d3' },\n { label: 'Red', value: '#dc2626' },\n]\n\nconst backgroundColors = [\n { label: 'Gray', value: '#f3f4f6' },\n { label: 'Brown', value: '#fef3c7' },\n { label: 'Orange', value: '#ffedd5' },\n { label: 'Yellow', value: '#fef9c3' },\n { label: 'Green', value: '#d1fae5' },\n { label: 'Blue', value: '#dbeafe' },\n { label: 'Purple', value: '#e9d5ff' },\n { label: 'Pink', value: '#fce7f3' },\n { label: 'Red', value: '#fecaca' },\n]\n\nfunction getTextColorState(editor: Editor<EditorExtension>) {\n return [{\n label: 'Default',\n value: 'currentColor',\n isActive: !editor.marks.textColor.isActive(),\n onClick: () => editor.commands.removeTextColor(),\n }].concat(textColors.map((color) => ({\n label: color.label,\n value: color.value,\n isActive: editor.marks.textColor.isActive({ color: color.value }),\n onClick: () => editor.commands.addTextColor({ color: color.value }),\n })))\n}\n\nfunction getBackgroundColorState(editor: Editor<EditorExtension>) {\n return [{\n label: 'Default',\n value: 'canvas',\n isActive: !editor.marks.backgroundColor.isActive(),\n onClick: () => editor.commands.removeBackgroundColor(),\n }].concat(\n backgroundColors.map((color) => ({\n label: color.label,\n value: color.value,\n isActive: editor.marks.backgroundColor.isActive({ color: color.value }),\n onClick: () => editor.commands.addBackgroundColor({ color: color.value }),\n })),\n )\n}\n\nexport default function InlineMenu() {\n const textColorState = useEditorDerivedValue(getTextColorState)\n const backgroundColorState = useEditorDerivedValue(getBackgroundColorState)\n const [open, setOpen] = useState(false)\n\n const keymap: Keymap = useMemo(() => ({\n Escape: () => {\n if (open) {\n setOpen(false)\n return true\n }\n return false\n },\n }), [open])\n\n useKeymap(keymap)\n\n return (\n <InlinePopoverRoot\n open={open}\n onOpenChange={(event) => setOpen(event.detail)}\n >\n <InlinePopoverPositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <InlinePopoverPopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-md p-1\">\n <div className=\"flex flex-col gap-4 p-4\">\n <div className=\"flex flex-col gap-2\">\n <div className=\"text-sm\">Text color</div>\n <div className=\"grid grid-cols-5 gap-1\">\n {textColorState.map((color) => (\n <Button\n key={color.label}\n pressed={color.isActive}\n tooltip={`Text: ${color.label}`}\n onClick={color.onClick}\n >\n <span\n className=\"text-base font-medium\"\n style={{ color: color.value }}\n >\n A\n </span>\n </Button>\n ))}\n </div>\n </div>\n <div className=\"flex flex-col gap-2\">\n <div className=\"text-sm\">Background color</div>\n <div className=\"grid grid-cols-5 gap-1\">\n {backgroundColorState.map((color) => (\n <Button\n key={color.label}\n pressed={color.isActive}\n tooltip={`Background: ${color.label}`}\n onClick={color.onClick}\n >\n <div\n className=\"w-6 h-6 rounded border border-gray-200 dark:border-gray-700\"\n style={{ backgroundColor: color.value }}\n />\n </Button>\n ))}\n </div>\n </div>\n </div>\n </InlinePopoverPopup>\n </InlinePopoverPositioner>\n </InlinePopoverRoot>\n )\n}\n"
36
+ "content": "'use client'\n\nimport type { Editor, Keymap } from 'prosekit/core'\nimport { useEditorDerivedValue, useKeymap } from 'prosekit/react'\nimport { InlinePopoverPopup, InlinePopoverPositioner, InlinePopoverRoot } from 'prosekit/react/inline-popover'\nimport { useMemo, useState } from 'react'\n\nimport { Button } from '../../ui/button'\n\nimport type { EditorExtension } from './extension'\n\nconst textColors = [\n { label: 'Gray', value: '#9ca3af' },\n { label: 'Brown', value: '#92400e' },\n { label: 'Orange', value: '#ea580c' },\n { label: 'Yellow', value: '#ca8a04' },\n { label: 'Green', value: '#16a34a' },\n { label: 'Blue', value: '#2563eb' },\n { label: 'Purple', value: '#9333ea' },\n { label: 'Magenta', value: '#c026d3' },\n { label: 'Red', value: '#dc2626' },\n]\n\nconst backgroundColors = [\n { label: 'Gray', value: '#f3f4f6' },\n { label: 'Brown', value: '#fef3c7' },\n { label: 'Orange', value: '#ffedd5' },\n { label: 'Yellow', value: '#fef9c3' },\n { label: 'Green', value: '#d1fae5' },\n { label: 'Blue', value: '#dbeafe' },\n { label: 'Purple', value: '#e9d5ff' },\n { label: 'Pink', value: '#fce7f3' },\n { label: 'Red', value: '#fecaca' },\n]\n\nfunction getTextColorState(editor: Editor<EditorExtension>) {\n return [{\n label: 'Default',\n value: 'currentColor',\n isActive: !editor.marks.textColor.isActive(),\n onClick: () => editor.commands.removeTextColor(),\n }].concat(textColors.map((color) => ({\n label: color.label,\n value: color.value,\n isActive: editor.marks.textColor.isActive({ color: color.value }),\n onClick: () => editor.commands.addTextColor({ color: color.value }),\n })))\n}\n\nfunction getBackgroundColorState(editor: Editor<EditorExtension>) {\n return [{\n label: 'Default',\n value: 'canvas',\n isActive: !editor.marks.backgroundColor.isActive(),\n onClick: () => editor.commands.removeBackgroundColor(),\n }].concat(\n backgroundColors.map((color) => ({\n label: color.label,\n value: color.value,\n isActive: editor.marks.backgroundColor.isActive({ color: color.value }),\n onClick: () => editor.commands.addBackgroundColor({ color: color.value }),\n })),\n )\n}\n\nexport default function InlineMenu() {\n const textColorState = useEditorDerivedValue(getTextColorState)\n const backgroundColorState = useEditorDerivedValue(getBackgroundColorState)\n const [open, setOpen] = useState(false)\n\n const keymap: Keymap = useMemo(() => ({\n Escape: () => {\n if (open) {\n setOpen(false)\n return true\n }\n return false\n },\n }), [open])\n\n useKeymap(keymap)\n\n return (\n <InlinePopoverRoot\n open={open}\n onOpenChange={(event) => setOpen(event.detail)}\n >\n <InlinePopoverPositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <InlinePopoverPopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-md p-1\">\n <div className=\"flex flex-col gap-4 p-4\">\n <div className=\"flex flex-col gap-2\">\n <div className=\"text-sm\">Text color</div>\n <div className=\"grid grid-cols-5 gap-1\">\n {textColorState.map((color) => (\n <Button\n key={color.label}\n pressed={color.isActive}\n tooltip={`Text: ${color.label}`}\n onClick={color.onClick}\n >\n <span\n className=\"text-base font-medium\"\n style={{ color: color.value }}\n >\n A\n </span>\n </Button>\n ))}\n </div>\n </div>\n <div className=\"flex flex-col gap-2\">\n <div className=\"text-sm\">Background color</div>\n <div className=\"grid grid-cols-5 gap-1\">\n {backgroundColorState.map((color) => (\n <Button\n key={color.label}\n pressed={color.isActive}\n tooltip={`Background: ${color.label}`}\n onClick={color.onClick}\n >\n <div\n className=\"w-6 h-6 rounded border border-gray-200 dark:border-gray-700\"\n style={{ backgroundColor: color.value }}\n />\n </Button>\n ))}\n </div>\n </div>\n </div>\n </InlinePopoverPopup>\n </InlinePopoverPositioner>\n </InlinePopoverRoot>\n )\n}\n"
37
37
  }
38
38
  ],
39
39
  "meta": {
@@ -33,13 +33,13 @@
33
33
  "path": "registry/src/react/examples/tweet/method-select.tsx",
34
34
  "type": "registry:component",
35
35
  "target": "components/editor/examples/tweet/method-select.tsx",
36
- "content": "'use client'\nimport { useId } from 'react'\n\nexport function MethodSelect(props: {\n value: 'basic' | 'advanced'\n onChange: (value: 'basic' | 'advanced') => void\n}) {\n const id = 'id-' + useId()\n const basicId = `${id}-basic`\n const advancedId = `${id}-advanced`\n\n return (\n <fieldset className=\"not-content\">\n <legend>Select a render method:</legend>\n\n <div>\n <input\n type=\"radio\"\n id={basicId}\n name={id}\n value=\"basic\"\n checked={props.value === 'basic'}\n onChange={() => props.onChange('basic')}\n />\n <label htmlFor={basicId}>basic</label>\n </div>\n\n <div>\n <input\n type=\"radio\"\n id={advancedId}\n name={id}\n value=\"advanced\"\n checked={props.value === 'advanced'}\n onChange={() => props.onChange('advanced')}\n />\n <label htmlFor={advancedId}>advanced</label>\n </div>\n </fieldset>\n )\n}\n"
36
+ "content": "'use client'\n\nimport { useId } from 'react'\n\nexport function MethodSelect(props: {\n value: 'basic' | 'advanced'\n onChange: (value: 'basic' | 'advanced') => void\n}) {\n const id = 'id-' + useId()\n const basicId = `${id}-basic`\n const advancedId = `${id}-advanced`\n\n return (\n <fieldset className=\"not-content\">\n <legend>Select a render method:</legend>\n\n <div>\n <input\n type=\"radio\"\n id={basicId}\n name={id}\n value=\"basic\"\n checked={props.value === 'basic'}\n onChange={() => props.onChange('basic')}\n />\n <label htmlFor={basicId}>basic</label>\n </div>\n\n <div>\n <input\n type=\"radio\"\n id={advancedId}\n name={id}\n value=\"advanced\"\n checked={props.value === 'advanced'}\n onChange={() => props.onChange('advanced')}\n />\n <label htmlFor={advancedId}>advanced</label>\n </div>\n </fieldset>\n )\n}\n"
37
37
  },
38
38
  {
39
39
  "path": "registry/src/react/examples/tweet/tweet-view.tsx",
40
40
  "type": "registry:component",
41
41
  "target": "components/editor/examples/tweet/tweet-view.tsx",
42
- "content": "'use client'\nimport type { ReactNodeViewProps } from 'prosekit/react'\nimport { Tweet } from 'react-tweet'\n\nexport function TweetView({ node }: ReactNodeViewProps) {\n const tweetId = node.attrs.tweetId as string\n return (\n <div className=\"[&_img]:m-0!\">\n <div>\n <strong>\n Rendered in React using library{' '}\n <span>\n <a href=\"https://github.com/vercel/react-tweet\" target=\"_blank\" rel=\"noopener noreferrer\">react-tweet</a>\n </span>\n </strong>\n </div>\n <Tweet id={tweetId} />\n </div>\n )\n}\n"
42
+ "content": "'use client'\n\nimport type { ReactNodeViewProps } from 'prosekit/react'\nimport { Tweet } from 'react-tweet'\n\nexport function TweetView({ node }: ReactNodeViewProps) {\n const tweetId = node.attrs.tweetId as string\n return (\n <div className=\"[&_img]:m-0!\">\n <div>\n <strong>\n Rendered in React using library{' '}\n <span>\n <a href=\"https://github.com/vercel/react-tweet\" target=\"_blank\" rel=\"noopener noreferrer\">react-tweet</a>\n </span>\n </strong>\n </div>\n <Tweet id={tweetId} />\n </div>\n )\n}\n"
43
43
  }
44
44
  ],
45
45
  "meta": {
@@ -14,7 +14,7 @@
14
14
  "path": "registry/src/react/examples/unmount/editor-component.tsx",
15
15
  "type": "registry:component",
16
16
  "target": "components/editor/examples/unmount/editor-component.tsx",
17
- "content": "'use client'\nimport 'prosekit/basic/style.css'\nimport 'prosekit/basic/typography.css'\n\nimport { defineBasicExtension } from 'prosekit/basic'\nimport { createEditor } from 'prosekit/core'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nimport { InlineMenu } from '../../ui/inline-menu'\n\nimport ExtensionComponent from './extension-component'\n\nexport default function EditorComponent(props: {\n placeholder: string\n}) {\n const editor = useMemo(() => {\n return createEditor({ extension: defineBasicExtension() })\n }, [])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n <InlineMenu />\n </div>\n </div>\n <ExtensionComponent placeholder={props.placeholder} />\n </ProseKit>\n )\n}\n"
17
+ "content": "'use client'\n\nimport 'prosekit/basic/style.css'\nimport 'prosekit/basic/typography.css'\n\nimport { defineBasicExtension } from 'prosekit/basic'\nimport { createEditor } from 'prosekit/core'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nimport { InlineMenu } from '../../ui/inline-menu'\n\nimport ExtensionComponent from './extension-component'\n\nexport default function EditorComponent(props: {\n placeholder: string\n}) {\n const editor = useMemo(() => {\n return createEditor({ extension: defineBasicExtension() })\n }, [])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n <InlineMenu />\n </div>\n </div>\n <ExtensionComponent placeholder={props.placeholder} />\n </ProseKit>\n )\n}\n"
18
18
  },
19
19
  {
20
20
  "path": "registry/src/react/examples/unmount/editor.tsx",
@@ -26,7 +26,7 @@
26
26
  "path": "registry/src/react/examples/unmount/extension-component.tsx",
27
27
  "type": "registry:component",
28
28
  "target": "components/editor/examples/unmount/extension-component.tsx",
29
- "content": "'use client'\nimport { definePlaceholder } from 'prosekit/extensions/placeholder'\nimport { useExtension } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nexport default function ExtensionComponent(props: {\n placeholder: string\n}) {\n const extension = useMemo(\n () => definePlaceholder({ placeholder: props.placeholder }),\n [props.placeholder],\n )\n\n useExtension(extension)\n\n return null\n}\n"
29
+ "content": "'use client'\n\nimport { definePlaceholder } from 'prosekit/extensions/placeholder'\nimport { useExtension } from 'prosekit/react'\nimport { useMemo } from 'react'\n\nexport default function ExtensionComponent(props: {\n placeholder: string\n}) {\n const extension = useMemo(\n () => definePlaceholder({ placeholder: props.placeholder }),\n [props.placeholder],\n )\n\n useExtension(extension)\n\n return null\n}\n"
30
30
  },
31
31
  {
32
32
  "path": "registry/src/react/examples/unmount/index.ts",
@@ -39,7 +39,7 @@
39
39
  "path": "registry/src/react/examples/user-menu-dynamic/user-menu-dynamic.tsx",
40
40
  "type": "registry:component",
41
41
  "target": "components/editor/examples/user-menu-dynamic/user-menu-dynamic.tsx",
42
- "content": "'use client'\nimport { useState } from 'react'\n\nimport { UserMenu } from '../../ui/user-menu'\n\nimport { useUserQuery } from './use-user-query'\n\nexport default function UserMenuDynamic() {\n const [query, setQuery] = useState('')\n const [open, setOpen] = useState(false)\n\n const { loading, users } = useUserQuery(query, open)\n\n return (\n <UserMenu\n users={users}\n loading={loading}\n onQueryChange={setQuery}\n onOpenChange={setOpen}\n />\n )\n}\n"
42
+ "content": "'use client'\n\nimport { useState } from 'react'\n\nimport { UserMenu } from '../../ui/user-menu'\n\nimport { useUserQuery } from './use-user-query'\n\nexport default function UserMenuDynamic() {\n const [query, setQuery] = useState('')\n const [open, setOpen] = useState(false)\n\n const { loading, users } = useUserQuery(query, open)\n\n return (\n <UserMenu\n users={users}\n loading={loading}\n onQueryChange={setQuery}\n onOpenChange={setOpen}\n />\n )\n}\n"
43
43
  }
44
44
  ],
45
45
  "meta": {
@@ -15,7 +15,7 @@
15
15
  "path": "registry/src/react/examples/view-adapter/atom-block-view.tsx",
16
16
  "type": "registry:component",
17
17
  "target": "components/editor/examples/view-adapter/atom-block-view.tsx",
18
- "content": "'use client'\nimport { useEditorDerivedValue, type ReactNodeViewProps } from 'prosekit/react'\nimport { useCallback } from 'react'\n\nexport function AtomBlockView(props: ReactNodeViewProps) {\n const docJSON = useEditorDerivedValue(useCallback(editor => {\n return JSON.stringify(editor.getDocJSON(), null, 2)\n }, []))\n\n return (\n <div data-atom-block=\"true\" data-atom-block-view=\"true\" className=\"bg-green-500/30\">\n <div data-testid=\"atom-block-view-label\">Atom Block View</div>\n <div data-testid=\"atom-block-view-pos\">{props.getPos()}</div>\n <div data-testid=\"atom-block-view-context\">\n <pre>{docJSON}</pre>\n </div>\n </div>\n )\n}\n"
18
+ "content": "'use client'\n\nimport { useEditorDerivedValue, type ReactNodeViewProps } from 'prosekit/react'\nimport { useCallback } from 'react'\n\nexport function AtomBlockView(props: ReactNodeViewProps) {\n const docJSON = useEditorDerivedValue(useCallback(editor => {\n return JSON.stringify(editor.getDocJSON(), null, 2)\n }, []))\n\n return (\n <div data-atom-block=\"true\" data-atom-block-view=\"true\" className=\"bg-green-500/30\">\n <div data-testid=\"atom-block-view-label\">Atom Block View</div>\n <div data-testid=\"atom-block-view-pos\">{props.getPos()}</div>\n <div data-testid=\"atom-block-view-context\">\n <pre>{docJSON}</pre>\n </div>\n </div>\n )\n}\n"
19
19
  },
20
20
  {
21
21
  "path": "registry/src/react/examples/view-adapter/editor.tsx",
@@ -16,7 +16,7 @@
16
16
  "path": "registry/src/react/examples/yjs/editor-component.tsx",
17
17
  "type": "registry:component",
18
18
  "target": "components/editor/examples/yjs/editor-component.tsx",
19
- "content": "'use client'\nimport 'prosekit/basic/style.css'\nimport 'prosekit/basic/typography.css'\nimport 'prosekit/extensions/yjs/style.css'\n\nimport { createEditor } from 'prosekit/core'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\nimport { WebsocketProvider } from 'y-websocket'\nimport * as Y from 'yjs'\n\nimport { Toolbar } from '../../ui/toolbar'\n\nimport { defineExtension } from './extension'\n\nexport default function EditorComponent(props: { room?: string }) {\n const editor = useMemo(() => {\n const doc = new Y.Doc()\n const provider = new WebsocketProvider(\n 'wss://demos.yjs.dev/ws',\n `github.com/prosekit/room_${props.room}`,\n doc,\n )\n\n const extension = defineExtension(doc, provider.awareness)\n return createEditor({ extension })\n }, [props.room])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <Toolbar />\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n </div>\n </div>\n </ProseKit>\n )\n}\n"
19
+ "content": "'use client'\n\nimport 'prosekit/basic/style.css'\nimport 'prosekit/basic/typography.css'\nimport 'prosekit/extensions/yjs/style.css'\n\nimport { createEditor } from 'prosekit/core'\nimport { ProseKit } from 'prosekit/react'\nimport { useMemo } from 'react'\nimport { WebsocketProvider } from 'y-websocket'\nimport * as Y from 'yjs'\n\nimport { Toolbar } from '../../ui/toolbar'\n\nimport { defineExtension } from './extension'\n\nexport default function EditorComponent(props: { room?: string }) {\n const editor = useMemo(() => {\n const doc = new Y.Doc()\n const provider = new WebsocketProvider(\n 'wss://demos.yjs.dev/ws',\n `github.com/prosekit/room_${props.room}`,\n doc,\n )\n\n const extension = defineExtension(doc, provider.awareness)\n return createEditor({ extension })\n }, [props.room])\n\n return (\n <ProseKit editor={editor}>\n <div className=\"box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white\">\n <Toolbar />\n <div className=\"relative w-full flex-1 box-border overflow-y-auto\">\n <div ref={editor.mount} className=\"ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500\"></div>\n </div>\n </div>\n </ProseKit>\n )\n}\n"
20
20
  },
21
21
  {
22
22
  "path": "registry/src/react/examples/yjs/editor.tsx",
@@ -14,7 +14,7 @@
14
14
  "path": "registry/src/react/ui/block-handle/block-handle.tsx",
15
15
  "type": "registry:component",
16
16
  "target": "components/editor/ui/block-handle/block-handle.tsx",
17
- "content": "'use client'\nimport { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopup, BlockHandlePositioner, BlockHandleRoot } from 'prosekit/react/block-handle'\n\ninterface Props {\n dir?: 'ltr' | 'rtl'\n}\n\nexport default function BlockHandle(props: Props) {\n return (\n <BlockHandleRoot>\n <BlockHandlePositioner\n placement={props.dir === 'rtl' ? 'right' : 'left'}\n className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\"\n >\n <BlockHandlePopup className=\"flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100\">\n <BlockHandleAdd className=\"h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50\">\n <div className=\"i-lucide-plus size-5 block\" />\n </BlockHandleAdd>\n <BlockHandleDraggable className=\"h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50\">\n <div className=\"i-lucide-grip-vertical size-5 block\" />\n </BlockHandleDraggable>\n </BlockHandlePopup>\n </BlockHandlePositioner>\n </BlockHandleRoot>\n )\n}\n"
17
+ "content": "'use client'\n\nimport { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopup, BlockHandlePositioner, BlockHandleRoot } from 'prosekit/react/block-handle'\n\ninterface Props {\n dir?: 'ltr' | 'rtl'\n}\n\nexport default function BlockHandle(props: Props) {\n return (\n <BlockHandleRoot>\n <BlockHandlePositioner\n placement={props.dir === 'rtl' ? 'right' : 'left'}\n className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\"\n >\n <BlockHandlePopup className=\"flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100\">\n <BlockHandleAdd className=\"h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50\">\n <div className=\"i-lucide-plus size-5 block\" />\n </BlockHandleAdd>\n <BlockHandleDraggable className=\"h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50\">\n <div className=\"i-lucide-grip-vertical size-5 block\" />\n </BlockHandleDraggable>\n </BlockHandlePopup>\n </BlockHandlePositioner>\n </BlockHandleRoot>\n )\n}\n"
18
18
  },
19
19
  {
20
20
  "path": "registry/src/react/ui/block-handle/index.ts",
@@ -12,7 +12,7 @@
12
12
  "path": "registry/src/react/ui/button/button.tsx",
13
13
  "type": "registry:component",
14
14
  "target": "components/editor/ui/button/button.tsx",
15
- "content": "'use client'\nimport { TooltipPopup, TooltipPositioner, TooltipRoot, TooltipTrigger } from 'prosekit/react/tooltip'\nimport type { MouseEventHandler, ReactNode } from 'react'\n\nexport default function Button(props: {\n pressed?: boolean\n disabled?: boolean\n onClick?: MouseEventHandler<HTMLButtonElement>\n tooltip?: string\n children: ReactNode\n}) {\n return (\n <TooltipRoot>\n <TooltipTrigger className=\"block\">\n <button\n data-state={props.pressed ? 'on' : 'off'}\n disabled={props.disabled}\n onClick={props.onClick}\n onMouseDown={(event) => {\n // Prevent the editor from being blurred when the button is clicked\n event.preventDefault()\n }}\n className=\"outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700\"\n >\n {props.children}\n {props.tooltip ? <span className=\"sr-only\">{props.tooltip}</span> : null}\n </button>\n </TooltipTrigger>\n {props.tooltip\n ? (\n <TooltipPositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <TooltipPopup className=\"flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-xs text-nowrap\">\n {props.tooltip}\n </TooltipPopup>\n </TooltipPositioner>\n )\n : null}\n </TooltipRoot>\n )\n}\n"
15
+ "content": "'use client'\n\nimport { TooltipPopup, TooltipPositioner, TooltipRoot, TooltipTrigger } from 'prosekit/react/tooltip'\nimport type { MouseEventHandler, ReactNode } from 'react'\n\nexport default function Button(props: {\n pressed?: boolean\n disabled?: boolean\n onClick?: MouseEventHandler<HTMLButtonElement>\n tooltip?: string\n children: ReactNode\n}) {\n return (\n <TooltipRoot>\n <TooltipTrigger className=\"block\">\n <button\n data-state={props.pressed ? 'on' : 'off'}\n disabled={props.disabled}\n onClick={props.onClick}\n onMouseDown={(event) => {\n // Prevent the editor from being blurred when the button is clicked\n event.preventDefault()\n }}\n className=\"outline-unset focus-visible:outline-unset flex items-center justify-center rounded-md p-2 font-medium transition focus-visible:ring-2 text-sm focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 disabled:pointer-events-none min-w-9 min-h-9 text-gray-900 dark:text-gray-50 disabled:text-gray-900/50 dark:disabled:text-gray-50/50 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 data-[state=on]:bg-gray-200 dark:data-[state=on]:bg-gray-700\"\n >\n {props.children}\n {props.tooltip ? <span className=\"sr-only\">{props.tooltip}</span> : null}\n </button>\n </TooltipTrigger>\n {props.tooltip\n ? (\n <TooltipPositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <TooltipPopup className=\"flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 overflow-hidden rounded-md border border-solid bg-gray-900 dark:bg-gray-50 px-3 py-1.5 text-xs text-gray-50 dark:text-gray-900 shadow-xs text-nowrap\">\n {props.tooltip}\n </TooltipPopup>\n </TooltipPositioner>\n )\n : null}\n </TooltipRoot>\n )\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "registry/src/react/ui/button/index.ts",
@@ -12,7 +12,7 @@
12
12
  "path": "registry/src/react/ui/code-block-view/code-block-view.tsx",
13
13
  "type": "registry:component",
14
14
  "target": "components/editor/ui/code-block-view/code-block-view.tsx",
15
- "content": "'use client'\nimport type { CodeBlockAttrs } from 'prosekit/extensions/code-block'\nimport { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'\nimport type { ReactNodeViewProps } from 'prosekit/react'\n\nexport default function CodeBlockView(props: ReactNodeViewProps) {\n const attrs = props.node.attrs as CodeBlockAttrs\n const language = attrs.language\n\n const setLanguage = (language: string) => {\n const attrs: CodeBlockAttrs = { language }\n props.setAttrs(attrs)\n }\n\n return (\n <>\n <div className=\"relative mx-2 top-3 h-0 select-none overflow-visible text-xs\" contentEditable={false}>\n <select\n aria-label=\"Code block language\"\n className=\"outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80\"\n onChange={(event) => setLanguage(event.target.value)}\n value={language || ''}\n >\n <option value=\"\">Plain Text</option>\n {shikiBundledLanguagesInfo.map((info) => (\n <option key={info.id} value={info.id}>\n {info.name}\n </option>\n ))}\n </select>\n </div>\n <pre ref={props.contentRef} data-language={language}></pre>\n </>\n )\n}\n"
15
+ "content": "'use client'\n\nimport type { CodeBlockAttrs } from 'prosekit/extensions/code-block'\nimport { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'\nimport type { ReactNodeViewProps } from 'prosekit/react'\n\nexport default function CodeBlockView(props: ReactNodeViewProps) {\n const attrs = props.node.attrs as CodeBlockAttrs\n const language = attrs.language\n\n const setLanguage = (language: string) => {\n const attrs: CodeBlockAttrs = { language }\n props.setAttrs(attrs)\n }\n\n return (\n <>\n <div className=\"relative mx-2 top-3 h-0 select-none overflow-visible text-xs\" contentEditable={false}>\n <select\n aria-label=\"Code block language\"\n className=\"outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80\"\n onChange={(event) => setLanguage(event.target.value)}\n value={language || ''}\n >\n <option value=\"\">Plain Text</option>\n {shikiBundledLanguagesInfo.map((info) => (\n <option key={info.id} value={info.id}>\n {info.name}\n </option>\n ))}\n </select>\n </div>\n <pre ref={props.contentRef} data-language={language}></pre>\n </>\n )\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "registry/src/react/ui/code-block-view/index.ts",
@@ -12,7 +12,7 @@
12
12
  "path": "registry/src/react/ui/drop-indicator/drop-indicator.tsx",
13
13
  "type": "registry:component",
14
14
  "target": "components/editor/ui/drop-indicator/drop-indicator.tsx",
15
- "content": "'use client'\nimport { DropIndicator as BaseDropIndicator } from 'prosekit/react/drop-indicator'\n\nexport default function DropIndicator() {\n return <BaseDropIndicator className=\"z-50 transition-all bg-blue-500\" />\n}\n"
15
+ "content": "'use client'\n\nimport { DropIndicator as BaseDropIndicator } from 'prosekit/react/drop-indicator'\n\nexport default function DropIndicator() {\n return <BaseDropIndicator className=\"z-50 transition-all bg-blue-500\" />\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "registry/src/react/ui/drop-indicator/index.ts",
@@ -14,7 +14,7 @@
14
14
  "path": "registry/src/react/ui/image-upload-popover/image-upload-popover.tsx",
15
15
  "type": "registry:component",
16
16
  "target": "components/editor/ui/image-upload-popover/image-upload-popover.tsx",
17
- "content": "'use client'\nimport type { Uploader } from 'prosekit/extensions/file'\nimport type { ImageExtension } from 'prosekit/extensions/image'\nimport { useEditor } from 'prosekit/react'\nimport { PopoverPopup, PopoverPositioner, PopoverRoot, PopoverTrigger } from 'prosekit/react/popover'\nimport type { OpenChangeEvent } from 'prosekit/web/popover'\nimport { useId, useState, type ReactNode } from 'react'\n\nimport { Button } from '../button'\n\nexport default function ImageUploadPopover(props: {\n uploader: Uploader<string>\n tooltip: string\n disabled: boolean\n children: ReactNode\n}) {\n const [open, setOpen] = useState(false)\n const [url, setUrl] = useState('')\n const [file, setFile] = useState<File | null>(null)\n const ariaId = useId()\n\n const editor = useEditor<ImageExtension>()\n\n const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = (\n event,\n ) => {\n const file = event.target.files?.[0]\n\n if (file) {\n setFile(file)\n setUrl('')\n } else {\n setFile(null)\n }\n }\n\n const handleUrlChange: React.ChangeEventHandler<HTMLInputElement> = (\n event,\n ) => {\n const url = event.target.value\n\n if (url) {\n setUrl(url)\n setFile(null)\n } else {\n setUrl('')\n }\n }\n\n const deferResetState = () => {\n setTimeout(() => {\n setUrl('')\n setFile(null)\n }, 300)\n }\n\n const handleSubmit = () => {\n if (url) {\n editor.commands.insertImage({ src: url })\n } else if (file) {\n editor.commands.uploadImage({ file, uploader: props.uploader })\n }\n setOpen(false)\n deferResetState()\n }\n\n const handleOpenChange = (event: OpenChangeEvent) => {\n if (!event.detail) {\n deferResetState()\n }\n setOpen(event.detail)\n }\n\n return (\n <PopoverRoot open={open} onOpenChange={handleOpenChange}>\n <PopoverTrigger>\n <Button pressed={open} disabled={props.disabled} tooltip={props.tooltip}>\n {props.children}\n </Button>\n </PopoverTrigger>\n\n <PopoverPositioner placement=\"bottom\" className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <PopoverPopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col gap-y-4 p-6 text-sm w-sm\">\n {file ? null : (\n <>\n <label htmlFor={`id-link-${ariaId}`}>Embed Link</label>\n <input\n id={`id-link-${ariaId}`}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50\"\n placeholder=\"Paste the image link...\"\n type=\"url\"\n value={url}\n onChange={handleUrlChange}\n />\n </>\n )}\n\n {url ? null : (\n <>\n <label htmlFor={`id-upload-${ariaId}`}>Upload</label>\n <input\n id={`id-upload-${ariaId}`}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50\"\n accept=\"image/*\"\n type=\"file\"\n onChange={handleFileChange}\n />\n </>\n )}\n\n {url\n ? (\n <button className=\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white dark:ring-offset-gray-950 transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-0 bg-gray-900 dark:bg-gray-50 text-gray-50 dark:text-gray-900 hover:bg-gray-900/90 dark:hover:bg-gray-50/90 h-10 px-4 py-2 w-full\" onClick={handleSubmit}>\n Insert Image\n </button>\n )\n : null}\n\n {file\n ? (\n <button className=\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white dark:ring-offset-gray-950 transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-0 bg-gray-900 dark:bg-gray-50 text-gray-50 dark:text-gray-900 hover:bg-gray-900/90 dark:hover:bg-gray-50/90 h-10 px-4 py-2 w-full\" onClick={handleSubmit}>\n Upload Image\n </button>\n )\n : null}\n </PopoverPopup>\n </PopoverPositioner>\n </PopoverRoot>\n )\n}\n"
17
+ "content": "'use client'\n\nimport type { Uploader } from 'prosekit/extensions/file'\nimport type { ImageExtension } from 'prosekit/extensions/image'\nimport { useEditor } from 'prosekit/react'\nimport { PopoverPopup, PopoverPositioner, PopoverRoot, PopoverTrigger } from 'prosekit/react/popover'\nimport type { OpenChangeEvent } from 'prosekit/web/popover'\nimport { useId, useState, type ReactNode } from 'react'\n\nimport { Button } from '../button'\n\nexport default function ImageUploadPopover(props: {\n uploader: Uploader<string>\n tooltip: string\n disabled: boolean\n children: ReactNode\n}) {\n const [open, setOpen] = useState(false)\n const [url, setUrl] = useState('')\n const [file, setFile] = useState<File | null>(null)\n const ariaId = useId()\n\n const editor = useEditor<ImageExtension>()\n\n const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = (\n event,\n ) => {\n const file = event.target.files?.[0]\n\n if (file) {\n setFile(file)\n setUrl('')\n } else {\n setFile(null)\n }\n }\n\n const handleUrlChange: React.ChangeEventHandler<HTMLInputElement> = (\n event,\n ) => {\n const url = event.target.value\n\n if (url) {\n setUrl(url)\n setFile(null)\n } else {\n setUrl('')\n }\n }\n\n const deferResetState = () => {\n setTimeout(() => {\n setUrl('')\n setFile(null)\n }, 300)\n }\n\n const handleSubmit = () => {\n if (url) {\n editor.commands.insertImage({ src: url })\n } else if (file) {\n editor.commands.uploadImage({ file, uploader: props.uploader })\n }\n setOpen(false)\n deferResetState()\n }\n\n const handleOpenChange = (event: OpenChangeEvent) => {\n if (!event.detail) {\n deferResetState()\n }\n setOpen(event.detail)\n }\n\n return (\n <PopoverRoot open={open} onOpenChange={handleOpenChange}>\n <PopoverTrigger>\n <Button pressed={open} disabled={props.disabled} tooltip={props.tooltip}>\n {props.children}\n </Button>\n </PopoverTrigger>\n\n <PopoverPositioner placement=\"bottom\" className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <PopoverPopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col gap-y-4 p-6 text-sm w-sm\">\n {file ? null : (\n <>\n <label htmlFor={`id-link-${ariaId}`}>Embed Link</label>\n <input\n id={`id-link-${ariaId}`}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50\"\n placeholder=\"Paste the image link...\"\n type=\"url\"\n value={url}\n onChange={handleUrlChange}\n />\n </>\n )}\n\n {url ? null : (\n <>\n <label htmlFor={`id-upload-${ariaId}`}>Upload</label>\n <input\n id={`id-upload-${ariaId}`}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50\"\n accept=\"image/*\"\n type=\"file\"\n onChange={handleFileChange}\n />\n </>\n )}\n\n {url\n ? (\n <button className=\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white dark:ring-offset-gray-950 transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-0 bg-gray-900 dark:bg-gray-50 text-gray-50 dark:text-gray-900 hover:bg-gray-900/90 dark:hover:bg-gray-50/90 h-10 px-4 py-2 w-full\" onClick={handleSubmit}>\n Insert Image\n </button>\n )\n : null}\n\n {file\n ? (\n <button className=\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white dark:ring-offset-gray-950 transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-0 bg-gray-900 dark:bg-gray-50 text-gray-50 dark:text-gray-900 hover:bg-gray-900/90 dark:hover:bg-gray-50/90 h-10 px-4 py-2 w-full\" onClick={handleSubmit}>\n Upload Image\n </button>\n )\n : null}\n </PopoverPopup>\n </PopoverPositioner>\n </PopoverRoot>\n )\n}\n"
18
18
  },
19
19
  {
20
20
  "path": "registry/src/react/ui/image-upload-popover/index.ts",
@@ -14,7 +14,7 @@
14
14
  "path": "registry/src/react/ui/image-view/image-view.tsx",
15
15
  "type": "registry:component",
16
16
  "target": "components/editor/ui/image-view/image-view.tsx",
17
- "content": "'use client'\nimport { UploadTask } from 'prosekit/extensions/file'\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { ReactNodeViewProps } from 'prosekit/react'\nimport { ResizableHandle, ResizableRoot } from 'prosekit/react/resizable'\nimport { useEffect, useState, type SyntheticEvent } from 'react'\n\nexport default function ImageView(props: ReactNodeViewProps) {\n const attrs = props.node.attrs as ImageAttrs\n const url = attrs.src || ''\n const uploading = url.startsWith('blob:')\n\n const [aspectRatio, setAspectRatio] = useState<number | undefined>()\n const [error, setError] = useState<string | undefined>()\n const [progress, setProgress] = useState(0)\n\n useEffect(() => {\n if (!uploading) return\n\n const uploadTask = UploadTask.get<string>(url)\n if (!uploadTask) return\n\n let canceled = false\n\n uploadTask.finished.catch((error) => {\n if (canceled) return\n setError(String(error))\n })\n const unsubscribeProgress = uploadTask.subscribeProgress(({ loaded, total }) => {\n if (canceled) return\n setProgress(total ? loaded / total : 0)\n })\n\n return () => {\n canceled = true\n unsubscribeProgress()\n }\n }, [url, uploading])\n\n const handleImageLoad = (event: SyntheticEvent) => {\n const img = event.target as HTMLImageElement\n const { naturalWidth, naturalHeight } = img\n const ratio = naturalWidth / naturalHeight\n if (ratio && Number.isFinite(ratio)) {\n setAspectRatio(ratio)\n }\n if (naturalWidth && naturalHeight && (!attrs.width || !attrs.height)) {\n props.setAttrs({ width: naturalWidth, height: naturalHeight })\n }\n }\n\n return (\n <ResizableRoot\n width={attrs.width ?? undefined}\n height={attrs.height ?? undefined}\n aspectRatio={aspectRatio}\n onResizeEnd={(event) => props.setAttrs(event.detail)}\n data-selected={props.selected ? '' : undefined}\n className=\"relative flex items-center justify-center box-border overflow-hidden my-2 group max-h-150 max-w-full min-h-16 min-w-16 outline-2 outline-transparent data-selected:outline-blue-500 outline-solid\"\n >\n {url && !error && (\n <img\n src={url}\n onLoad={handleImageLoad}\n alt=\"upload preview\"\n className=\"h-full w-full max-w-full max-h-full object-contain\"\n />\n )}\n {uploading && !error && (\n <div className=\"absolute bottom-0 left-0 m-1 flex content-center items-center gap-2 rounded-sm bg-gray-800/60 p-1.5 text-xs text-white/80 transition\">\n <div className=\"i-lucide-loader-circle size-4 animate-spin block\"></div>\n <div>{Math.round(progress * 100)}%</div>\n </div>\n )}\n {error && (\n <div className=\"absolute bottom-0 left-0 right-0 top-0 flex flex-col items-center justify-center gap-4 bg-gray-200 p-2 text-sm dark:bg-gray-800 @container\">\n <div className=\"i-lucide-image-off size-8 block\"></div>\n <div className=\"hidden opacity-80 @xs:block\">\n Failed to upload image\n </div>\n </div>\n )}\n <ResizableHandle\n className=\"absolute bottom-0 right-0 rounded-sm m-1.5 p-1 transition bg-gray-900/30 active:bg-gray-800/60 hover:bg-gray-800/60 text-white/50 active:text-white/80 active:translate-x-0.5 active:translate-y-0.5 opacity-0 hover:opacity-100 group-hover:opacity-100 group-data-resizing:opacity-100\"\n position=\"bottom-right\"\n >\n <div className=\"i-lucide-arrow-down-right size-4 block\"></div>\n </ResizableHandle>\n </ResizableRoot>\n )\n}\n"
17
+ "content": "'use client'\n\nimport { UploadTask } from 'prosekit/extensions/file'\nimport type { ImageAttrs } from 'prosekit/extensions/image'\nimport type { ReactNodeViewProps } from 'prosekit/react'\nimport { ResizableHandle, ResizableRoot } from 'prosekit/react/resizable'\nimport { useEffect, useState, type SyntheticEvent } from 'react'\n\nexport default function ImageView(props: ReactNodeViewProps) {\n const attrs = props.node.attrs as ImageAttrs\n const url = attrs.src || ''\n const uploading = url.startsWith('blob:')\n\n const [aspectRatio, setAspectRatio] = useState<number | undefined>()\n const [error, setError] = useState<string | undefined>()\n const [progress, setProgress] = useState(0)\n\n useEffect(() => {\n if (!uploading) return\n\n const uploadTask = UploadTask.get<string>(url)\n if (!uploadTask) return\n\n let canceled = false\n\n uploadTask.finished.catch((error) => {\n if (canceled) return\n setError(String(error))\n })\n const unsubscribeProgress = uploadTask.subscribeProgress(({ loaded, total }) => {\n if (canceled) return\n setProgress(total ? loaded / total : 0)\n })\n\n return () => {\n canceled = true\n unsubscribeProgress()\n }\n }, [url, uploading])\n\n const handleImageLoad = (event: SyntheticEvent) => {\n const img = event.target as HTMLImageElement\n const { naturalWidth, naturalHeight } = img\n const ratio = naturalWidth / naturalHeight\n if (ratio && Number.isFinite(ratio)) {\n setAspectRatio(ratio)\n }\n if (naturalWidth && naturalHeight && (!attrs.width || !attrs.height)) {\n props.setAttrs({ width: naturalWidth, height: naturalHeight })\n }\n }\n\n return (\n <ResizableRoot\n width={attrs.width ?? undefined}\n height={attrs.height ?? undefined}\n aspectRatio={aspectRatio}\n onResizeEnd={(event) => props.setAttrs(event.detail)}\n data-selected={props.selected ? '' : undefined}\n className=\"relative flex items-center justify-center box-border overflow-hidden my-2 group max-h-150 max-w-full min-h-16 min-w-16 outline-2 outline-transparent data-selected:outline-blue-500 outline-solid\"\n >\n {url && !error && (\n <img\n src={url}\n onLoad={handleImageLoad}\n alt=\"upload preview\"\n className=\"h-full w-full max-w-full max-h-full object-contain\"\n />\n )}\n {uploading && !error && (\n <div className=\"absolute bottom-0 left-0 m-1 flex content-center items-center gap-2 rounded-sm bg-gray-800/60 p-1.5 text-xs text-white/80 transition\">\n <div className=\"i-lucide-loader-circle size-4 animate-spin block\"></div>\n <div>{Math.round(progress * 100)}%</div>\n </div>\n )}\n {error && (\n <div className=\"absolute bottom-0 left-0 right-0 top-0 flex flex-col items-center justify-center gap-4 bg-gray-200 p-2 text-sm dark:bg-gray-800 @container\">\n <div className=\"i-lucide-image-off size-8 block\"></div>\n <div className=\"hidden opacity-80 @xs:block\">\n Failed to upload image\n </div>\n </div>\n )}\n <ResizableHandle\n className=\"absolute bottom-0 right-0 rounded-sm m-1.5 p-1 transition bg-gray-900/30 active:bg-gray-800/60 hover:bg-gray-800/60 text-white/50 active:text-white/80 active:translate-x-0.5 active:translate-y-0.5 opacity-0 hover:opacity-100 group-hover:opacity-100 group-data-resizing:opacity-100\"\n position=\"bottom-right\"\n >\n <div className=\"i-lucide-arrow-down-right size-4 block\"></div>\n </ResizableHandle>\n </ResizableRoot>\n )\n}\n"
18
18
  },
19
19
  {
20
20
  "path": "registry/src/react/ui/image-view/index.ts",
@@ -22,7 +22,7 @@
22
22
  "path": "registry/src/react/ui/inline-menu/inline-menu.tsx",
23
23
  "type": "registry:component",
24
24
  "target": "components/editor/ui/inline-menu/inline-menu.tsx",
25
- "content": "'use client'\nimport type { BasicExtension } from 'prosekit/basic'\nimport type { Editor } from 'prosekit/core'\nimport type { LinkAttrs } from 'prosekit/extensions/link'\nimport type { EditorState } from 'prosekit/pm/state'\nimport { useEditor, useEditorDerivedValue } from 'prosekit/react'\nimport { InlinePopoverPopup, InlinePopoverPositioner, InlinePopoverRoot } from 'prosekit/react/inline-popover'\nimport { useState } from 'react'\n\nimport { Button } from '../button'\n\nfunction getInlineMenuItems(editor: Editor<BasicExtension>) {\n return {\n bold: editor.commands.toggleBold\n ? {\n isActive: editor.marks.bold.isActive(),\n canExec: editor.commands.toggleBold.canExec(),\n command: () => editor.commands.toggleBold(),\n }\n : undefined,\n italic: editor.commands.toggleItalic\n ? {\n isActive: editor.marks.italic.isActive(),\n canExec: editor.commands.toggleItalic.canExec(),\n command: () => editor.commands.toggleItalic(),\n }\n : undefined,\n underline: editor.commands.toggleUnderline\n ? {\n isActive: editor.marks.underline.isActive(),\n canExec: editor.commands.toggleUnderline.canExec(),\n command: () => editor.commands.toggleUnderline(),\n }\n : undefined,\n strike: editor.commands.toggleStrike\n ? {\n isActive: editor.marks.strike.isActive(),\n canExec: editor.commands.toggleStrike.canExec(),\n command: () => editor.commands.toggleStrike(),\n }\n : undefined,\n code: editor.commands.toggleCode\n ? {\n isActive: editor.marks.code.isActive(),\n canExec: editor.commands.toggleCode.canExec(),\n command: () => editor.commands.toggleCode(),\n }\n : undefined,\n link: editor.commands.addLink\n ? {\n isActive: editor.marks.link.isActive(),\n canExec: editor.commands.addLink.canExec({ href: '' }),\n command: () => editor.commands.expandLink(),\n currentLink: getCurrentLink(editor.state) || '',\n }\n : undefined,\n }\n}\n\nfunction getCurrentLink(state: EditorState): string | undefined {\n const { $from } = state.selection\n const marks = $from.marksAcross($from)\n if (!marks) {\n return\n }\n for (const mark of marks) {\n if (mark.type.name === 'link') {\n return (mark.attrs as LinkAttrs).href\n }\n }\n}\n\nexport default function InlineMenu() {\n const editor = useEditor<BasicExtension>()\n const items = useEditorDerivedValue(getInlineMenuItems)\n\n const [linkMenuOpen, setLinkMenuOpen] = useState(false)\n const toggleLinkMenuOpen = () => setLinkMenuOpen((open) => !open)\n\n const handleLinkUpdate = (href?: string) => {\n if (href) {\n editor.commands.addLink({ href })\n } else {\n editor.commands.removeLink()\n }\n\n setLinkMenuOpen(false)\n editor.focus()\n }\n\n return (\n <>\n <InlinePopoverRoot\n onOpenChange={(event) => {\n if (!event.detail) {\n setLinkMenuOpen(false)\n }\n }}\n >\n <InlinePopoverPositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <InlinePopoverPopup\n data-testid=\"inline-menu-main\"\n className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-md p-1\"\n >\n {items.bold && (\n <Button\n pressed={items.bold.isActive}\n disabled={!items.bold.canExec}\n onClick={items.bold.command}\n tooltip=\"Bold\"\n >\n <div className=\"i-lucide-bold size-5 block\"></div>\n </Button>\n )}\n {items.italic && (\n <Button\n pressed={items.italic.isActive}\n disabled={!items.italic.canExec}\n onClick={items.italic.command}\n tooltip=\"Italic\"\n >\n <div className=\"i-lucide-italic size-5 block\"></div>\n </Button>\n )}\n {items.underline && (\n <Button\n pressed={items.underline.isActive}\n disabled={!items.underline.canExec}\n onClick={items.underline.command}\n tooltip=\"Underline\"\n >\n <div className=\"i-lucide-underline size-5 block\"></div>\n </Button>\n )}\n {items.strike && (\n <Button\n pressed={items.strike.isActive}\n disabled={!items.strike.canExec}\n onClick={items.strike.command}\n tooltip=\"Strikethrough\"\n >\n <div className=\"i-lucide-strikethrough size-5 block\"></div>\n </Button>\n )}\n {items.code && (\n <Button\n pressed={items.code.isActive}\n disabled={!items.code.canExec}\n onClick={items.code.command}\n tooltip=\"Code\"\n >\n <div className=\"i-lucide-code size-5 block\"></div>\n </Button>\n )}\n {items.link && items.link.canExec && (\n <Button\n pressed={items.link.isActive}\n onClick={() => {\n items.link?.command?.()\n toggleLinkMenuOpen()\n }}\n tooltip=\"Link\"\n >\n <div className=\"i-lucide-link size-5 block\"></div>\n </Button>\n )}\n </InlinePopoverPopup>\n </InlinePopoverPositioner>\n </InlinePopoverRoot>\n\n {items.link && (\n <InlinePopoverRoot\n defaultOpen={false}\n open={linkMenuOpen}\n onOpenChange={(event) => setLinkMenuOpen(event.detail)}\n >\n <InlinePopoverPositioner placement=\"bottom\" className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <InlinePopoverPopup\n data-testid=\"inline-menu-link\"\n className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex flex-col w-xs rounded-lg p-4 gap-y-2 items-stretch\"\n >\n {linkMenuOpen && (\n <form\n onSubmit={(event) => {\n event.preventDefault()\n const target = event.target as HTMLFormElement | null\n const href = target?.querySelector('input')?.value?.trim()\n handleLinkUpdate(href)\n }}\n >\n <input\n placeholder=\"Paste the link...\"\n defaultValue={items.link.currentLink}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50\"\n />\n </form>\n )}\n {items.link.isActive && (\n <button\n onClick={() => handleLinkUpdate()}\n onMouseDown={(event) => event.preventDefault()}\n className=\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white dark:ring-offset-gray-950 transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-0 bg-gray-900 dark:bg-gray-50 text-gray-50 dark:text-gray-900 hover:bg-gray-900/90 dark:hover:bg-gray-50/90 h-9 px-3\"\n >\n Remove link\n </button>\n )}\n </InlinePopoverPopup>\n </InlinePopoverPositioner>\n </InlinePopoverRoot>\n )}\n </>\n )\n}\n"
25
+ "content": "'use client'\n\nimport type { BasicExtension } from 'prosekit/basic'\nimport type { Editor } from 'prosekit/core'\nimport type { LinkAttrs } from 'prosekit/extensions/link'\nimport type { EditorState } from 'prosekit/pm/state'\nimport { useEditor, useEditorDerivedValue } from 'prosekit/react'\nimport { InlinePopoverPopup, InlinePopoverPositioner, InlinePopoverRoot } from 'prosekit/react/inline-popover'\nimport { useState } from 'react'\n\nimport { Button } from '../button'\n\nfunction getInlineMenuItems(editor: Editor<BasicExtension>) {\n return {\n bold: editor.commands.toggleBold\n ? {\n isActive: editor.marks.bold.isActive(),\n canExec: editor.commands.toggleBold.canExec(),\n command: () => editor.commands.toggleBold(),\n }\n : undefined,\n italic: editor.commands.toggleItalic\n ? {\n isActive: editor.marks.italic.isActive(),\n canExec: editor.commands.toggleItalic.canExec(),\n command: () => editor.commands.toggleItalic(),\n }\n : undefined,\n underline: editor.commands.toggleUnderline\n ? {\n isActive: editor.marks.underline.isActive(),\n canExec: editor.commands.toggleUnderline.canExec(),\n command: () => editor.commands.toggleUnderline(),\n }\n : undefined,\n strike: editor.commands.toggleStrike\n ? {\n isActive: editor.marks.strike.isActive(),\n canExec: editor.commands.toggleStrike.canExec(),\n command: () => editor.commands.toggleStrike(),\n }\n : undefined,\n code: editor.commands.toggleCode\n ? {\n isActive: editor.marks.code.isActive(),\n canExec: editor.commands.toggleCode.canExec(),\n command: () => editor.commands.toggleCode(),\n }\n : undefined,\n link: editor.commands.addLink\n ? {\n isActive: editor.marks.link.isActive(),\n canExec: editor.commands.addLink.canExec({ href: '' }),\n command: () => editor.commands.expandLink(),\n currentLink: getCurrentLink(editor.state) || '',\n }\n : undefined,\n }\n}\n\nfunction getCurrentLink(state: EditorState): string | undefined {\n const { $from } = state.selection\n const marks = $from.marksAcross($from)\n if (!marks) {\n return\n }\n for (const mark of marks) {\n if (mark.type.name === 'link') {\n return (mark.attrs as LinkAttrs).href\n }\n }\n}\n\nexport default function InlineMenu() {\n const editor = useEditor<BasicExtension>()\n const items = useEditorDerivedValue(getInlineMenuItems)\n\n const [linkMenuOpen, setLinkMenuOpen] = useState(false)\n const toggleLinkMenuOpen = () => setLinkMenuOpen((open) => !open)\n\n const handleLinkUpdate = (href?: string) => {\n if (href) {\n editor.commands.addLink({ href })\n } else {\n editor.commands.removeLink()\n }\n\n setLinkMenuOpen(false)\n editor.focus()\n }\n\n return (\n <>\n <InlinePopoverRoot\n onOpenChange={(event) => {\n if (!event.detail) {\n setLinkMenuOpen(false)\n }\n }}\n >\n <InlinePopoverPositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <InlinePopoverPopup\n data-testid=\"inline-menu-main\"\n className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-md p-1\"\n >\n {items.bold && (\n <Button\n pressed={items.bold.isActive}\n disabled={!items.bold.canExec}\n onClick={items.bold.command}\n tooltip=\"Bold\"\n >\n <div className=\"i-lucide-bold size-5 block\"></div>\n </Button>\n )}\n {items.italic && (\n <Button\n pressed={items.italic.isActive}\n disabled={!items.italic.canExec}\n onClick={items.italic.command}\n tooltip=\"Italic\"\n >\n <div className=\"i-lucide-italic size-5 block\"></div>\n </Button>\n )}\n {items.underline && (\n <Button\n pressed={items.underline.isActive}\n disabled={!items.underline.canExec}\n onClick={items.underline.command}\n tooltip=\"Underline\"\n >\n <div className=\"i-lucide-underline size-5 block\"></div>\n </Button>\n )}\n {items.strike && (\n <Button\n pressed={items.strike.isActive}\n disabled={!items.strike.canExec}\n onClick={items.strike.command}\n tooltip=\"Strikethrough\"\n >\n <div className=\"i-lucide-strikethrough size-5 block\"></div>\n </Button>\n )}\n {items.code && (\n <Button\n pressed={items.code.isActive}\n disabled={!items.code.canExec}\n onClick={items.code.command}\n tooltip=\"Code\"\n >\n <div className=\"i-lucide-code size-5 block\"></div>\n </Button>\n )}\n {items.link && items.link.canExec && (\n <Button\n pressed={items.link.isActive}\n onClick={() => {\n items.link?.command?.()\n toggleLinkMenuOpen()\n }}\n tooltip=\"Link\"\n >\n <div className=\"i-lucide-link size-5 block\"></div>\n </Button>\n )}\n </InlinePopoverPopup>\n </InlinePopoverPositioner>\n </InlinePopoverRoot>\n\n {items.link && (\n <InlinePopoverRoot\n defaultOpen={false}\n open={linkMenuOpen}\n onOpenChange={(event) => setLinkMenuOpen(event.detail)}\n >\n <InlinePopoverPositioner placement=\"bottom\" className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <InlinePopoverPopup\n data-testid=\"inline-menu-link\"\n className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex flex-col w-xs rounded-lg p-4 gap-y-2 items-stretch\"\n >\n {linkMenuOpen && (\n <form\n onSubmit={(event) => {\n event.preventDefault()\n const target = event.target as HTMLFormElement | null\n const href = target?.querySelector('input')?.value?.trim()\n handleLinkUpdate(href)\n }}\n >\n <input\n placeholder=\"Paste the link...\"\n defaultValue={items.link.currentLink}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50\"\n />\n </form>\n )}\n {items.link.isActive && (\n <button\n onClick={() => handleLinkUpdate()}\n onMouseDown={(event) => event.preventDefault()}\n className=\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white dark:ring-offset-gray-950 transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-0 bg-gray-900 dark:bg-gray-50 text-gray-50 dark:text-gray-900 hover:bg-gray-900/90 dark:hover:bg-gray-50/90 h-9 px-3\"\n >\n Remove link\n </button>\n )}\n </InlinePopoverPopup>\n </InlinePopoverPositioner>\n </InlinePopoverRoot>\n )}\n </>\n )\n}\n"
26
26
  }
27
27
  ],
28
28
  "meta": {
@@ -22,7 +22,7 @@
22
22
  "path": "registry/src/react/ui/search/search.tsx",
23
23
  "type": "registry:component",
24
24
  "target": "components/editor/ui/search/search.tsx",
25
- "content": "'use client'\nimport { defineSearchQuery, type SearchCommandsExtension } from 'prosekit/extensions/search'\nimport { useEditor, useExtension } from 'prosekit/react'\nimport { useMemo, useState } from 'react'\n\nimport { Button } from '../button'\n\nexport default function Search(props: { onClose?: VoidFunction }) {\n const [showReplace, setShowReplace] = useState(false)\n const toggleReplace = () => setShowReplace((value) => !value)\n\n const [searchText, setSearchText] = useState('')\n const [replaceText, setReplaceText] = useState('')\n\n const extension = useMemo(() => {\n if (!searchText) {\n return null\n }\n return defineSearchQuery({ search: searchText, replace: replaceText })\n }, [searchText, replaceText])\n\n useExtension(extension)\n\n const editor = useEditor<SearchCommandsExtension>()\n\n const handleSearchKeyDown = (event: React.KeyboardEvent) => {\n if (isEnter(event)) {\n event.preventDefault()\n editor.commands.findNext()\n } else if (isShiftEnter(event)) {\n event.preventDefault()\n editor.commands.findPrev()\n }\n }\n\n const handleReplaceKeyDown = (event: React.KeyboardEvent) => {\n if (isEnter(event)) {\n event.preventDefault()\n editor.commands.replaceNext()\n } else if (isShiftEnter(event)) {\n event.preventDefault()\n editor.commands.replaceAll()\n }\n }\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b grid grid-cols-[min-content_1fr_min-content] gap-2 p-2\">\n <Button tooltip=\"Toggle Replace\" onClick={toggleReplace}>\n <span\n data-rotate={showReplace ? '' : undefined}\n className=\"i-lucide-chevron-right size-5 block transition-transform data-rotate:rotate-90\"\n />\n </Button>\n <input\n placeholder=\"Search\"\n type=\"text\"\n value={searchText}\n onChange={(event) => setSearchText(event.target.value)}\n onKeyDown={handleSearchKeyDown}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50 col-start-2\"\n />\n <div className=\"flex items-center justify-between gap-1\">\n <Button\n tooltip=\"Previous (Shift Enter)\"\n onClick={editor.commands.findPrev}\n >\n <span className=\"i-lucide-arrow-left size-5 block\" />\n </Button>\n <Button tooltip=\"Next (Enter)\" onClick={editor.commands.findNext}>\n <span className=\"i-lucide-arrow-right size-5 block\" />\n </Button>\n <Button tooltip=\"Close\" onClick={props.onClose}>\n <span className=\"i-lucide-x size-5 block\" />\n </Button>\n </div>\n {showReplace && (\n <input\n placeholder=\"Replace\"\n type=\"text\"\n value={replaceText}\n onChange={(event) => setReplaceText(event.target.value)}\n onKeyDown={handleReplaceKeyDown}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50 col-start-2\"\n />\n )}\n {showReplace && (\n <div className=\"flex items-center justify-between gap-1\">\n <Button\n tooltip=\"Replace (Enter)\"\n onClick={editor.commands.replaceNext}\n >\n Replace\n </Button>\n <Button\n tooltip=\"Replace All (Shift Enter)\"\n onClick={editor.commands.replaceAll}\n >\n All\n </Button>\n </div>\n )}\n </div>\n )\n}\n\nfunction isEnter(event: React.KeyboardEvent) {\n return (\n event.key === 'Enter'\n && !event.shiftKey\n && !event.metaKey\n && !event.altKey\n && !event.ctrlKey\n && !event.nativeEvent.isComposing\n )\n}\n\nfunction isShiftEnter(event: React.KeyboardEvent) {\n return (\n event.key === 'Enter'\n && event.shiftKey\n && !event.metaKey\n && !event.altKey\n && !event.ctrlKey\n && !event.nativeEvent.isComposing\n )\n}\n"
25
+ "content": "'use client'\n\nimport { defineSearchQuery, type SearchCommandsExtension } from 'prosekit/extensions/search'\nimport { useEditor, useExtension } from 'prosekit/react'\nimport { useMemo, useState } from 'react'\n\nimport { Button } from '../button'\n\nexport default function Search(props: { onClose?: VoidFunction }) {\n const [showReplace, setShowReplace] = useState(false)\n const toggleReplace = () => setShowReplace((value) => !value)\n\n const [searchText, setSearchText] = useState('')\n const [replaceText, setReplaceText] = useState('')\n\n const extension = useMemo(() => {\n if (!searchText) {\n return null\n }\n return defineSearchQuery({ search: searchText, replace: replaceText })\n }, [searchText, replaceText])\n\n useExtension(extension)\n\n const editor = useEditor<SearchCommandsExtension>()\n\n const handleSearchKeyDown = (event: React.KeyboardEvent) => {\n if (isEnter(event)) {\n event.preventDefault()\n editor.commands.findNext()\n } else if (isShiftEnter(event)) {\n event.preventDefault()\n editor.commands.findPrev()\n }\n }\n\n const handleReplaceKeyDown = (event: React.KeyboardEvent) => {\n if (isEnter(event)) {\n event.preventDefault()\n editor.commands.replaceNext()\n } else if (isShiftEnter(event)) {\n event.preventDefault()\n editor.commands.replaceAll()\n }\n }\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b grid grid-cols-[min-content_1fr_min-content] gap-2 p-2\">\n <Button tooltip=\"Toggle Replace\" onClick={toggleReplace}>\n <span\n data-rotate={showReplace ? '' : undefined}\n className=\"i-lucide-chevron-right size-5 block transition-transform data-rotate:rotate-90\"\n />\n </Button>\n <input\n placeholder=\"Search\"\n type=\"text\"\n value={searchText}\n onChange={(event) => setSearchText(event.target.value)}\n onKeyDown={handleSearchKeyDown}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50 col-start-2\"\n />\n <div className=\"flex items-center justify-between gap-1\">\n <Button\n tooltip=\"Previous (Shift Enter)\"\n onClick={editor.commands.findPrev}\n >\n <span className=\"i-lucide-arrow-left size-5 block\" />\n </Button>\n <Button tooltip=\"Next (Enter)\" onClick={editor.commands.findNext}>\n <span className=\"i-lucide-arrow-right size-5 block\" />\n </Button>\n <Button tooltip=\"Close\" onClick={props.onClose}>\n <span className=\"i-lucide-x size-5 block\" />\n </Button>\n </div>\n {showReplace && (\n <input\n placeholder=\"Replace\"\n type=\"text\"\n value={replaceText}\n onChange={(event) => setReplaceText(event.target.value)}\n onKeyDown={handleReplaceKeyDown}\n className=\"flex h-9 rounded-md w-full bg-white dark:bg-gray-950 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-500 transition border box-border border-gray-200 dark:border-gray-800 border-solid ring-0 ring-transparent focus-visible:ring-2 focus-visible:ring-gray-900 dark:focus-visible:ring-gray-300 focus-visible:ring-offset-0 outline-hidden focus-visible:outline-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50 col-start-2\"\n />\n )}\n {showReplace && (\n <div className=\"flex items-center justify-between gap-1\">\n <Button\n tooltip=\"Replace (Enter)\"\n onClick={editor.commands.replaceNext}\n >\n Replace\n </Button>\n <Button\n tooltip=\"Replace All (Shift Enter)\"\n onClick={editor.commands.replaceAll}\n >\n All\n </Button>\n </div>\n )}\n </div>\n )\n}\n\nfunction isEnter(event: React.KeyboardEvent) {\n return (\n event.key === 'Enter'\n && !event.shiftKey\n && !event.metaKey\n && !event.altKey\n && !event.ctrlKey\n && !event.nativeEvent.isComposing\n )\n}\n\nfunction isShiftEnter(event: React.KeyboardEvent) {\n return (\n event.key === 'Enter'\n && event.shiftKey\n && !event.metaKey\n && !event.altKey\n && !event.ctrlKey\n && !event.nativeEvent.isComposing\n )\n}\n"
26
26
  }
27
27
  ],
28
28
  "meta": {
@@ -18,19 +18,19 @@
18
18
  "path": "registry/src/react/ui/slash-menu/slash-menu-empty.tsx",
19
19
  "type": "registry:component",
20
20
  "target": "components/editor/ui/slash-menu/slash-menu-empty.tsx",
21
- "content": "'use client'\nimport { AutocompleteEmpty } from 'prosekit/react/autocomplete'\n\nexport default function SlashMenuEmpty() {\n return (\n <AutocompleteEmpty className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n <span>No results</span>\n </AutocompleteEmpty>\n )\n}\n"
21
+ "content": "'use client'\n\nimport { AutocompleteEmpty } from 'prosekit/react/autocomplete'\n\nexport default function SlashMenuEmpty() {\n return (\n <AutocompleteEmpty className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n <span>No results</span>\n </AutocompleteEmpty>\n )\n}\n"
22
22
  },
23
23
  {
24
24
  "path": "registry/src/react/ui/slash-menu/slash-menu-item.tsx",
25
25
  "type": "registry:component",
26
26
  "target": "components/editor/ui/slash-menu/slash-menu-item.tsx",
27
- "content": "'use client'\nimport { AutocompleteItem } from 'prosekit/react/autocomplete'\n\nexport default function SlashMenuItem(props: {\n label: string\n kbd?: string\n onSelect: () => void\n}) {\n return (\n <AutocompleteItem onSelect={props.onSelect} className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n <span>{props.label}</span>\n {props.kbd && <kbd className=\"text-xs font-mono text-gray-400 dark:text-gray-500\">{props.kbd}</kbd>}\n </AutocompleteItem>\n )\n}\n"
27
+ "content": "'use client'\n\nimport { AutocompleteItem } from 'prosekit/react/autocomplete'\n\nexport default function SlashMenuItem(props: {\n label: string\n kbd?: string\n onSelect: () => void\n}) {\n return (\n <AutocompleteItem onSelect={props.onSelect} className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n <span>{props.label}</span>\n {props.kbd && <kbd className=\"text-xs font-mono text-gray-400 dark:text-gray-500\">{props.kbd}</kbd>}\n </AutocompleteItem>\n )\n}\n"
28
28
  },
29
29
  {
30
30
  "path": "registry/src/react/ui/slash-menu/slash-menu.tsx",
31
31
  "type": "registry:component",
32
32
  "target": "components/editor/ui/slash-menu/slash-menu.tsx",
33
- "content": "'use client'\nimport type { BasicExtension } from 'prosekit/basic'\nimport { canUseRegexLookbehind } from 'prosekit/core'\nimport { useEditor } from 'prosekit/react'\nimport { AutocompletePopup, AutocompletePositioner, AutocompleteRoot } from 'prosekit/react/autocomplete'\n\nimport SlashMenuEmpty from './slash-menu-empty'\nimport SlashMenuItem from './slash-menu-item'\n\n// Match inputs like \"/\", \"/table\", \"/heading 1\" etc. Do not match \"/ heading\".\nconst regex = canUseRegexLookbehind() ? /(?<!\\S)\\/(\\S.*)?$/u : /\\/(\\S.*)?$/u\n\nexport default function SlashMenu() {\n const editor = useEditor<BasicExtension>()\n\n return (\n <AutocompleteRoot regex={regex}>\n <AutocompletePositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <AutocompletePopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col relative max-h-100 min-w-60 select-none overflow-auto whitespace-nowrap p-1\">\n <SlashMenuItem\n label=\"Text\"\n onSelect={() => editor.commands.setParagraph()}\n />\n\n <SlashMenuItem\n label=\"Heading 1\"\n kbd=\"#\"\n onSelect={() => editor.commands.setHeading({ level: 1 })}\n />\n\n <SlashMenuItem\n label=\"Heading 2\"\n kbd=\"##\"\n onSelect={() => editor.commands.setHeading({ level: 2 })}\n />\n\n <SlashMenuItem\n label=\"Heading 3\"\n kbd=\"###\"\n onSelect={() => editor.commands.setHeading({ level: 3 })}\n />\n\n <SlashMenuItem\n label=\"Bullet list\"\n kbd=\"-\"\n onSelect={() => editor.commands.wrapInList({ kind: 'bullet' })}\n />\n\n <SlashMenuItem\n label=\"Ordered list\"\n kbd=\"1.\"\n onSelect={() => editor.commands.wrapInList({ kind: 'ordered' })}\n />\n\n <SlashMenuItem\n label=\"Task list\"\n kbd=\"[]\"\n onSelect={() => editor.commands.wrapInList({ kind: 'task' })}\n />\n\n <SlashMenuItem\n label=\"Toggle list\"\n kbd=\">>\"\n onSelect={() => editor.commands.wrapInList({ kind: 'toggle' })}\n />\n\n <SlashMenuItem\n label=\"Quote\"\n kbd=\">\"\n onSelect={() => editor.commands.setBlockquote()}\n />\n\n <SlashMenuItem\n label=\"Table\"\n onSelect={() => editor.commands.insertTable({ row: 3, col: 3 })}\n />\n\n <SlashMenuItem\n label=\"Divider\"\n kbd=\"---\"\n onSelect={() => editor.commands.insertHorizontalRule()}\n />\n\n <SlashMenuItem\n label=\"Code\"\n kbd=\"```\"\n onSelect={() => editor.commands.setCodeBlock()}\n />\n\n <SlashMenuEmpty />\n </AutocompletePopup>\n </AutocompletePositioner>\n </AutocompleteRoot>\n )\n}\n"
33
+ "content": "'use client'\n\nimport type { BasicExtension } from 'prosekit/basic'\nimport { canUseRegexLookbehind } from 'prosekit/core'\nimport { useEditor } from 'prosekit/react'\nimport { AutocompletePopup, AutocompletePositioner, AutocompleteRoot } from 'prosekit/react/autocomplete'\n\nimport SlashMenuEmpty from './slash-menu-empty'\nimport SlashMenuItem from './slash-menu-item'\n\n// Match inputs like \"/\", \"/table\", \"/heading 1\" etc. Do not match \"/ heading\".\nconst regex = canUseRegexLookbehind() ? /(?<!\\S)\\/(\\S.*)?$/u : /\\/(\\S.*)?$/u\n\nexport default function SlashMenu() {\n const editor = useEditor<BasicExtension>()\n\n return (\n <AutocompleteRoot regex={regex}>\n <AutocompletePositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <AutocompletePopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col relative max-h-100 min-w-60 select-none overflow-auto whitespace-nowrap p-1\">\n <SlashMenuItem\n label=\"Text\"\n onSelect={() => editor.commands.setParagraph()}\n />\n\n <SlashMenuItem\n label=\"Heading 1\"\n kbd=\"#\"\n onSelect={() => editor.commands.setHeading({ level: 1 })}\n />\n\n <SlashMenuItem\n label=\"Heading 2\"\n kbd=\"##\"\n onSelect={() => editor.commands.setHeading({ level: 2 })}\n />\n\n <SlashMenuItem\n label=\"Heading 3\"\n kbd=\"###\"\n onSelect={() => editor.commands.setHeading({ level: 3 })}\n />\n\n <SlashMenuItem\n label=\"Bullet list\"\n kbd=\"-\"\n onSelect={() => editor.commands.wrapInList({ kind: 'bullet' })}\n />\n\n <SlashMenuItem\n label=\"Ordered list\"\n kbd=\"1.\"\n onSelect={() => editor.commands.wrapInList({ kind: 'ordered' })}\n />\n\n <SlashMenuItem\n label=\"Task list\"\n kbd=\"[]\"\n onSelect={() => editor.commands.wrapInList({ kind: 'task' })}\n />\n\n <SlashMenuItem\n label=\"Toggle list\"\n kbd=\">>\"\n onSelect={() => editor.commands.wrapInList({ kind: 'toggle' })}\n />\n\n <SlashMenuItem\n label=\"Quote\"\n kbd=\">\"\n onSelect={() => editor.commands.setBlockquote()}\n />\n\n <SlashMenuItem\n label=\"Table\"\n onSelect={() => editor.commands.insertTable({ row: 3, col: 3 })}\n />\n\n <SlashMenuItem\n label=\"Divider\"\n kbd=\"---\"\n onSelect={() => editor.commands.insertHorizontalRule()}\n />\n\n <SlashMenuItem\n label=\"Code\"\n kbd=\"```\"\n onSelect={() => editor.commands.setCodeBlock()}\n />\n\n <SlashMenuEmpty />\n </AutocompletePopup>\n </AutocompletePositioner>\n </AutocompleteRoot>\n )\n}\n"
34
34
  }
35
35
  ],
36
36
  "meta": {
@@ -20,7 +20,7 @@
20
20
  "path": "registry/src/react/ui/table-handle/table-handle.tsx",
21
21
  "type": "registry:component",
22
22
  "target": "components/editor/ui/table-handle/table-handle.tsx",
23
- "content": "'use client'\nimport type { Editor } from 'prosekit/core'\nimport type { TableExtension } from 'prosekit/extensions/table'\nimport { useEditorDerivedValue } from 'prosekit/react'\nimport { MenuItem, MenuPopup, MenuPositioner } from 'prosekit/react/menu'\nimport {\n TableHandleColumnMenuRoot,\n TableHandleColumnMenuTrigger,\n TableHandleColumnPopup,\n TableHandleColumnPositioner,\n TableHandleDragPreview,\n TableHandleDropIndicator,\n TableHandleRoot,\n TableHandleRowMenuRoot,\n TableHandleRowMenuTrigger,\n TableHandleRowPopup,\n TableHandleRowPositioner,\n} from 'prosekit/react/table-handle'\n\nfunction getTableHandleState(editor: Editor<TableExtension>) {\n return {\n addTableColumnBefore: {\n canExec: editor.commands.addTableColumnBefore.canExec(),\n command: () => editor.commands.addTableColumnBefore(),\n },\n addTableColumnAfter: {\n canExec: editor.commands.addTableColumnAfter.canExec(),\n command: () => editor.commands.addTableColumnAfter(),\n },\n deleteCellSelection: {\n canExec: editor.commands.deleteCellSelection.canExec(),\n command: () => editor.commands.deleteCellSelection(),\n },\n deleteTableColumn: {\n canExec: editor.commands.deleteTableColumn.canExec(),\n command: () => editor.commands.deleteTableColumn(),\n },\n addTableRowAbove: {\n canExec: editor.commands.addTableRowAbove.canExec(),\n command: () => editor.commands.addTableRowAbove(),\n },\n addTableRowBelow: {\n canExec: editor.commands.addTableRowBelow.canExec(),\n command: () => editor.commands.addTableRowBelow(),\n },\n deleteTableRow: {\n canExec: editor.commands.deleteTableRow.canExec(),\n command: () => editor.commands.deleteTableRow(),\n },\n deleteTable: {\n canExec: editor.commands.deleteTable.canExec(),\n command: () => editor.commands.deleteTable(),\n },\n }\n}\n\ninterface Props {\n dir?: 'ltr' | 'rtl'\n}\n\nexport default function TableHandle(props: Props) {\n const state = useEditorDerivedValue(getTableHandleState)\n\n return (\n <TableHandleRoot>\n <TableHandleDragPreview />\n <TableHandleDropIndicator />\n <TableHandleColumnPositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <TableHandleColumnPopup className=\"translate-y-[50%] flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100\">\n <TableHandleColumnMenuRoot>\n <TableHandleColumnMenuTrigger className=\"h-4.5 w-6 flex items-center box-border justify-center bg-white dark:bg-gray-950 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50 border border-gray-200 dark:border-gray-800 border-solid p-0 transition-colors overflow-clip\">\n <div className=\"i-lucide-grip-horizontal size-5 min-h-5 min-w-5 block\"></div>\n </TableHandleColumnMenuTrigger>\n <MenuPositioner className=\"overflow-visible bg-transparent\">\n <MenuPopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex flex-col max-h-100 min-w-32 select-none overflow-auto whitespace-nowrap p-1 outline-none\">\n {state.addTableColumnBefore.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.addTableColumnBefore.command}\n >\n <span>Insert Left</span>\n </MenuItem>\n )}\n {state.addTableColumnAfter.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.addTableColumnAfter.command}\n >\n <span>Insert Right</span>\n </MenuItem>\n )}\n {state.deleteCellSelection.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.deleteCellSelection.command}\n >\n <span>Clear Contents</span>\n <span className=\"text-xs tracking-widest text-gray-500 dark:text-gray-500\">Del</span>\n </MenuItem>\n )}\n {state.deleteTableColumn.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.deleteTableColumn.command}\n >\n <span>Delete Column</span>\n </MenuItem>\n )}\n {state.deleteTable.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n data-danger=\"\"\n onSelect={state.deleteTable.command}\n >\n <span>Delete Table</span>\n </MenuItem>\n )}\n </MenuPopup>\n </MenuPositioner>\n </TableHandleColumnMenuRoot>\n </TableHandleColumnPopup>\n </TableHandleColumnPositioner>\n <TableHandleRowPositioner\n placement={props.dir === 'rtl' ? 'right' : 'left'}\n className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\"\n >\n <TableHandleRowPopup className=\"ltr:translate-x-[50%] rtl:translate-x-[-50%] flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100\">\n <TableHandleRowMenuRoot>\n <TableHandleRowMenuTrigger className=\"h-6 w-4.5 flex items-center box-border justify-center bg-white dark:bg-gray-950 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50 border border-gray-200 dark:border-gray-800 border-solid p-0 transition-colors overflow-clip\">\n <div className=\"i-lucide-grip-vertical size-5 min-h-5 min-w-5 block\"></div>\n </TableHandleRowMenuTrigger>\n <MenuPositioner className=\"overflow-visible bg-transparent\">\n <MenuPopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex flex-col max-h-100 min-w-32 select-none overflow-auto whitespace-nowrap p-1 outline-none\">\n {state.addTableRowAbove.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.addTableRowAbove.command}\n >\n <span>Insert Above</span>\n </MenuItem>\n )}\n {state.addTableRowBelow.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.addTableRowBelow.command}\n >\n <span>Insert Below</span>\n </MenuItem>\n )}\n {state.deleteCellSelection.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.deleteCellSelection.command}\n >\n <span>Clear Contents</span>\n <span className=\"text-xs tracking-widest text-gray-500 dark:text-gray-500\">Del</span>\n </MenuItem>\n )}\n {state.deleteTableRow.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.deleteTableRow.command}\n >\n <span>Delete Row</span>\n </MenuItem>\n )}\n {state.deleteTable.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n data-danger=\"\"\n onSelect={state.deleteTable.command}\n >\n <span>Delete Table</span>\n </MenuItem>\n )}\n </MenuPopup>\n </MenuPositioner>\n </TableHandleRowMenuRoot>\n </TableHandleRowPopup>\n </TableHandleRowPositioner>\n </TableHandleRoot>\n )\n}\n"
23
+ "content": "'use client'\n\nimport type { Editor } from 'prosekit/core'\nimport type { TableExtension } from 'prosekit/extensions/table'\nimport { useEditorDerivedValue } from 'prosekit/react'\nimport { MenuItem, MenuPopup, MenuPositioner } from 'prosekit/react/menu'\nimport {\n TableHandleColumnMenuRoot,\n TableHandleColumnMenuTrigger,\n TableHandleColumnPopup,\n TableHandleColumnPositioner,\n TableHandleDragPreview,\n TableHandleDropIndicator,\n TableHandleRoot,\n TableHandleRowMenuRoot,\n TableHandleRowMenuTrigger,\n TableHandleRowPopup,\n TableHandleRowPositioner,\n} from 'prosekit/react/table-handle'\n\nfunction getTableHandleState(editor: Editor<TableExtension>) {\n return {\n addTableColumnBefore: {\n canExec: editor.commands.addTableColumnBefore.canExec(),\n command: () => editor.commands.addTableColumnBefore(),\n },\n addTableColumnAfter: {\n canExec: editor.commands.addTableColumnAfter.canExec(),\n command: () => editor.commands.addTableColumnAfter(),\n },\n deleteCellSelection: {\n canExec: editor.commands.deleteCellSelection.canExec(),\n command: () => editor.commands.deleteCellSelection(),\n },\n deleteTableColumn: {\n canExec: editor.commands.deleteTableColumn.canExec(),\n command: () => editor.commands.deleteTableColumn(),\n },\n addTableRowAbove: {\n canExec: editor.commands.addTableRowAbove.canExec(),\n command: () => editor.commands.addTableRowAbove(),\n },\n addTableRowBelow: {\n canExec: editor.commands.addTableRowBelow.canExec(),\n command: () => editor.commands.addTableRowBelow(),\n },\n deleteTableRow: {\n canExec: editor.commands.deleteTableRow.canExec(),\n command: () => editor.commands.deleteTableRow(),\n },\n deleteTable: {\n canExec: editor.commands.deleteTable.canExec(),\n command: () => editor.commands.deleteTable(),\n },\n }\n}\n\ninterface Props {\n dir?: 'ltr' | 'rtl'\n}\n\nexport default function TableHandle(props: Props) {\n const state = useEditorDerivedValue(getTableHandleState)\n\n return (\n <TableHandleRoot>\n <TableHandleDragPreview />\n <TableHandleDropIndicator />\n <TableHandleColumnPositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <TableHandleColumnPopup className=\"translate-y-[50%] flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100\">\n <TableHandleColumnMenuRoot>\n <TableHandleColumnMenuTrigger className=\"h-4.5 w-6 flex items-center box-border justify-center bg-white dark:bg-gray-950 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50 border border-gray-200 dark:border-gray-800 border-solid p-0 transition-colors overflow-clip\">\n <div className=\"i-lucide-grip-horizontal size-5 min-h-5 min-w-5 block\"></div>\n </TableHandleColumnMenuTrigger>\n <MenuPositioner className=\"overflow-visible bg-transparent\">\n <MenuPopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex flex-col max-h-100 min-w-32 select-none overflow-auto whitespace-nowrap p-1 outline-none\">\n {state.addTableColumnBefore.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.addTableColumnBefore.command}\n >\n <span>Insert Left</span>\n </MenuItem>\n )}\n {state.addTableColumnAfter.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.addTableColumnAfter.command}\n >\n <span>Insert Right</span>\n </MenuItem>\n )}\n {state.deleteCellSelection.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.deleteCellSelection.command}\n >\n <span>Clear Contents</span>\n <span className=\"text-xs tracking-widest text-gray-500 dark:text-gray-500\">Del</span>\n </MenuItem>\n )}\n {state.deleteTableColumn.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.deleteTableColumn.command}\n >\n <span>Delete Column</span>\n </MenuItem>\n )}\n {state.deleteTable.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n data-danger=\"\"\n onSelect={state.deleteTable.command}\n >\n <span>Delete Table</span>\n </MenuItem>\n )}\n </MenuPopup>\n </MenuPositioner>\n </TableHandleColumnMenuRoot>\n </TableHandleColumnPopup>\n </TableHandleColumnPositioner>\n <TableHandleRowPositioner\n placement={props.dir === 'rtl' ? 'right' : 'left'}\n className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\"\n >\n <TableHandleRowPopup className=\"ltr:translate-x-[50%] rtl:translate-x-[-50%] flex box-border motion-safe:duration-100 data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100\">\n <TableHandleRowMenuRoot>\n <TableHandleRowMenuTrigger className=\"h-6 w-4.5 flex items-center box-border justify-center bg-white dark:bg-gray-950 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50 border border-gray-200 dark:border-gray-800 border-solid p-0 transition-colors overflow-clip\">\n <div className=\"i-lucide-grip-vertical size-5 min-h-5 min-w-5 block\"></div>\n </TableHandleRowMenuTrigger>\n <MenuPositioner className=\"overflow-visible bg-transparent\">\n <MenuPopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg relative flex flex-col max-h-100 min-w-32 select-none overflow-auto whitespace-nowrap p-1 outline-none\">\n {state.addTableRowAbove.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.addTableRowAbove.command}\n >\n <span>Insert Above</span>\n </MenuItem>\n )}\n {state.addTableRowBelow.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.addTableRowBelow.command}\n >\n <span>Insert Below</span>\n </MenuItem>\n )}\n {state.deleteCellSelection.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.deleteCellSelection.command}\n >\n <span>Clear Contents</span>\n <span className=\"text-xs tracking-widest text-gray-500 dark:text-gray-500\">Del</span>\n </MenuItem>\n )}\n {state.deleteTableRow.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={state.deleteTableRow.command}\n >\n <span>Delete Row</span>\n </MenuItem>\n )}\n {state.deleteTable.canExec && (\n <MenuItem\n className=\"relative min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 flex items-center justify-between gap-8 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 hover:data-[disabled=true]:opacity-50 data-danger:text-red-500 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n data-danger=\"\"\n onSelect={state.deleteTable.command}\n >\n <span>Delete Table</span>\n </MenuItem>\n )}\n </MenuPopup>\n </MenuPositioner>\n </TableHandleRowMenuRoot>\n </TableHandleRowPopup>\n </TableHandleRowPositioner>\n </TableHandleRoot>\n )\n}\n"
24
24
  }
25
25
  ],
26
26
  "meta": {
@@ -18,7 +18,7 @@
18
18
  "path": "registry/src/react/ui/tag-menu/tag-menu.tsx",
19
19
  "type": "registry:component",
20
20
  "target": "components/editor/ui/tag-menu/tag-menu.tsx",
21
- "content": "'use client'\nimport type { BasicExtension } from 'prosekit/basic'\nimport type { Union } from 'prosekit/core'\nimport type { MentionExtension } from 'prosekit/extensions/mention'\nimport { useEditor } from 'prosekit/react'\nimport {\n AutocompleteEmpty,\n AutocompleteItem,\n AutocompletePopup,\n AutocompletePositioner,\n AutocompleteRoot,\n} from 'prosekit/react/autocomplete'\n\nconst regex = /#[\\da-z]*$/i\n\nexport default function TagMenu(props: { tags: { id: number; label: string }[] }) {\n const editor = useEditor<Union<[MentionExtension, BasicExtension]>>()\n\n const handleTagInsert = (id: number, label: string) => {\n editor.commands.insertMention({\n id: id.toString(),\n value: '#' + label,\n kind: 'tag',\n })\n editor.commands.insertText({ text: ' ' })\n }\n\n return (\n <AutocompleteRoot regex={regex}>\n <AutocompletePositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <AutocompletePopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col relative max-h-100 min-w-60 select-none overflow-auto whitespace-nowrap p-1\">\n <AutocompleteEmpty className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n No results\n </AutocompleteEmpty>\n\n {props.tags.map((tag) => (\n <AutocompleteItem\n key={tag.id}\n className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={() => handleTagInsert(tag.id, tag.label)}\n >\n #{tag.label}\n </AutocompleteItem>\n ))}\n </AutocompletePopup>\n </AutocompletePositioner>\n </AutocompleteRoot>\n )\n}\n"
21
+ "content": "'use client'\n\nimport type { BasicExtension } from 'prosekit/basic'\nimport type { Union } from 'prosekit/core'\nimport type { MentionExtension } from 'prosekit/extensions/mention'\nimport { useEditor } from 'prosekit/react'\nimport {\n AutocompleteEmpty,\n AutocompleteItem,\n AutocompletePopup,\n AutocompletePositioner,\n AutocompleteRoot,\n} from 'prosekit/react/autocomplete'\n\nconst regex = /#[\\da-z]*$/i\n\nexport default function TagMenu(props: { tags: { id: number; label: string }[] }) {\n const editor = useEditor<Union<[MentionExtension, BasicExtension]>>()\n\n const handleTagInsert = (id: number, label: string) => {\n editor.commands.insertMention({\n id: id.toString(),\n value: '#' + label,\n kind: 'tag',\n })\n editor.commands.insertText({ text: ' ' })\n }\n\n return (\n <AutocompleteRoot regex={regex}>\n <AutocompletePositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <AutocompletePopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col relative max-h-100 min-w-60 select-none overflow-auto whitespace-nowrap p-1\">\n <AutocompleteEmpty className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n No results\n </AutocompleteEmpty>\n\n {props.tags.map((tag) => (\n <AutocompleteItem\n key={tag.id}\n className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={() => handleTagInsert(tag.id, tag.label)}\n >\n #{tag.label}\n </AutocompleteItem>\n ))}\n </AutocompletePopup>\n </AutocompletePositioner>\n </AutocompleteRoot>\n )\n}\n"
22
22
  }
23
23
  ],
24
24
  "meta": {
@@ -23,7 +23,7 @@
23
23
  "path": "registry/src/react/ui/toolbar/toolbar.tsx",
24
24
  "type": "registry:component",
25
25
  "target": "components/editor/ui/toolbar/toolbar.tsx",
26
- "content": "'use client'\nimport type { BasicExtension } from 'prosekit/basic'\nimport type { Editor } from 'prosekit/core'\nimport type { Uploader } from 'prosekit/extensions/file'\nimport { useEditorDerivedValue } from 'prosekit/react'\n\nimport { Button } from '../button'\nimport { ImageUploadPopover } from '../image-upload-popover'\n\nfunction getToolbarItems(editor: Editor<BasicExtension>) {\n return {\n undo: editor.commands.undo\n ? {\n isActive: false,\n canExec: editor.commands.undo.canExec(),\n command: () => editor.commands.undo(),\n }\n : undefined,\n redo: editor.commands.redo\n ? {\n isActive: false,\n canExec: editor.commands.redo.canExec(),\n command: () => editor.commands.redo(),\n }\n : undefined,\n bold: editor.commands.toggleBold\n ? {\n isActive: editor.marks.bold.isActive(),\n canExec: editor.commands.toggleBold.canExec(),\n command: () => editor.commands.toggleBold(),\n }\n : undefined,\n italic: editor.commands.toggleItalic\n ? {\n isActive: editor.marks.italic.isActive(),\n canExec: editor.commands.toggleItalic.canExec(),\n command: () => editor.commands.toggleItalic(),\n }\n : undefined,\n underline: editor.commands.toggleUnderline\n ? {\n isActive: editor.marks.underline.isActive(),\n canExec: editor.commands.toggleUnderline.canExec(),\n command: () => editor.commands.toggleUnderline(),\n }\n : undefined,\n strike: editor.commands.toggleStrike\n ? {\n isActive: editor.marks.strike.isActive(),\n canExec: editor.commands.toggleStrike.canExec(),\n command: () => editor.commands.toggleStrike(),\n }\n : undefined,\n code: editor.commands.toggleCode\n ? {\n isActive: editor.marks.code.isActive(),\n canExec: editor.commands.toggleCode.canExec(),\n command: () => editor.commands.toggleCode(),\n }\n : undefined,\n codeBlock: editor.commands.insertCodeBlock\n ? {\n isActive: editor.nodes.codeBlock.isActive(),\n canExec: editor.commands.insertCodeBlock.canExec({ language: 'javascript' }),\n command: () => editor.commands.insertCodeBlock({ language: 'javascript' }),\n }\n : undefined,\n heading1: editor.commands.toggleHeading\n ? {\n isActive: editor.nodes.heading.isActive({ level: 1 }),\n canExec: editor.commands.toggleHeading.canExec({ level: 1 }),\n command: () => editor.commands.toggleHeading({ level: 1 }),\n }\n : undefined,\n heading2: editor.commands.toggleHeading\n ? {\n isActive: editor.nodes.heading.isActive({ level: 2 }),\n canExec: editor.commands.toggleHeading.canExec({ level: 2 }),\n command: () => editor.commands.toggleHeading({ level: 2 }),\n }\n : undefined,\n heading3: editor.commands.toggleHeading\n ? {\n isActive: editor.nodes.heading.isActive({ level: 3 }),\n canExec: editor.commands.toggleHeading.canExec({ level: 3 }),\n command: () => editor.commands.toggleHeading({ level: 3 }),\n }\n : undefined,\n horizontalRule: editor.commands.insertHorizontalRule\n ? {\n isActive: editor.nodes.horizontalRule.isActive(),\n canExec: editor.commands.insertHorizontalRule.canExec(),\n command: () => editor.commands.insertHorizontalRule(),\n }\n : undefined,\n blockquote: editor.commands.toggleBlockquote\n ? {\n isActive: editor.nodes.blockquote.isActive(),\n canExec: editor.commands.toggleBlockquote.canExec(),\n command: () => editor.commands.toggleBlockquote(),\n }\n : undefined,\n bulletList: editor.commands.toggleList\n ? {\n isActive: editor.nodes.list.isActive({ kind: 'bullet' }),\n canExec: editor.commands.toggleList.canExec({ kind: 'bullet' }),\n command: () => editor.commands.toggleList({ kind: 'bullet' }),\n }\n : undefined,\n orderedList: editor.commands.toggleList\n ? {\n isActive: editor.nodes.list.isActive({ kind: 'ordered' }),\n canExec: editor.commands.toggleList.canExec({ kind: 'ordered' }),\n command: () => editor.commands.toggleList({ kind: 'ordered' }),\n }\n : undefined,\n taskList: editor.commands.toggleList\n ? {\n isActive: editor.nodes.list.isActive({ kind: 'task' }),\n canExec: editor.commands.toggleList.canExec({ kind: 'task' }),\n command: () => editor.commands.toggleList({ kind: 'task' }),\n }\n : undefined,\n toggleList: editor.commands.toggleList\n ? {\n isActive: editor.nodes.list.isActive({ kind: 'toggle' }),\n canExec: editor.commands.toggleList.canExec({ kind: 'toggle' }),\n command: () => editor.commands.toggleList({ kind: 'toggle' }),\n }\n : undefined,\n indentList: editor.commands.indentList\n ? {\n isActive: false,\n canExec: editor.commands.indentList.canExec(),\n command: () => editor.commands.indentList(),\n }\n : undefined,\n dedentList: editor.commands.dedentList\n ? {\n isActive: false,\n canExec: editor.commands.dedentList.canExec(),\n command: () => editor.commands.dedentList(),\n }\n : undefined,\n insertImage: editor.commands.insertImage\n ? {\n isActive: false,\n canExec: editor.commands.insertImage.canExec(),\n }\n : undefined,\n }\n}\n\nexport default function Toolbar(props: { uploader?: Uploader<string> }) {\n const items = useEditorDerivedValue(getToolbarItems)\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n {items.undo && (\n <Button\n pressed={items.undo.isActive}\n disabled={!items.undo.canExec}\n onClick={items.undo.command}\n tooltip=\"Undo\"\n >\n <div className=\"i-lucide-undo-2 size-5 block\" />\n </Button>\n )}\n {items.redo && (\n <Button\n pressed={items.redo.isActive}\n disabled={!items.redo.canExec}\n onClick={items.redo.command}\n tooltip=\"Redo\"\n >\n <div className=\"i-lucide-redo-2 size-5 block\" />\n </Button>\n )}\n\n {items.bold && (\n <Button\n pressed={items.bold.isActive}\n disabled={!items.bold.canExec}\n onClick={items.bold.command}\n tooltip=\"Bold\"\n >\n <div className=\"i-lucide-bold size-5 block\" />\n </Button>\n )}\n {items.italic && (\n <Button\n pressed={items.italic.isActive}\n disabled={!items.italic.canExec}\n onClick={items.italic.command}\n tooltip=\"Italic\"\n >\n <div className=\"i-lucide-italic size-5 block\" />\n </Button>\n )}\n {items.underline && (\n <Button\n pressed={items.underline.isActive}\n disabled={!items.underline.canExec}\n onClick={items.underline.command}\n tooltip=\"Underline\"\n >\n <div className=\"i-lucide-underline size-5 block\" />\n </Button>\n )}\n {items.strike && (\n <Button\n pressed={items.strike.isActive}\n disabled={!items.strike.canExec}\n onClick={items.strike.command}\n tooltip=\"Strike\"\n >\n <div className=\"i-lucide-strikethrough size-5 block\" />\n </Button>\n )}\n {items.code && (\n <Button\n pressed={items.code.isActive}\n disabled={!items.code.canExec}\n onClick={items.code.command}\n tooltip=\"Code\"\n >\n <div className=\"i-lucide-code size-5 block\" />\n </Button>\n )}\n {items.codeBlock && (\n <Button\n pressed={items.codeBlock.isActive}\n disabled={!items.codeBlock.canExec}\n onClick={items.codeBlock.command}\n tooltip=\"Code Block\"\n >\n <div className=\"i-lucide-square-code size-5 block\" />\n </Button>\n )}\n {items.heading1 && (\n <Button\n pressed={items.heading1.isActive}\n disabled={!items.heading1.canExec}\n onClick={items.heading1.command}\n tooltip=\"Heading 1\"\n >\n <div className=\"i-lucide-heading-1 size-5 block\" />\n </Button>\n )}\n {items.heading2 && (\n <Button\n pressed={items.heading2.isActive}\n disabled={!items.heading2.canExec}\n onClick={items.heading2.command}\n tooltip=\"Heading 2\"\n >\n <div className=\"i-lucide-heading-2 size-5 block\" />\n </Button>\n )}\n {items.heading3 && (\n <Button\n pressed={items.heading3.isActive}\n disabled={!items.heading3.canExec}\n onClick={items.heading3.command}\n tooltip=\"Heading 3\"\n >\n <div className=\"i-lucide-heading-3 size-5 block\" />\n </Button>\n )}\n {items.horizontalRule && (\n <Button\n pressed={items.horizontalRule.isActive}\n disabled={!items.horizontalRule.canExec}\n onClick={items.horizontalRule.command}\n tooltip=\"Divider\"\n >\n <div className=\"i-lucide-minus size-5 block\" />\n </Button>\n )}\n {items.blockquote && (\n <Button\n pressed={items.blockquote.isActive}\n disabled={!items.blockquote.canExec}\n onClick={items.blockquote.command}\n tooltip=\"Blockquote\"\n >\n <div className=\"i-lucide-text-quote size-5 block\" />\n </Button>\n )}\n {items.bulletList && (\n <Button\n pressed={items.bulletList.isActive}\n disabled={!items.bulletList.canExec}\n onClick={items.bulletList.command}\n tooltip=\"Bullet List\"\n >\n <div className=\"i-lucide-list size-5 block\" />\n </Button>\n )}\n {items.orderedList && (\n <Button\n pressed={items.orderedList.isActive}\n disabled={!items.orderedList.canExec}\n onClick={items.orderedList.command}\n tooltip=\"Ordered List\"\n >\n <div className=\"i-lucide-list-ordered size-5 block\" />\n </Button>\n )}\n {items.taskList && (\n <Button\n pressed={items.taskList.isActive}\n disabled={!items.taskList.canExec}\n onClick={items.taskList.command}\n tooltip=\"Task List\"\n >\n <div className=\"i-lucide-list-checks size-5 block\" />\n </Button>\n )}\n {items.toggleList && (\n <Button\n pressed={items.toggleList.isActive}\n disabled={!items.toggleList.canExec}\n onClick={items.toggleList.command}\n tooltip=\"Toggle List\"\n >\n <div className=\"i-lucide-list-collapse size-5 block\" />\n </Button>\n )}\n {items.indentList && (\n <Button\n pressed={items.indentList.isActive}\n disabled={!items.indentList.canExec}\n onClick={items.indentList.command}\n tooltip=\"Increase indentation\"\n >\n <div className=\"i-lucide-indent-increase size-5 block\" />\n </Button>\n )}\n {items.dedentList && (\n <Button\n pressed={items.dedentList.isActive}\n disabled={!items.dedentList.canExec}\n onClick={items.dedentList.command}\n tooltip=\"Decrease indentation\"\n >\n <div className=\"i-lucide-indent-decrease size-5 block\" />\n </Button>\n )}\n {props.uploader && items.insertImage && (\n <ImageUploadPopover\n uploader={props.uploader}\n disabled={!items.insertImage.canExec}\n tooltip=\"Insert Image\"\n >\n <div className=\"i-lucide-image size-5 block\" />\n </ImageUploadPopover>\n )}\n </div>\n )\n}\n"
26
+ "content": "'use client'\n\nimport type { BasicExtension } from 'prosekit/basic'\nimport type { Editor } from 'prosekit/core'\nimport type { Uploader } from 'prosekit/extensions/file'\nimport { useEditorDerivedValue } from 'prosekit/react'\n\nimport { Button } from '../button'\nimport { ImageUploadPopover } from '../image-upload-popover'\n\nfunction getToolbarItems(editor: Editor<BasicExtension>) {\n return {\n undo: editor.commands.undo\n ? {\n isActive: false,\n canExec: editor.commands.undo.canExec(),\n command: () => editor.commands.undo(),\n }\n : undefined,\n redo: editor.commands.redo\n ? {\n isActive: false,\n canExec: editor.commands.redo.canExec(),\n command: () => editor.commands.redo(),\n }\n : undefined,\n bold: editor.commands.toggleBold\n ? {\n isActive: editor.marks.bold.isActive(),\n canExec: editor.commands.toggleBold.canExec(),\n command: () => editor.commands.toggleBold(),\n }\n : undefined,\n italic: editor.commands.toggleItalic\n ? {\n isActive: editor.marks.italic.isActive(),\n canExec: editor.commands.toggleItalic.canExec(),\n command: () => editor.commands.toggleItalic(),\n }\n : undefined,\n underline: editor.commands.toggleUnderline\n ? {\n isActive: editor.marks.underline.isActive(),\n canExec: editor.commands.toggleUnderline.canExec(),\n command: () => editor.commands.toggleUnderline(),\n }\n : undefined,\n strike: editor.commands.toggleStrike\n ? {\n isActive: editor.marks.strike.isActive(),\n canExec: editor.commands.toggleStrike.canExec(),\n command: () => editor.commands.toggleStrike(),\n }\n : undefined,\n code: editor.commands.toggleCode\n ? {\n isActive: editor.marks.code.isActive(),\n canExec: editor.commands.toggleCode.canExec(),\n command: () => editor.commands.toggleCode(),\n }\n : undefined,\n codeBlock: editor.commands.insertCodeBlock\n ? {\n isActive: editor.nodes.codeBlock.isActive(),\n canExec: editor.commands.insertCodeBlock.canExec({ language: 'javascript' }),\n command: () => editor.commands.insertCodeBlock({ language: 'javascript' }),\n }\n : undefined,\n heading1: editor.commands.toggleHeading\n ? {\n isActive: editor.nodes.heading.isActive({ level: 1 }),\n canExec: editor.commands.toggleHeading.canExec({ level: 1 }),\n command: () => editor.commands.toggleHeading({ level: 1 }),\n }\n : undefined,\n heading2: editor.commands.toggleHeading\n ? {\n isActive: editor.nodes.heading.isActive({ level: 2 }),\n canExec: editor.commands.toggleHeading.canExec({ level: 2 }),\n command: () => editor.commands.toggleHeading({ level: 2 }),\n }\n : undefined,\n heading3: editor.commands.toggleHeading\n ? {\n isActive: editor.nodes.heading.isActive({ level: 3 }),\n canExec: editor.commands.toggleHeading.canExec({ level: 3 }),\n command: () => editor.commands.toggleHeading({ level: 3 }),\n }\n : undefined,\n horizontalRule: editor.commands.insertHorizontalRule\n ? {\n isActive: editor.nodes.horizontalRule.isActive(),\n canExec: editor.commands.insertHorizontalRule.canExec(),\n command: () => editor.commands.insertHorizontalRule(),\n }\n : undefined,\n blockquote: editor.commands.toggleBlockquote\n ? {\n isActive: editor.nodes.blockquote.isActive(),\n canExec: editor.commands.toggleBlockquote.canExec(),\n command: () => editor.commands.toggleBlockquote(),\n }\n : undefined,\n bulletList: editor.commands.toggleList\n ? {\n isActive: editor.nodes.list.isActive({ kind: 'bullet' }),\n canExec: editor.commands.toggleList.canExec({ kind: 'bullet' }),\n command: () => editor.commands.toggleList({ kind: 'bullet' }),\n }\n : undefined,\n orderedList: editor.commands.toggleList\n ? {\n isActive: editor.nodes.list.isActive({ kind: 'ordered' }),\n canExec: editor.commands.toggleList.canExec({ kind: 'ordered' }),\n command: () => editor.commands.toggleList({ kind: 'ordered' }),\n }\n : undefined,\n taskList: editor.commands.toggleList\n ? {\n isActive: editor.nodes.list.isActive({ kind: 'task' }),\n canExec: editor.commands.toggleList.canExec({ kind: 'task' }),\n command: () => editor.commands.toggleList({ kind: 'task' }),\n }\n : undefined,\n toggleList: editor.commands.toggleList\n ? {\n isActive: editor.nodes.list.isActive({ kind: 'toggle' }),\n canExec: editor.commands.toggleList.canExec({ kind: 'toggle' }),\n command: () => editor.commands.toggleList({ kind: 'toggle' }),\n }\n : undefined,\n indentList: editor.commands.indentList\n ? {\n isActive: false,\n canExec: editor.commands.indentList.canExec(),\n command: () => editor.commands.indentList(),\n }\n : undefined,\n dedentList: editor.commands.dedentList\n ? {\n isActive: false,\n canExec: editor.commands.dedentList.canExec(),\n command: () => editor.commands.dedentList(),\n }\n : undefined,\n insertImage: editor.commands.insertImage\n ? {\n isActive: false,\n canExec: editor.commands.insertImage.canExec(),\n }\n : undefined,\n }\n}\n\nexport default function Toolbar(props: { uploader?: Uploader<string> }) {\n const items = useEditorDerivedValue(getToolbarItems)\n\n return (\n <div className=\"z-2 box-border border-gray-200 dark:border-gray-800 border-solid border-l-0 border-r-0 border-t-0 border-b flex flex-wrap gap-1 p-2 items-center\">\n {items.undo && (\n <Button\n pressed={items.undo.isActive}\n disabled={!items.undo.canExec}\n onClick={items.undo.command}\n tooltip=\"Undo\"\n >\n <div className=\"i-lucide-undo-2 size-5 block\" />\n </Button>\n )}\n {items.redo && (\n <Button\n pressed={items.redo.isActive}\n disabled={!items.redo.canExec}\n onClick={items.redo.command}\n tooltip=\"Redo\"\n >\n <div className=\"i-lucide-redo-2 size-5 block\" />\n </Button>\n )}\n\n {items.bold && (\n <Button\n pressed={items.bold.isActive}\n disabled={!items.bold.canExec}\n onClick={items.bold.command}\n tooltip=\"Bold\"\n >\n <div className=\"i-lucide-bold size-5 block\" />\n </Button>\n )}\n {items.italic && (\n <Button\n pressed={items.italic.isActive}\n disabled={!items.italic.canExec}\n onClick={items.italic.command}\n tooltip=\"Italic\"\n >\n <div className=\"i-lucide-italic size-5 block\" />\n </Button>\n )}\n {items.underline && (\n <Button\n pressed={items.underline.isActive}\n disabled={!items.underline.canExec}\n onClick={items.underline.command}\n tooltip=\"Underline\"\n >\n <div className=\"i-lucide-underline size-5 block\" />\n </Button>\n )}\n {items.strike && (\n <Button\n pressed={items.strike.isActive}\n disabled={!items.strike.canExec}\n onClick={items.strike.command}\n tooltip=\"Strike\"\n >\n <div className=\"i-lucide-strikethrough size-5 block\" />\n </Button>\n )}\n {items.code && (\n <Button\n pressed={items.code.isActive}\n disabled={!items.code.canExec}\n onClick={items.code.command}\n tooltip=\"Code\"\n >\n <div className=\"i-lucide-code size-5 block\" />\n </Button>\n )}\n {items.codeBlock && (\n <Button\n pressed={items.codeBlock.isActive}\n disabled={!items.codeBlock.canExec}\n onClick={items.codeBlock.command}\n tooltip=\"Code Block\"\n >\n <div className=\"i-lucide-square-code size-5 block\" />\n </Button>\n )}\n {items.heading1 && (\n <Button\n pressed={items.heading1.isActive}\n disabled={!items.heading1.canExec}\n onClick={items.heading1.command}\n tooltip=\"Heading 1\"\n >\n <div className=\"i-lucide-heading-1 size-5 block\" />\n </Button>\n )}\n {items.heading2 && (\n <Button\n pressed={items.heading2.isActive}\n disabled={!items.heading2.canExec}\n onClick={items.heading2.command}\n tooltip=\"Heading 2\"\n >\n <div className=\"i-lucide-heading-2 size-5 block\" />\n </Button>\n )}\n {items.heading3 && (\n <Button\n pressed={items.heading3.isActive}\n disabled={!items.heading3.canExec}\n onClick={items.heading3.command}\n tooltip=\"Heading 3\"\n >\n <div className=\"i-lucide-heading-3 size-5 block\" />\n </Button>\n )}\n {items.horizontalRule && (\n <Button\n pressed={items.horizontalRule.isActive}\n disabled={!items.horizontalRule.canExec}\n onClick={items.horizontalRule.command}\n tooltip=\"Divider\"\n >\n <div className=\"i-lucide-minus size-5 block\" />\n </Button>\n )}\n {items.blockquote && (\n <Button\n pressed={items.blockquote.isActive}\n disabled={!items.blockquote.canExec}\n onClick={items.blockquote.command}\n tooltip=\"Blockquote\"\n >\n <div className=\"i-lucide-text-quote size-5 block\" />\n </Button>\n )}\n {items.bulletList && (\n <Button\n pressed={items.bulletList.isActive}\n disabled={!items.bulletList.canExec}\n onClick={items.bulletList.command}\n tooltip=\"Bullet List\"\n >\n <div className=\"i-lucide-list size-5 block\" />\n </Button>\n )}\n {items.orderedList && (\n <Button\n pressed={items.orderedList.isActive}\n disabled={!items.orderedList.canExec}\n onClick={items.orderedList.command}\n tooltip=\"Ordered List\"\n >\n <div className=\"i-lucide-list-ordered size-5 block\" />\n </Button>\n )}\n {items.taskList && (\n <Button\n pressed={items.taskList.isActive}\n disabled={!items.taskList.canExec}\n onClick={items.taskList.command}\n tooltip=\"Task List\"\n >\n <div className=\"i-lucide-list-checks size-5 block\" />\n </Button>\n )}\n {items.toggleList && (\n <Button\n pressed={items.toggleList.isActive}\n disabled={!items.toggleList.canExec}\n onClick={items.toggleList.command}\n tooltip=\"Toggle List\"\n >\n <div className=\"i-lucide-list-collapse size-5 block\" />\n </Button>\n )}\n {items.indentList && (\n <Button\n pressed={items.indentList.isActive}\n disabled={!items.indentList.canExec}\n onClick={items.indentList.command}\n tooltip=\"Increase indentation\"\n >\n <div className=\"i-lucide-indent-increase size-5 block\" />\n </Button>\n )}\n {items.dedentList && (\n <Button\n pressed={items.dedentList.isActive}\n disabled={!items.dedentList.canExec}\n onClick={items.dedentList.command}\n tooltip=\"Decrease indentation\"\n >\n <div className=\"i-lucide-indent-decrease size-5 block\" />\n </Button>\n )}\n {props.uploader && items.insertImage && (\n <ImageUploadPopover\n uploader={props.uploader}\n disabled={!items.insertImage.canExec}\n tooltip=\"Insert Image\"\n >\n <div className=\"i-lucide-image size-5 block\" />\n </ImageUploadPopover>\n )}\n </div>\n )\n}\n"
27
27
  }
28
28
  ],
29
29
  "meta": {
@@ -18,7 +18,7 @@
18
18
  "path": "registry/src/react/ui/user-menu/user-menu.tsx",
19
19
  "type": "registry:component",
20
20
  "target": "components/editor/ui/user-menu/user-menu.tsx",
21
- "content": "'use client'\nimport type { BasicExtension } from 'prosekit/basic'\nimport { canUseRegexLookbehind, type Union } from 'prosekit/core'\nimport type { MentionExtension } from 'prosekit/extensions/mention'\nimport { useEditor } from 'prosekit/react'\nimport {\n AutocompleteEmpty,\n AutocompleteItem,\n AutocompletePopup,\n AutocompletePositioner,\n AutocompleteRoot,\n} from 'prosekit/react/autocomplete'\n\n// Match inputs like \"@\", \"@foo\", \"@foo bar\" etc. Do not match \"@ foo\".\nconst regex = canUseRegexLookbehind() ? /(?<!\\S)@(\\S.*)?$/u : /@(\\S.*)?$/u\n\nexport default function UserMenu(props: {\n users: { id: number; name: string }[]\n loading?: boolean\n onQueryChange?: (query: string) => void\n onOpenChange?: (open: boolean) => void\n}) {\n const editor = useEditor<Union<[MentionExtension, BasicExtension]>>()\n\n const handleUserInsert = (id: number, username: string) => {\n editor.commands.insertMention({\n id: id.toString(),\n value: '@' + username,\n kind: 'user',\n })\n editor.commands.insertText({ text: ' ' })\n }\n\n return (\n <AutocompleteRoot\n regex={regex}\n onQueryChange={(event) => props.onQueryChange?.(event.detail)}\n onOpenChange={(event) => props.onOpenChange?.(event.detail)}\n >\n <AutocompletePositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <AutocompletePopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col relative max-h-100 min-w-60 select-none overflow-auto whitespace-nowrap p-1\">\n <AutocompleteEmpty className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n {props.loading ? 'Loading...' : 'No results'}\n </AutocompleteEmpty>\n\n {props.users.map((user) => (\n <AutocompleteItem\n key={user.id}\n className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={() => handleUserInsert(user.id, user.name)}\n >\n <span className={props.loading ? 'opacity-50' : undefined}>\n {user.name}\n </span>\n </AutocompleteItem>\n ))}\n </AutocompletePopup>\n </AutocompletePositioner>\n </AutocompleteRoot>\n )\n}\n"
21
+ "content": "'use client'\n\nimport type { BasicExtension } from 'prosekit/basic'\nimport { canUseRegexLookbehind, type Union } from 'prosekit/core'\nimport type { MentionExtension } from 'prosekit/extensions/mention'\nimport { useEditor } from 'prosekit/react'\nimport {\n AutocompleteEmpty,\n AutocompleteItem,\n AutocompletePopup,\n AutocompletePositioner,\n AutocompleteRoot,\n} from 'prosekit/react/autocomplete'\n\n// Match inputs like \"@\", \"@foo\", \"@foo bar\" etc. Do not match \"@ foo\".\nconst regex = canUseRegexLookbehind() ? /(?<!\\S)@(\\S.*)?$/u : /@(\\S.*)?$/u\n\nexport default function UserMenu(props: {\n users: { id: number; name: string }[]\n loading?: boolean\n onQueryChange?: (query: string) => void\n onOpenChange?: (open: boolean) => void\n}) {\n const editor = useEditor<Union<[MentionExtension, BasicExtension]>>()\n\n const handleUserInsert = (id: number, username: string) => {\n editor.commands.insertMention({\n id: id.toString(),\n value: '@' + username,\n kind: 'user',\n })\n editor.commands.insertText({ text: ' ' })\n }\n\n return (\n <AutocompleteRoot\n regex={regex}\n onQueryChange={(event) => props.onQueryChange?.(event.detail)}\n onOpenChange={(event) => props.onOpenChange?.(event.detail)}\n >\n <AutocompletePositioner className=\"block overflow-visible bg-transparent w-min h-min z-50 motion-safe:ease-out motion-safe:transition-transform motion-safe:duration-100\">\n <AutocompletePopup className=\"box-border data-[state=closed]:motion-safe:duration-150 motion-safe:transition-discrete motion-safe:transition-all data-[state=closed]:opacity-0 starting:opacity-0 opacity-100 data-[state=closed]:scale-95 starting:scale-95 scale-100 motion-safe:duration-40 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-lg flex flex-col relative max-h-100 min-w-60 select-none overflow-auto whitespace-nowrap p-1\">\n <AutocompleteEmpty className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\">\n {props.loading ? 'Loading...' : 'No results'}\n </AutocompleteEmpty>\n\n {props.users.map((user) => (\n <AutocompleteItem\n key={user.id}\n className=\"relative flex items-center justify-between min-w-32 scroll-my-1 rounded-sm px-3 py-1.5 box-border cursor-default select-none whitespace-nowrap outline-hidden data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-800\"\n onSelect={() => handleUserInsert(user.id, user.name)}\n >\n <span className={props.loading ? 'opacity-50' : undefined}>\n {user.name}\n </span>\n </AutocompleteItem>\n ))}\n </AutocompletePopup>\n </AutocompletePositioner>\n </AutocompleteRoot>\n )\n}\n"
22
22
  }
23
23
  ],
24
24
  "meta": {
@@ -18,7 +18,7 @@
18
18
  "path": "registry/src/react/ui/word-counter/word-counter.tsx",
19
19
  "type": "registry:component",
20
20
  "target": "components/editor/ui/word-counter/word-counter.tsx",
21
- "content": "'use client'\nimport type { Editor } from 'prosekit/core'\nimport { useEditorDerivedValue } from 'prosekit/react'\n\nfunction getWordCount(editor: Editor) {\n const doc = editor.state.doc\n const words = doc ? doc.textBetween(0, doc.content.size, ' ') : ''\n const wordCount = words.split(/\\s+/).filter((s) => s).length\n const characterCount = doc ? doc.textContent.length : 0\n return { wordCount, characterCount }\n}\n\nexport default function WordCounter() {\n const { wordCount, characterCount } = useEditorDerivedValue(getWordCount)\n\n return (\n <div className=\"p-4 text-center italic tabular-nums\">\n Word Count: {wordCount}\n <br />\n Character Count: {characterCount}\n </div>\n )\n}\n"
21
+ "content": "'use client'\n\nimport type { Editor } from 'prosekit/core'\nimport { useEditorDerivedValue } from 'prosekit/react'\n\nfunction getWordCount(editor: Editor) {\n const doc = editor.state.doc\n const words = doc ? doc.textBetween(0, doc.content.size, ' ') : ''\n const wordCount = words.split(/\\s+/).filter((s) => s).length\n const characterCount = doc ? doc.textContent.length : 0\n return { wordCount, characterCount }\n}\n\nexport default function WordCounter() {\n const { wordCount, characterCount } = useEditorDerivedValue(getWordCount)\n\n return (\n <div className=\"p-4 text-center italic tabular-nums\">\n Word Count: {wordCount}\n <br />\n Character Count: {characterCount}\n </div>\n )\n}\n"
22
22
  }
23
23
  ],
24
24
  "meta": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "prosekit-registry",
3
3
  "type": "module",
4
- "version": "0.0.9",
4
+ "version": "0.0.12",
5
5
  "private": false,
6
6
  "description": "The registry of ProseKit examples",
7
7
  "license": "MIT",
package/dist/package.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "name": "prosekit-registry",
3
- "version": "0.0.2",
4
- "private": false,
5
- "license": "MIT",
6
- "repository": "github:prosekit/prosekit"
7
- }