prosekit-registry 0.0.7 → 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 (66) hide show
  1. package/dist/r/react-example-block-handle.json +1 -1
  2. package/dist/r/react-example-blockquote.json +1 -1
  3. package/dist/r/react-example-bold.json +1 -1
  4. package/dist/r/react-example-change-tracking.json +3 -3
  5. package/dist/r/react-example-code-block-themes.json +3 -3
  6. package/dist/r/react-example-code-block.json +1 -1
  7. package/dist/r/react-example-code.json +1 -1
  8. package/dist/r/react-example-drop-cursor.json +1 -1
  9. package/dist/r/react-example-emoji-rules.json +1 -1
  10. package/dist/r/react-example-full.json +1 -1
  11. package/dist/r/react-example-gap-cursor.json +1 -1
  12. package/dist/r/react-example-hard-break.json +2 -2
  13. package/dist/r/react-example-heading.json +1 -1
  14. package/dist/r/react-example-horizontal-rule.json +1 -1
  15. package/dist/r/react-example-image-view.json +1 -1
  16. package/dist/r/react-example-inline-menu.json +1 -1
  17. package/dist/r/react-example-italic.json +1 -1
  18. package/dist/r/react-example-keymap.json +2 -2
  19. package/dist/r/react-example-link-mark-view.json +2 -2
  20. package/dist/r/react-example-link.json +1 -1
  21. package/dist/r/react-example-list-custom-checkbox.json +1 -1
  22. package/dist/r/react-example-list.json +1 -1
  23. package/dist/r/react-example-loro.json +2 -2
  24. package/dist/r/react-example-mark-rule.json +1 -1
  25. package/dist/r/react-example-minimal.json +1 -1
  26. package/dist/r/react-example-notion.json +11 -11
  27. package/dist/r/react-example-page.json +2 -2
  28. package/dist/r/react-example-placeholder.json +1 -1
  29. package/dist/r/react-example-readonly.json +2 -2
  30. package/dist/r/react-example-rtl.json +1 -1
  31. package/dist/r/react-example-save-html.json +1 -1
  32. package/dist/r/react-example-save-json.json +1 -1
  33. package/dist/r/react-example-save-markdown.json +1 -1
  34. package/dist/r/react-example-search.json +1 -1
  35. package/dist/r/react-example-slash-menu.json +1 -1
  36. package/dist/r/react-example-strike.json +2 -2
  37. package/dist/r/react-example-table.json +1 -1
  38. package/dist/r/react-example-temml.json +1 -1
  39. package/dist/r/react-example-text-align.json +2 -2
  40. package/dist/r/react-example-text-color.json +2 -2
  41. package/dist/r/react-example-toolbar.json +1 -1
  42. package/dist/r/react-example-tweet.json +3 -3
  43. package/dist/r/react-example-typography.json +1 -1
  44. package/dist/r/react-example-underline.json +1 -1
  45. package/dist/r/react-example-unmount.json +3 -3
  46. package/dist/r/react-example-user-menu-dynamic.json +2 -2
  47. package/dist/r/react-example-user-menu.json +1 -1
  48. package/dist/r/react-example-view-adapter.json +2 -2
  49. package/dist/r/react-example-word-counter.json +1 -1
  50. package/dist/r/react-example-yjs.json +2 -2
  51. package/dist/r/react-ui-block-handle.json +2 -2
  52. package/dist/r/react-ui-button.json +2 -2
  53. package/dist/r/react-ui-code-block-view.json +2 -2
  54. package/dist/r/react-ui-drop-indicator.json +2 -2
  55. package/dist/r/react-ui-image-upload-popover.json +2 -2
  56. package/dist/r/react-ui-image-view.json +2 -2
  57. package/dist/r/react-ui-inline-menu.json +2 -2
  58. package/dist/r/react-ui-search.json +2 -2
  59. package/dist/r/react-ui-slash-menu.json +4 -4
  60. package/dist/r/react-ui-table-handle.json +2 -2
  61. package/dist/r/react-ui-tag-menu.json +2 -2
  62. package/dist/r/react-ui-toolbar.json +2 -2
  63. package/dist/r/react-ui-user-menu.json +2 -2
  64. package/dist/r/react-ui-word-counter.json +2 -2
  65. package/package.json +1 -1
  66. package/dist/package.json +0 -7
@@ -29,7 +29,7 @@
29
29
  "path": "registry/src/react/examples/block-handle/index.ts",
30
30
  "type": "registry:component",
31
31
  "target": "components/editor/examples/block-handle/index.ts",
32
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
32
+ "content": "export { default as ExampleEditor } from './editor'\n"
33
33
  }
34
34
  ],
35
35
  "meta": {
@@ -26,7 +26,7 @@
26
26
  "path": "registry/src/react/examples/blockquote/index.ts",
27
27
  "type": "registry:component",
28
28
  "target": "components/editor/examples/blockquote/index.ts",
29
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
29
+ "content": "export { default as ExampleEditor } from './editor'\n"
30
30
  }
31
31
  ],
32
32
  "meta": {
@@ -27,7 +27,7 @@
27
27
  "path": "registry/src/react/examples/bold/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/bold/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  }
32
32
  ],
33
33
  "meta": {
@@ -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": "import { 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": "import '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",
@@ -30,7 +30,7 @@
30
30
  "path": "registry/src/react/examples/change-tracking/index.ts",
31
31
  "type": "registry:component",
32
32
  "target": "components/editor/examples/change-tracking/index.ts",
33
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
33
+ "content": "export { default as ExampleEditor } from './editor'\n"
34
34
  }
35
35
  ],
36
36
  "meta": {
@@ -27,19 +27,19 @@
27
27
  "path": "registry/src/react/examples/code-block-themes/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/code-block-themes/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  },
32
32
  {
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": "import { 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": "import { 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": {
@@ -28,7 +28,7 @@
28
28
  "path": "registry/src/react/examples/code-block/index.ts",
29
29
  "type": "registry:component",
30
30
  "target": "components/editor/examples/code-block/index.ts",
31
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
31
+ "content": "export { default as ExampleEditor } from './editor'\n"
32
32
  }
33
33
  ],
34
34
  "meta": {
@@ -27,7 +27,7 @@
27
27
  "path": "registry/src/react/examples/code/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/code/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  }
32
32
  ],
33
33
  "meta": {
@@ -26,7 +26,7 @@
26
26
  "path": "registry/src/react/examples/drop-cursor/index.ts",
27
27
  "type": "registry:component",
28
28
  "target": "components/editor/examples/drop-cursor/index.ts",
29
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
29
+ "content": "export { default as ExampleEditor } from './editor'\n"
30
30
  }
31
31
  ],
32
32
  "meta": {
@@ -30,7 +30,7 @@
30
30
  "path": "registry/src/react/examples/emoji-rules/index.ts",
31
31
  "type": "registry:component",
32
32
  "target": "components/editor/examples/emoji-rules/index.ts",
33
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
33
+ "content": "export { default as ExampleEditor } from './editor'\n"
34
34
  }
35
35
  ],
36
36
  "meta": {
@@ -46,7 +46,7 @@
46
46
  "path": "registry/src/react/examples/full/index.ts",
47
47
  "type": "registry:component",
48
48
  "target": "components/editor/examples/full/index.ts",
49
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\nexport { renderHTML } from './html'\n"
49
+ "content": "export { default as ExampleEditor } from './editor'\nexport { renderHTML } from './html'\n"
50
50
  }
51
51
  ],
52
52
  "meta": {
@@ -26,7 +26,7 @@
26
26
  "path": "registry/src/react/examples/gap-cursor/index.ts",
27
27
  "type": "registry:component",
28
28
  "target": "components/editor/examples/gap-cursor/index.ts",
29
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
29
+ "content": "export { default as ExampleEditor } from './editor'\n"
30
30
  }
31
31
  ],
32
32
  "meta": {
@@ -27,13 +27,13 @@
27
27
  "path": "registry/src/react/examples/hard-break/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/hard-break/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  },
32
32
  {
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": "import { 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": {
@@ -27,7 +27,7 @@
27
27
  "path": "registry/src/react/examples/heading/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/heading/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  }
32
32
  ],
33
33
  "meta": {
@@ -26,7 +26,7 @@
26
26
  "path": "registry/src/react/examples/horizontal-rule/index.ts",
27
27
  "type": "registry:component",
28
28
  "target": "components/editor/examples/horizontal-rule/index.ts",
29
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
29
+ "content": "export { default as ExampleEditor } from './editor'\n"
30
30
  }
31
31
  ],
32
32
  "meta": {
@@ -28,7 +28,7 @@
28
28
  "path": "registry/src/react/examples/image-view/index.ts",
29
29
  "type": "registry:component",
30
30
  "target": "components/editor/examples/image-view/index.ts",
31
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
31
+ "content": "export { default as ExampleEditor } from './editor'\n"
32
32
  }
33
33
  ],
34
34
  "meta": {
@@ -27,7 +27,7 @@
27
27
  "path": "registry/src/react/examples/inline-menu/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/inline-menu/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  }
32
32
  ],
33
33
  "meta": {
@@ -27,7 +27,7 @@
27
27
  "path": "registry/src/react/examples/italic/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/italic/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  }
32
32
  ],
33
33
  "meta": {
@@ -26,13 +26,13 @@
26
26
  "path": "registry/src/react/examples/keymap/index.ts",
27
27
  "type": "registry:component",
28
28
  "target": "components/editor/examples/keymap/index.ts",
29
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
29
+ "content": "export { default as ExampleEditor } from './editor'\n"
30
30
  },
31
31
  {
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": "import { 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",
@@ -26,13 +26,13 @@
26
26
  "path": "registry/src/react/examples/link-mark-view/index.ts",
27
27
  "type": "registry:component",
28
28
  "target": "components/editor/examples/link-mark-view/index.ts",
29
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
29
+ "content": "export { default as ExampleEditor } from './editor'\n"
30
30
  },
31
31
  {
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": "import 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": {
@@ -27,7 +27,7 @@
27
27
  "path": "registry/src/react/examples/link/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/link/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  }
32
32
  ],
33
33
  "meta": {
@@ -33,7 +33,7 @@
33
33
  "path": "registry/src/react/examples/list-custom-checkbox/index.ts",
34
34
  "type": "registry:component",
35
35
  "target": "components/editor/examples/list-custom-checkbox/index.ts",
36
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
36
+ "content": "export { default as ExampleEditor } from './editor'\n"
37
37
  }
38
38
  ],
39
39
  "meta": {
@@ -27,7 +27,7 @@
27
27
  "path": "registry/src/react/examples/list/index.ts",
28
28
  "type": "registry:component",
29
29
  "target": "components/editor/examples/list/index.ts",
30
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
30
+ "content": "export { default as ExampleEditor } from './editor'\n"
31
31
  }
32
32
  ],
33
33
  "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": "import '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",
@@ -34,7 +34,7 @@
34
34
  "path": "registry/src/react/examples/loro/index.ts",
35
35
  "type": "registry:component",
36
36
  "target": "components/editor/examples/loro/index.ts",
37
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
37
+ "content": "export { default as ExampleEditor } from './editor'\n"
38
38
  }
39
39
  ],
40
40
  "meta": {
@@ -24,7 +24,7 @@
24
24
  "path": "registry/src/react/examples/mark-rule/index.ts",
25
25
  "type": "registry:component",
26
26
  "target": "components/editor/examples/mark-rule/index.ts",
27
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
27
+ "content": "export { default as ExampleEditor } from './editor'\n"
28
28
  },
29
29
  {
30
30
  "path": "registry/src/react/examples/mark-rule/issue-link.ts",
@@ -18,7 +18,7 @@
18
18
  "path": "registry/src/react/examples/minimal/index.ts",
19
19
  "type": "registry:component",
20
20
  "target": "components/editor/examples/minimal/index.ts",
21
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
21
+ "content": "export { default as ExampleEditor } from './editor'\n"
22
22
  }
23
23
  ],
24
24
  "meta": {
@@ -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": "import { 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": "import { 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,55 +50,55 @@
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": "import { 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": "import { 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": "import 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",
69
69
  "type": "registry:component",
70
70
  "target": "components/editor/examples/notion/image-view/index.ts",
71
- "content": "'use client'\n\nimport type { Extension } from 'prosekit/core'\nimport { defineReactNodeView, type ReactNodeViewComponent } from 'prosekit/react'\n\nimport ImageView from './image-view'\n\nexport function defineImageView(): Extension {\n return defineReactNodeView({\n name: 'image',\n component: ImageView satisfies ReactNodeViewComponent,\n })\n}\n"
71
+ "content": "import type { Extension } from 'prosekit/core'\nimport { defineReactNodeView, type ReactNodeViewComponent } from 'prosekit/react'\n\nimport ImageView from './image-view'\n\nexport function defineImageView(): Extension {\n return defineReactNodeView({\n name: 'image',\n component: ImageView satisfies ReactNodeViewComponent,\n })\n}\n"
72
72
  },
73
73
  {
74
74
  "path": "registry/src/react/examples/notion/index.ts",
75
75
  "type": "registry:component",
76
76
  "target": "components/editor/examples/notion/index.ts",
77
- "content": "'use client'\n\nexport { default as ExampleEditor } from './editor'\n"
77
+ "content": "export { default as ExampleEditor } from './editor'\n"
78
78
  },
79
79
  {
80
80
  "path": "registry/src/react/examples/notion/slash-menu/index.ts",
81
81
  "type": "registry:component",
82
82
  "target": "components/editor/examples/notion/slash-menu/index.ts",
83
- "content": "'use client'\n\nexport { default as SlashMenu } from './slash-menu'\n"
83
+ "content": "export { default as SlashMenu } from './slash-menu'\n"
84
84
  },
85
85
  {
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": "import { 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": "import { 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": "import 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",