neuphlo-editor 2.5.0 → 2.6.0

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 (171) hide show
  1. package/dist/Markdown-BAW0cb-U.d.cts +153 -0
  2. package/dist/Markdown-BAW0cb-U.d.ts +153 -0
  3. package/dist/{chunk-4L7FHW5M.js → chunk-ABL4D65Z.js} +113 -47
  4. package/dist/chunk-ABL4D65Z.js.map +1 -0
  5. package/dist/{chunk-BVFTQKY7.js → chunk-Z2DR2FK3.js} +245 -50
  6. package/dist/chunk-Z2DR2FK3.js.map +1 -0
  7. package/dist/headless/components/editor-bubble-item.d.ts +9 -0
  8. package/dist/headless/components/editor-bubble-item.js +29 -0
  9. package/dist/headless/components/editor-bubble.d.ts +9 -0
  10. package/dist/headless/components/editor-bubble.js +9 -0
  11. package/dist/headless/components/editor-command-item.d.ts +3 -0
  12. package/dist/headless/components/editor-command-item.js +27 -0
  13. package/dist/headless/components/editor-command.d.ts +3 -0
  14. package/dist/headless/components/editor-command.js +136 -0
  15. package/dist/headless/components/editor.d.ts +24 -0
  16. package/dist/headless/components/editor.js +13 -0
  17. package/dist/headless/extensions/AISuggestion/AISuggestion.d.ts +82 -0
  18. package/dist/headless/extensions/AISuggestion/AISuggestion.js +244 -0
  19. package/dist/headless/extensions/AISuggestion/index.d.ts +2 -0
  20. package/dist/headless/extensions/AISuggestion/index.js +1 -0
  21. package/dist/headless/extensions/CodeBlock/CodeBlock.d.ts +1 -0
  22. package/dist/headless/extensions/CodeBlock/CodeBlock.js +6 -0
  23. package/dist/headless/extensions/CodeBlock/index.d.ts +1 -0
  24. package/dist/headless/extensions/CodeBlock/index.js +1 -0
  25. package/dist/headless/extensions/DragHandle/DragHandle.d.ts +9 -0
  26. package/dist/headless/extensions/DragHandle/DragHandle.js +47 -0
  27. package/dist/headless/extensions/DragHandle/index.d.ts +2 -0
  28. package/dist/headless/extensions/DragHandle/index.js +1 -0
  29. package/dist/headless/extensions/Image/Image.d.ts +6 -0
  30. package/dist/headless/extensions/Image/Image.js +197 -0
  31. package/dist/headless/extensions/ImageBlock/ImageBlock.d.ts +23 -0
  32. package/dist/headless/extensions/ImageBlock/ImageBlock.js +242 -0
  33. package/dist/headless/extensions/ImageBlock/index.d.ts +2 -0
  34. package/dist/headless/extensions/ImageBlock/index.js +1 -0
  35. package/dist/headless/extensions/Link/Link.d.ts +2 -0
  36. package/dist/headless/extensions/Link/Link.js +44 -0
  37. package/dist/headless/extensions/Link/index.d.ts +1 -0
  38. package/dist/headless/extensions/Link/index.js +1 -0
  39. package/dist/headless/extensions/Markdown.d.ts +9 -0
  40. package/dist/headless/extensions/Markdown.js +138 -0
  41. package/dist/headless/extensions/MarkdownPaste.d.ts +3 -0
  42. package/dist/headless/extensions/MarkdownPaste.js +168 -0
  43. package/dist/headless/extensions/Mention/index.d.ts +4 -0
  44. package/dist/headless/extensions/Mention/index.js +3 -0
  45. package/dist/headless/extensions/Mention/mention-command.d.ts +8 -0
  46. package/dist/headless/extensions/Mention/mention-command.js +165 -0
  47. package/dist/headless/extensions/Mention/mention-list.d.ts +8 -0
  48. package/dist/headless/extensions/Mention/mention-list.js +150 -0
  49. package/dist/headless/extensions/Mention/mention-node-view.d.ts +2 -0
  50. package/dist/headless/extensions/Mention/mention-node-view.js +92 -0
  51. package/dist/headless/extensions/Mention/mention.d.ts +46 -0
  52. package/dist/headless/extensions/Mention/mention.js +218 -0
  53. package/dist/headless/extensions/Table/index.d.ts +2 -0
  54. package/dist/headless/extensions/Table/index.js +2 -0
  55. package/dist/headless/extensions/VideoBlock/VideoBlock.d.ts +17 -0
  56. package/dist/headless/extensions/VideoBlock/VideoBlock.js +73 -0
  57. package/dist/headless/extensions/VideoBlock/index.d.ts +2 -0
  58. package/dist/headless/extensions/VideoBlock/index.js +1 -0
  59. package/dist/headless/extensions/extension-kit.d.ts +29 -0
  60. package/dist/headless/extensions/extension-kit.js +132 -0
  61. package/dist/headless/extensions/index.d.ts +16 -0
  62. package/dist/headless/extensions/index.js +12 -0
  63. package/dist/headless/extensions/slash-command.d.ts +24 -0
  64. package/dist/headless/extensions/slash-command.js +99 -0
  65. package/dist/headless/index.cjs +143 -2
  66. package/dist/headless/index.cjs.map +1 -1
  67. package/dist/headless/index.d.cts +3 -2
  68. package/dist/headless/index.d.ts +3 -2
  69. package/dist/headless/index.js +5 -1
  70. package/dist/headless/utils/atoms.d.ts +13 -0
  71. package/dist/headless/utils/atoms.js +5 -0
  72. package/dist/headless/utils/store.d.ts +2 -0
  73. package/dist/headless/utils/store.js +4 -0
  74. package/dist/highlight.css +77 -1
  75. package/dist/react/Editor.d.ts +43 -0
  76. package/dist/react/Editor.js +119 -0
  77. package/dist/react/TableOfContents.d.ts +7 -0
  78. package/dist/react/TableOfContents.js +96 -0
  79. package/dist/react/index.cjs +156 -9
  80. package/dist/react/index.cjs.map +1 -1
  81. package/dist/react/index.d.cts +5 -2
  82. package/dist/react/index.d.ts +5 -2
  83. package/dist/react/index.js +14 -4
  84. package/dist/react/index.js.map +1 -1
  85. package/dist/react/menus/DragHandle/BlockActionMenu.d.ts +7 -0
  86. package/dist/react/menus/DragHandle/BlockActionMenu.js +111 -0
  87. package/dist/react/menus/DragHandle/DragHandleMenu.d.ts +7 -0
  88. package/dist/react/menus/DragHandle/DragHandleMenu.js +97 -0
  89. package/dist/react/menus/ImageBlock/ImageBlockLoading.d.ts +2 -0
  90. package/dist/react/menus/ImageBlock/ImageBlockLoading.js +6 -0
  91. package/dist/react/menus/ImageBlock/ImageBlockMenu.d.ts +8 -0
  92. package/dist/react/menus/ImageBlock/ImageBlockMenu.js +75 -0
  93. package/dist/react/menus/ImageBlock/ImageBlockView.d.ts +10 -0
  94. package/dist/react/menus/ImageBlock/ImageBlockView.js +60 -0
  95. package/dist/react/menus/ImageBlock/ImageBlockWidth.d.ts +6 -0
  96. package/dist/react/menus/ImageBlock/ImageBlockWidth.js +29 -0
  97. package/dist/react/menus/ImageBlock/ImageResizeHandle.d.ts +8 -0
  98. package/dist/react/menus/ImageBlock/ImageResizeHandle.js +37 -0
  99. package/dist/react/menus/ImageBlock/ImageUploader.d.ts +7 -0
  100. package/dist/react/menus/ImageBlock/ImageUploader.js +73 -0
  101. package/dist/react/menus/ImageBlock/index.d.ts +4 -0
  102. package/dist/react/menus/ImageBlock/index.js +4 -0
  103. package/dist/react/menus/ImageMenu.d.ts +10 -0
  104. package/dist/react/menus/ImageMenu.js +77 -0
  105. package/dist/react/menus/ImagePopover.d.ts +8 -0
  106. package/dist/react/menus/ImagePopover.js +146 -0
  107. package/dist/react/menus/LinkMenu.d.ts +2 -0
  108. package/dist/react/menus/LinkMenu.js +69 -0
  109. package/dist/react/menus/MenuList.d.ts +10 -0
  110. package/dist/react/menus/MenuList.js +94 -0
  111. package/dist/react/menus/SlashMenu.d.ts +5 -0
  112. package/dist/react/menus/SlashMenu.js +156 -0
  113. package/dist/react/menus/TableMenu.d.ts +5 -0
  114. package/dist/react/menus/TableMenu.js +462 -0
  115. package/dist/react/menus/TaskItem/TaskItemView.d.ts +12 -0
  116. package/dist/react/menus/TaskItem/TaskItemView.js +11 -0
  117. package/dist/react/menus/TextMenu.d.ts +10 -0
  118. package/dist/react/menus/TextMenu.js +139 -0
  119. package/dist/react/menus/VideoBlock/VideoBlockMenu.d.ts +7 -0
  120. package/dist/react/menus/VideoBlock/VideoBlockMenu.js +72 -0
  121. package/dist/react/menus/VideoBlock/VideoBlockView.d.ts +10 -0
  122. package/dist/react/menus/VideoBlock/VideoBlockView.js +87 -0
  123. package/dist/react/menus/VideoBlock/index.d.ts +2 -0
  124. package/dist/react/menus/VideoBlock/index.js +2 -0
  125. package/dist/react/menus/index.d.ts +15 -0
  126. package/dist/react/menus/index.js +9 -0
  127. package/package.json +3 -1
  128. package/dist/chunk-2DWEJI45.js +0 -1296
  129. package/dist/chunk-2DWEJI45.js.map +0 -1
  130. package/dist/chunk-3CLWAYRP.js +0 -1387
  131. package/dist/chunk-3CLWAYRP.js.map +0 -1
  132. package/dist/chunk-457ETWB6.js +0 -1351
  133. package/dist/chunk-457ETWB6.js.map +0 -1
  134. package/dist/chunk-4L7FHW5M.js.map +0 -1
  135. package/dist/chunk-4MV3UUZN.js +0 -1073
  136. package/dist/chunk-4MV3UUZN.js.map +0 -1
  137. package/dist/chunk-62DYB7FY.js +0 -1305
  138. package/dist/chunk-62DYB7FY.js.map +0 -1
  139. package/dist/chunk-BVFTQKY7.js.map +0 -1
  140. package/dist/chunk-DWGPGRTQ.js +0 -1302
  141. package/dist/chunk-DWGPGRTQ.js.map +0 -1
  142. package/dist/chunk-EG7NQJRA.js +0 -1324
  143. package/dist/chunk-EG7NQJRA.js.map +0 -1
  144. package/dist/chunk-FLLPFFI5.js +0 -1296
  145. package/dist/chunk-FLLPFFI5.js.map +0 -1
  146. package/dist/chunk-FVQHB6VC.js +0 -1128
  147. package/dist/chunk-FVQHB6VC.js.map +0 -1
  148. package/dist/chunk-G5E32MLB.js +0 -1352
  149. package/dist/chunk-G5E32MLB.js.map +0 -1
  150. package/dist/chunk-GXJGZHKR.js +0 -1326
  151. package/dist/chunk-GXJGZHKR.js.map +0 -1
  152. package/dist/chunk-KCPPTLGY.js +0 -1299
  153. package/dist/chunk-KCPPTLGY.js.map +0 -1
  154. package/dist/chunk-LHG2NX6C.js +0 -1123
  155. package/dist/chunk-LHG2NX6C.js.map +0 -1
  156. package/dist/chunk-MSNTVITF.js +0 -1352
  157. package/dist/chunk-MSNTVITF.js.map +0 -1
  158. package/dist/chunk-N4RDYJ5Z.js +0 -1351
  159. package/dist/chunk-N4RDYJ5Z.js.map +0 -1
  160. package/dist/chunk-OCNM37WJ.js +0 -1289
  161. package/dist/chunk-OCNM37WJ.js.map +0 -1
  162. package/dist/chunk-P3YFYEUB.js +0 -1351
  163. package/dist/chunk-P3YFYEUB.js.map +0 -1
  164. package/dist/chunk-RW6QBMJB.js +0 -1300
  165. package/dist/chunk-RW6QBMJB.js.map +0 -1
  166. package/dist/chunk-SJQQVB3M.js +0 -1353
  167. package/dist/chunk-SJQQVB3M.js.map +0 -1
  168. package/dist/chunk-SOXTEP7H.js +0 -6705
  169. package/dist/chunk-SOXTEP7H.js.map +0 -1
  170. package/dist/chunk-VPI26I4P.js +0 -1350
  171. package/dist/chunk-VPI26I4P.js.map +0 -1
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { NodeSelection } from "@tiptap/pm/state";
3
+ import { useCurrentEditor } from "@tiptap/react";
4
+ import { BubbleMenu } from "@tiptap/react/menus";
5
+ import { IconUpload, IconTrash, IconAlignLeft, IconAlignCenter, IconAlignRight, } from "@tabler/icons-react";
6
+ import { Fragment, useEffect, useState } from "react";
7
+ export function ImageMenu({ className, leadingExtras, trailingExtras, }) {
8
+ const { editor } = useCurrentEditor();
9
+ const [size, setSize] = useState(100);
10
+ const [align, setAlign] = useState("left");
11
+ useEffect(() => {
12
+ if (!editor)
13
+ return;
14
+ const update = () => {
15
+ if (!editor.isActive("imageBlock"))
16
+ return;
17
+ const attrs = editor.getAttributes("imageBlock") ?? {
18
+ width: "",
19
+ align: "left",
20
+ };
21
+ const width = attrs.width ?? "";
22
+ if (width.includes("%")) {
23
+ setSize(parseInt(width) || 100);
24
+ }
25
+ else {
26
+ setSize(100);
27
+ }
28
+ setAlign(attrs.align ?? "left");
29
+ };
30
+ update();
31
+ editor.on("selectionUpdate", update);
32
+ editor.on("transaction", update);
33
+ editor.on("update", update);
34
+ return () => {
35
+ editor.off("selectionUpdate", update);
36
+ editor.off("transaction", update);
37
+ editor.off("update", update);
38
+ };
39
+ }, [editor]);
40
+ if (!editor)
41
+ return null;
42
+ const updateImageSize = (newSize) => {
43
+ setSize(newSize);
44
+ editor.commands.setImageBlockWidth(newSize);
45
+ };
46
+ const updateImageAlign = (newAlign) => {
47
+ setAlign(newAlign);
48
+ editor.commands.setImageBlockAlign(newAlign);
49
+ };
50
+ const removeImage = () => {
51
+ editor.chain().focus().deleteSelection().run();
52
+ };
53
+ const triggerUpload = () => {
54
+ editor.commands.updateAttributes("imageBlock", { src: "", loading: false });
55
+ };
56
+ return (_jsx(BubbleMenu, { editor: editor, pluginKey: "imageBubbleMenu", shouldShow: ({ editor: e, state }) => {
57
+ // Only show when an image node is selected
58
+ if (!e.isActive("imageBlock"))
59
+ return false;
60
+ // Check if we have a node selection (clicking on image)
61
+ const { selection } = state;
62
+ const isNodeSelection = selection instanceof NodeSelection;
63
+ // Only show for node selections (when clicking directly on the image)
64
+ // This prevents showing when there's a text selection that includes an image
65
+ return isNodeSelection;
66
+ }, updateDelay: 0, children: _jsxs("div", { className: className ? `bubble-menu ${className}` : "bubble-menu", children: [leadingExtras && editor
67
+ ? leadingExtras.map((renderExtra, index) => (_jsx(Fragment, { children: renderExtra(editor) }, `image-leading-extra-${index}`)))
68
+ : null, _jsxs("div", { style: {
69
+ display: "flex",
70
+ alignItems: "center",
71
+ gap: 8,
72
+ minWidth: 140,
73
+ }, children: [_jsxs("span", { style: { fontSize: "12px", whiteSpace: "nowrap" }, children: [size, "%"] }), _jsx("input", { type: "range", min: "25", max: "100", step: "5", value: size, onChange: (e) => updateImageSize(parseInt(e.target.value)), onMouseDown: (e) => e.stopPropagation(), style: { flex: 1 } })] }), _jsx("div", { className: "nph-link-popover__divider", style: { margin: "0 4px" } }), _jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "left" ? " is-active" : ""}`, title: "Align left", onMouseDown: (e) => e.preventDefault(), onClick: () => updateImageAlign("left"), children: _jsx(IconAlignLeft, { size: 16 }) }), _jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "center" ? " is-active" : ""}`, title: "Align center", onMouseDown: (e) => e.preventDefault(), onClick: () => updateImageAlign("center"), children: _jsx(IconAlignCenter, { size: 16 }) }), _jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "right" ? " is-active" : ""}`, title: "Align right", onMouseDown: (e) => e.preventDefault(), onClick: () => updateImageAlign("right"), children: _jsx(IconAlignRight, { size: 16 }) }), _jsx("div", { className: "nph-link-popover__divider", style: { margin: "0 4px" } }), _jsx("button", { type: "button", className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", title: "Upload new image", onMouseDown: (e) => e.preventDefault(), onClick: triggerUpload, children: _jsx(IconUpload, { size: 16 }) }), _jsx("button", { type: "button", className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", title: "Remove image", onMouseDown: (e) => e.preventDefault(), onClick: removeImage, children: _jsx(IconTrash, { size: 16 }) }), trailingExtras && editor
74
+ ? trailingExtras.map((renderExtra, index) => (_jsx(Fragment, { children: renderExtra(editor) }, `image-trailing-extra-${index}`)))
75
+ : null] }) }));
76
+ }
77
+ export default ImageMenu;
@@ -0,0 +1,8 @@
1
+ export type ImagePopoverProps = {
2
+ editor: any | null;
3
+ hideWhenUnavailable?: boolean;
4
+ onSetImage?: () => void;
5
+ onOpenChange?: (isOpen: boolean) => void;
6
+ };
7
+ export declare function ImagePopover({ editor, hideWhenUnavailable, onSetImage, onOpenChange, }: ImagePopoverProps): import("react/jsx-runtime").JSX.Element | null;
8
+ export default ImagePopover;
@@ -0,0 +1,146 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { IconPhoto, IconTrash, IconAlignLeft, IconAlignCenter, IconAlignRight, } from "@tabler/icons-react";
4
+ export function ImagePopover({ editor, hideWhenUnavailable = false, onSetImage, onOpenChange, }) {
5
+ const [open, setOpen] = useState(false);
6
+ const [alt, setAlt] = useState("");
7
+ const [size, setSize] = useState(100); // Size as percentage (25-200%)
8
+ const [align, setAlign] = useState("left");
9
+ const inputRef = useRef(null);
10
+ const rootRef = useRef(null);
11
+ const isActive = !!editor?.isActive?.("image");
12
+ const currentAttrs = useMemo(() => {
13
+ if (!editor?.getAttributes)
14
+ return { alt: "", width: "", align: "left" };
15
+ return (editor.getAttributes("image") ?? {
16
+ alt: "",
17
+ width: "",
18
+ align: "left",
19
+ });
20
+ }, [editor, editor?.state?.selection?.from, editor?.state?.selection?.to]);
21
+ const canSet = useMemo(() => {
22
+ if (!editor)
23
+ return false;
24
+ try {
25
+ return editor
26
+ .can()
27
+ .chain()
28
+ .focus()
29
+ .setImage({ src: "https://example.com/image.jpg" })
30
+ .run();
31
+ }
32
+ catch (_) {
33
+ return false;
34
+ }
35
+ }, [editor]);
36
+ useEffect(() => {
37
+ if (!editor)
38
+ return;
39
+ const update = () => {
40
+ const attrs = editor.getAttributes("image") ?? {
41
+ alt: "",
42
+ width: "",
43
+ align: "left",
44
+ };
45
+ setAlt(attrs.alt ?? "");
46
+ // Parse width to extract percentage or convert px to percentage
47
+ const width = attrs.width ?? "";
48
+ if (width.includes("%")) {
49
+ setSize(parseInt(width) || 100);
50
+ }
51
+ else if (width.includes("px")) {
52
+ setSize(100); // Default to 100% if px is used
53
+ }
54
+ else {
55
+ setSize(100);
56
+ }
57
+ setAlign(attrs.align ?? "left");
58
+ };
59
+ update();
60
+ editor.on("selectionUpdate", update);
61
+ editor.on("transaction", update);
62
+ editor.on("update", update);
63
+ return () => {
64
+ editor.off("selectionUpdate", update);
65
+ editor.off("transaction", update);
66
+ editor.off("update", update);
67
+ };
68
+ }, [editor]);
69
+ useEffect(() => {
70
+ if (open)
71
+ inputRef.current?.focus();
72
+ onOpenChange?.(open);
73
+ }, [open, onOpenChange]);
74
+ if (!editor)
75
+ return null;
76
+ if (hideWhenUnavailable && !canSet && !isActive)
77
+ return null;
78
+ const updateImageSize = (newSize) => {
79
+ setSize(newSize);
80
+ if (isActive) {
81
+ ;
82
+ editor.commands.updateAttributes("image", {
83
+ width: `${newSize}%`,
84
+ });
85
+ }
86
+ };
87
+ const updateImageAlign = (newAlign) => {
88
+ setAlign(newAlign);
89
+ if (isActive) {
90
+ ;
91
+ editor.commands.updateAttributes("image", { align: newAlign });
92
+ }
93
+ };
94
+ const updateAltText = (newAlt) => {
95
+ setAlt(newAlt);
96
+ if (isActive) {
97
+ ;
98
+ editor.commands.updateAttributes("image", { alt: newAlt });
99
+ }
100
+ };
101
+ const removeImage = () => {
102
+ ;
103
+ editor.chain().focus().deleteSelection().run();
104
+ setOpen(false);
105
+ };
106
+ const triggerUpload = () => {
107
+ ;
108
+ editor.chain().focus().uploadImage().run();
109
+ setOpen(false);
110
+ };
111
+ // Close when clicking outside of the popover/button container
112
+ useEffect(() => {
113
+ const handler = (e) => {
114
+ if (!open)
115
+ return;
116
+ const el = rootRef.current;
117
+ if (!el)
118
+ return;
119
+ if (!el.contains(e.target))
120
+ setOpen(false);
121
+ };
122
+ document.addEventListener("pointerdown", handler);
123
+ return () => document.removeEventListener("pointerdown", handler);
124
+ }, [open]);
125
+ return (_jsxs("div", { ref: rootRef, className: "nph-dropdown", style: { position: "relative" }, children: [_jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => setOpen((v) => !v), className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${isActive ? " is-active" : ""}`, "aria-label": isActive ? "Edit image" : "Add image", "aria-pressed": isActive, "aria-expanded": open, title: isActive ? "Edit image" : "Add image", children: _jsx(IconPhoto, { size: 16 }) }), open ? (_jsx("div", { className: "nph-dropdown__panel", style: {
126
+ padding: 0,
127
+ minWidth: 320,
128
+ background: "transparent",
129
+ boxShadow: "none",
130
+ left: "50%",
131
+ transform: "translateX(-50%)",
132
+ }, role: "dialog", "aria-label": "Image editor", children: _jsxs("div", { className: "nph-link-popover", children: [_jsx("input", { ref: inputRef, type: "text", value: alt, onChange: (e) => updateAltText(e.target.value), placeholder: "Alt text", className: "nph-link-popover__input", onMouseDown: (e) => {
133
+ e.stopPropagation();
134
+ }, onKeyDown: (e) => {
135
+ if (e.key === "Escape") {
136
+ e.preventDefault();
137
+ setOpen(false);
138
+ }
139
+ } }), _jsx("div", { className: "nph-link-popover__divider" }), _jsxs("div", { style: {
140
+ display: "flex",
141
+ alignItems: "center",
142
+ gap: 8,
143
+ minWidth: 180,
144
+ }, children: [_jsxs("span", { style: { fontSize: "12px", whiteSpace: "nowrap" }, children: [size, "%"] }), _jsx("input", { type: "range", min: "25", max: "100", step: "5", value: size, onChange: (e) => updateImageSize(parseInt(e.target.value)), onMouseDown: (e) => e.stopPropagation(), style: { flex: 1 } })] }), _jsx("div", { className: "nph-link-popover__divider" }), _jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "left" ? " is-active" : ""}`, title: "Align left", onMouseDown: (e) => e.preventDefault(), onClick: () => updateImageAlign("left"), children: _jsx(IconAlignLeft, { size: 16 }) }), _jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "center" ? " is-active" : ""}`, title: "Align center", onMouseDown: (e) => e.preventDefault(), onClick: () => updateImageAlign("center"), children: _jsx(IconAlignCenter, { size: 16 }) }), _jsx("button", { type: "button", className: `nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon${align === "right" ? " is-active" : ""}`, title: "Align right", onMouseDown: (e) => e.preventDefault(), onClick: () => updateImageAlign("right"), children: _jsx(IconAlignRight, { size: 16 }) }), _jsx("button", { type: "button", className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", title: "Upload image", onMouseDown: (e) => e.preventDefault(), onClick: triggerUpload, children: _jsx(IconPhoto, { size: 16 }) }), _jsx("button", { type: "button", className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", title: "Remove image", onMouseDown: (e) => e.preventDefault(), onClick: removeImage, disabled: !isActive, children: _jsx(IconTrash, { size: 16 }) })] }) })) : null] }));
145
+ }
146
+ export default ImagePopover;
@@ -0,0 +1,2 @@
1
+ export declare function LinkMenu(): import("react/jsx-runtime").JSX.Element | null;
2
+ export default LinkMenu;
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCurrentEditor } from "@tiptap/react";
3
+ import { BubbleMenu } from "@tiptap/react/menus";
4
+ import { useCallback, useState, useRef, useEffect } from "react";
5
+ import { IconLink, IconExternalLink, IconTrash, IconCheck, IconX, } from "@tabler/icons-react";
6
+ export function LinkMenu() {
7
+ const { editor } = useCurrentEditor();
8
+ const [url, setUrl] = useState("");
9
+ const [isEditing, setIsEditing] = useState(false);
10
+ const inputRef = useRef(null);
11
+ const currentUrl = editor?.getAttributes("link")?.href || "";
12
+ useEffect(() => {
13
+ setUrl(currentUrl);
14
+ }, [currentUrl]);
15
+ useEffect(() => {
16
+ if (isEditing && inputRef.current) {
17
+ inputRef.current.focus();
18
+ inputRef.current.select();
19
+ }
20
+ }, [isEditing]);
21
+ const handleSetLink = useCallback(() => {
22
+ if (!editor || !url)
23
+ return;
24
+ editor
25
+ .chain()
26
+ .focus()
27
+ .extendMarkRange("link")
28
+ .setLink({ href: url })
29
+ .run();
30
+ setIsEditing(false);
31
+ }, [editor, url]);
32
+ const handleRemoveLink = useCallback(() => {
33
+ if (!editor)
34
+ return;
35
+ editor.chain().focus().unsetLink().run();
36
+ setIsEditing(false);
37
+ }, [editor]);
38
+ const handleOpenLink = useCallback(() => {
39
+ if (url || currentUrl) {
40
+ window.open(url || currentUrl, "_blank", "noopener,noreferrer");
41
+ }
42
+ }, [url, currentUrl]);
43
+ if (!editor)
44
+ return null;
45
+ return (_jsx(BubbleMenu, { editor: editor, pluginKey: "linkBubbleMenu", shouldShow: ({ editor: e, state }) => {
46
+ // Show when link is active (whether selection is empty or not)
47
+ return e.isActive("link");
48
+ }, updateDelay: 0, children: _jsx("div", { className: "bubble-menu", children: !isEditing ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => setIsEditing(true), className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", "aria-label": "Edit link", title: currentUrl, children: _jsx(IconLink, { size: 16 }) }), _jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: handleOpenLink, className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", "aria-label": "Open link", title: "Open link in new tab", children: _jsx(IconExternalLink, { size: 16 }) }), _jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: handleRemoveLink, className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", "aria-label": "Remove link", title: "Remove link", children: _jsx(IconTrash, { size: 16 }) })] })) : (_jsxs(_Fragment, { children: [_jsx("input", { ref: inputRef, type: "url", value: url, onChange: (e) => setUrl(e.target.value), onKeyDown: (e) => {
49
+ if (e.key === "Enter") {
50
+ e.preventDefault();
51
+ handleSetLink();
52
+ }
53
+ else if (e.key === "Escape") {
54
+ e.preventDefault();
55
+ setUrl(currentUrl);
56
+ setIsEditing(false);
57
+ }
58
+ }, placeholder: "https://example.com", className: "nph-link-input", style: {
59
+ padding: "4px 8px",
60
+ border: "1px solid #ccc",
61
+ borderRadius: "4px",
62
+ minWidth: "200px",
63
+ fontSize: "14px",
64
+ } }), _jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: handleSetLink, className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", "aria-label": "Apply", title: "Apply", disabled: !url, children: _jsx(IconCheck, { size: 16 }) }), _jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => {
65
+ setUrl(currentUrl);
66
+ setIsEditing(false);
67
+ }, className: "nph-btn nph-btn-ghost nph-btn-xs nph-btn-icon", "aria-label": "Cancel", title: "Cancel", children: _jsx(IconX, { size: 16 }) })] })) }) }));
68
+ }
69
+ export default LinkMenu;
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ export type MenuListProps = {
3
+ editor: any;
4
+ className?: string;
5
+ style?: React.CSSProperties;
6
+ onSelect?: () => void;
7
+ buttonClassName?: string;
8
+ };
9
+ export declare function MenuList({ editor, className, style, onSelect, buttonClassName, }: MenuListProps): import("react/jsx-runtime").JSX.Element;
10
+ export default MenuList;
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { IconTypography, IconH1, IconH2, IconH3, IconH4, IconList, IconListNumbers, IconBlockquote, IconCode, IconChevronDown, IconSourceCode, } from "@tabler/icons-react";
4
+ export function MenuList({ editor, className, style, onSelect, buttonClassName, }) {
5
+ const [open, setOpen] = useState(false);
6
+ const [label, setLabel] = useState("Paragraph");
7
+ const rootRef = useRef(null);
8
+ const computeLabel = useCallback(() => {
9
+ if (!editor)
10
+ return "Paragraph";
11
+ if (editor.isActive("heading", { level: 1 }))
12
+ return "Heading 1";
13
+ if (editor.isActive("heading", { level: 2 }))
14
+ return "Heading 2";
15
+ if (editor.isActive("heading", { level: 3 }))
16
+ return "Heading 3";
17
+ if (editor.isActive("heading", { level: 4 }))
18
+ return "Heading 4";
19
+ if (editor.isActive("bulletList"))
20
+ return "Bullet list";
21
+ if (editor.isActive("orderedList"))
22
+ return "Ordered list";
23
+ if (editor.isActive("blockquote"))
24
+ return "Quote";
25
+ if (editor.isActive("code"))
26
+ return "Code";
27
+ if (editor.isActive("codeBlock"))
28
+ return "Code Block";
29
+ return "Paragraph";
30
+ }, [editor]);
31
+ useEffect(() => {
32
+ setLabel(computeLabel());
33
+ if (!editor)
34
+ return;
35
+ const update = () => setLabel(computeLabel());
36
+ editor.on("selectionUpdate", update);
37
+ editor.on("transaction", update);
38
+ editor.on("update", update);
39
+ return () => {
40
+ editor.off("selectionUpdate", update);
41
+ editor.off("transaction", update);
42
+ editor.off("update", update);
43
+ };
44
+ }, [editor, computeLabel]);
45
+ useEffect(() => {
46
+ if (!editor)
47
+ return;
48
+ const close = () => setOpen(false);
49
+ editor.on("selectionUpdate", close);
50
+ editor.on("blur", close);
51
+ return () => {
52
+ editor.off("selectionUpdate", close);
53
+ editor.off("blur", close);
54
+ };
55
+ }, [editor]);
56
+ useEffect(() => {
57
+ const handlePointerDown = (e) => {
58
+ if (!open)
59
+ return;
60
+ const el = rootRef.current;
61
+ if (!el)
62
+ return;
63
+ if (!el.contains(e.target))
64
+ setOpen(false);
65
+ };
66
+ document.addEventListener("pointerdown", handlePointerDown);
67
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
68
+ }, [open]);
69
+ const handle = (fn) => () => {
70
+ fn();
71
+ onSelect?.();
72
+ setOpen(false);
73
+ };
74
+ const isActive = useCallback((name) => label === name, [label]);
75
+ return (_jsxs("div", { ref: rootRef, className: "nph-dropdown", children: [_jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => setOpen((v) => !v), className: buttonClassName ??
76
+ `nph-btn nph-btn-ghost nph-btn-xs${open || label !== "Paragraph" ? " is-active" : ""}`, "aria-expanded": open, "aria-label": "Change block type", children: _jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: [_jsx("span", { style: { fontSize: 12 }, children: label }), _jsx(IconChevronDown, { size: 16 })] }) }), open ? (_jsx("div", { className: "nph-dropdown__panel nph-command", onMouseDown: (e) => e.preventDefault(), children: _jsxs("div", { className: className ? className : "nph-command__list", style: { maxHeight: 240, ...(style ?? {}) }, children: [_jsxs("button", { type: "button", className: `nph-command__item${isActive("Paragraph") ? " is-active" : ""}`, "aria-selected": isActive("Paragraph"), onClick: handle(() => editor.chain().focus().setParagraph().run()), children: [_jsx(IconTypography, { size: 16 }), _jsx("span", { children: "Paragraph" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Heading 1") ? " is-active" : ""}`, "aria-selected": isActive("Heading 1"), onClick: handle(() => editor
77
+ .chain()
78
+ .focus()
79
+ .toggleHeading({ level: 1 })
80
+ .run()), children: [_jsx(IconH1, { size: 16 }), _jsx("span", { children: "Heading 1" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Heading 2") ? " is-active" : ""}`, "aria-selected": isActive("Heading 2"), onClick: handle(() => editor
81
+ .chain()
82
+ .focus()
83
+ .toggleHeading({ level: 2 })
84
+ .run()), children: [_jsx(IconH2, { size: 16 }), _jsx("span", { children: "Heading 2" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Heading 3") ? " is-active" : ""}`, "aria-selected": isActive("Heading 3"), onClick: handle(() => editor
85
+ .chain()
86
+ .focus()
87
+ .toggleHeading({ level: 3 })
88
+ .run()), children: [_jsx(IconH3, { size: 16 }), _jsx("span", { children: "Heading 3" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Heading 4") ? " is-active" : ""}`, "aria-selected": isActive("Heading 4"), onClick: handle(() => editor
89
+ .chain()
90
+ .focus()
91
+ .toggleHeading({ level: 4 })
92
+ .run()), children: [_jsx(IconH4, { size: 16 }), _jsx("span", { children: "Heading 4" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Bullet list") ? " is-active" : ""}`, "aria-selected": isActive("Bullet list"), onClick: handle(() => editor.chain().focus().toggleBulletList().run()), children: [_jsx(IconList, { size: 16 }), _jsx("span", { children: "Bullet list" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Ordered list") ? " is-active" : ""}`, "aria-selected": isActive("Ordered list"), onClick: handle(() => editor.chain().focus().toggleOrderedList().run()), children: [_jsx(IconListNumbers, { size: 16 }), _jsx("span", { children: "Ordered list" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Quote") ? " is-active" : ""}`, "aria-selected": isActive("Quote"), onClick: handle(() => editor.chain().focus().toggleBlockquote().run()), children: [_jsx(IconBlockquote, { size: 16 }), _jsx("span", { children: "Quote" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Code") ? " is-active" : ""}`, "aria-selected": isActive("Code"), onClick: handle(() => editor.chain().focus().toggleCode().run()), children: [_jsx(IconCode, { size: 16 }), _jsx("span", { children: "Code" })] }), _jsxs("button", { type: "button", className: `nph-command__item${isActive("Code Block") ? " is-active" : ""}`, "aria-selected": isActive("Code Block"), onClick: handle(() => editor.chain().focus().toggleCodeBlock().run()), children: [_jsx(IconSourceCode, { size: 16 }), _jsx("span", { children: "Code Block" })] })] }) })) : null] }));
93
+ }
94
+ export default MenuList;
@@ -0,0 +1,5 @@
1
+ export type SlashMenuProps = {
2
+ className?: string;
3
+ };
4
+ export declare function SlashMenu({ className }: SlashMenuProps): import("react/jsx-runtime").JSX.Element | null;
5
+ export default SlashMenu;
@@ -0,0 +1,156 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCurrentEditor } from "@tiptap/react";
3
+ import { useAtomValue } from "jotai";
4
+ import { EditorCommand, EditorCommandList, EditorCommandItem, } from "../../headless";
5
+ import { queryAtom } from "../../headless/utils/atoms";
6
+ import { novelStore } from "../../headless/utils/store";
7
+ import { IconTypography, IconH1, IconH2, IconH3, IconH4, IconList, IconListNumbers, IconBlockquote, IconCode, IconSourceCode, IconPhoto, IconVideo, IconMinus, IconTable, IconListCheck, } from "@tabler/icons-react";
8
+ import { useMemo } from "react";
9
+ const SLASH_COMMANDS = [
10
+ {
11
+ group: "Format",
12
+ items: [
13
+ {
14
+ value: "paragraph text",
15
+ label: "Paragraph",
16
+ description: "Plain text block",
17
+ icon: IconTypography,
18
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).setParagraph().run(),
19
+ },
20
+ {
21
+ value: "heading1 h1",
22
+ label: "Heading 1",
23
+ description: "Large section heading",
24
+ icon: IconH1,
25
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleHeading({ level: 1 }).run(),
26
+ },
27
+ {
28
+ value: "heading2 h2",
29
+ label: "Heading 2",
30
+ description: "Medium section heading",
31
+ icon: IconH2,
32
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleHeading({ level: 2 }).run(),
33
+ },
34
+ {
35
+ value: "heading3 h3",
36
+ label: "Heading 3",
37
+ description: "Small section heading",
38
+ icon: IconH3,
39
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleHeading({ level: 3 }).run(),
40
+ },
41
+ {
42
+ value: "heading4 h4",
43
+ label: "Heading 4",
44
+ description: "Subsection heading",
45
+ icon: IconH4,
46
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleHeading({ level: 4 }).run(),
47
+ },
48
+ ],
49
+ },
50
+ {
51
+ group: "Lists",
52
+ items: [
53
+ {
54
+ value: "bullet list ul",
55
+ label: "Bullet List",
56
+ description: "Unordered list of items",
57
+ icon: IconList,
58
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleBulletList().run(),
59
+ },
60
+ {
61
+ value: "ordered list ol numbered",
62
+ label: "Numbered List",
63
+ description: "Ordered list of items",
64
+ icon: IconListNumbers,
65
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleOrderedList().run(),
66
+ },
67
+ {
68
+ value: "task list todo checklist",
69
+ label: "Task List",
70
+ description: "Checklist of to-do items",
71
+ icon: IconListCheck,
72
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleTaskList().run(),
73
+ },
74
+ {
75
+ value: "quote blockquote",
76
+ label: "Blockquote",
77
+ description: "Highlight a quote or excerpt",
78
+ icon: IconBlockquote,
79
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleBlockquote().run(),
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ group: "Insert",
85
+ items: [
86
+ {
87
+ value: "image photo picture",
88
+ label: "Image",
89
+ description: "Upload or embed an image",
90
+ icon: IconPhoto,
91
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).setImageBlock({ src: "" }).run(),
92
+ },
93
+ {
94
+ value: "video embed youtube vimeo",
95
+ label: "Video",
96
+ description: "Embed a YouTube or Vimeo video",
97
+ icon: IconVideo,
98
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).setVideoBlock({ src: "" }).run(),
99
+ },
100
+ {
101
+ value: "table grid",
102
+ label: "Table",
103
+ description: "Insert a table with rows and columns",
104
+ icon: IconTable,
105
+ onCommand: ({ editor, range }) => {
106
+ editor.chain().focus().deleteRange(range).run();
107
+ if (editor.commands.insertTable) {
108
+ editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true });
109
+ }
110
+ },
111
+ },
112
+ {
113
+ value: "code block codeblock",
114
+ label: "Code Block",
115
+ description: "Syntax-highlighted code block",
116
+ icon: IconSourceCode,
117
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
118
+ },
119
+ {
120
+ value: "code inline",
121
+ label: "Inline Code",
122
+ description: "Mark text as inline code",
123
+ icon: IconCode,
124
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCode().run(),
125
+ },
126
+ {
127
+ value: "divider horizontal rule separator",
128
+ label: "Divider",
129
+ description: "Horizontal separator line",
130
+ icon: IconMinus,
131
+ onCommand: ({ editor, range }) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(),
132
+ },
133
+ ],
134
+ },
135
+ ];
136
+ export function SlashMenu({ className }) {
137
+ const { editor } = useCurrentEditor();
138
+ const query = useAtomValue(queryAtom, { store: novelStore });
139
+ // Pre-filter groups to determine if we should show empty state
140
+ const filteredGroups = useMemo(() => {
141
+ if (!query)
142
+ return SLASH_COMMANDS;
143
+ const q = query.toLowerCase();
144
+ return SLASH_COMMANDS.map((group) => ({
145
+ ...group,
146
+ items: group.items.filter((item) => item.value.toLowerCase().includes(q)),
147
+ })).filter((group) => group.items.length > 0);
148
+ }, [query]);
149
+ if (!editor)
150
+ return null;
151
+ return (_jsx(EditorCommand, { className: className ?? "nph-command", children: _jsx(EditorCommandList, { className: "nph-command__list", style: { display: "flex", flexDirection: "column", gap: 2 }, children: filteredGroups.length === 0 ? (_jsx("div", { style: { padding: "8px 12px", fontSize: 14, color: "var(--muted-foreground, #6b7280)" }, children: "No commands found" })) : (filteredGroups.map((group) => (_jsxs("div", { children: [_jsx("div", { className: "nph-command__group-header", children: group.group }), group.items.map((item) => {
152
+ const Icon = item.icon;
153
+ return (_jsxs(EditorCommandItem, { value: item.value, className: "nph-command__item", onCommand: item.onCommand, children: [_jsx("div", { className: "nph-command__item-icon", children: _jsx(Icon, { size: 18 }) }), _jsxs("div", { className: "nph-command__item-content", children: [_jsx("span", { className: "nph-command__item-title", children: item.label }), _jsx("span", { className: "nph-command__item-description", children: item.description })] })] }, item.value));
154
+ })] }, group.group)))) }) }));
155
+ }
156
+ export default SlashMenu;
@@ -0,0 +1,5 @@
1
+ export type TableMenuProps = {
2
+ className?: string;
3
+ };
4
+ export declare function TableMenu({ className: _className }: TableMenuProps): import("react").ReactPortal | null;
5
+ export default TableMenu;