@comitor/ui 3.3.4 → 3.5.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 (55) hide show
  1. package/README.md +19 -5
  2. package/dist/components/composite/form-section.js.map +1 -1
  3. package/dist/components/composite/relative-time.d.ts +13 -2
  4. package/dist/components/composite/relative-time.js +12 -1
  5. package/dist/components/composite/relative-time.js.map +1 -1
  6. package/dist/components/composite/stat-card.d.ts +3 -0
  7. package/dist/components/composite/stat-card.js +2 -1
  8. package/dist/components/composite/stat-card.js.map +1 -1
  9. package/dist/components/ui/badge.js.map +1 -1
  10. package/dist/components/ui/field.js.map +1 -1
  11. package/dist/components/ui/item.d.ts +1 -1
  12. package/dist/data-grid/use-data-grid.js +24 -10
  13. package/dist/data-grid/use-data-grid.js.map +1 -1
  14. package/dist/editor/bubble-toolbar.d.ts +34 -0
  15. package/dist/editor/bubble-toolbar.js +391 -0
  16. package/dist/editor/bubble-toolbar.js.map +1 -0
  17. package/dist/editor/image-upload.d.ts +57 -0
  18. package/dist/editor/image-upload.js +136 -0
  19. package/dist/editor/image-upload.js.map +1 -0
  20. package/dist/editor/index.d.ts +5 -0
  21. package/dist/editor/index.js +11 -0
  22. package/dist/editor/index.js.map +1 -0
  23. package/dist/editor/markdown-editor-classes.d.ts +16 -0
  24. package/dist/editor/markdown-editor-classes.js +117 -0
  25. package/dist/editor/markdown-editor-classes.js.map +1 -0
  26. package/dist/editor/markdown-editor-constants.d.ts +92 -0
  27. package/dist/editor/markdown-editor-constants.js +6 -0
  28. package/dist/editor/markdown-editor-constants.js.map +1 -0
  29. package/dist/editor/markdown-editor.d.ts +125 -0
  30. package/dist/editor/markdown-editor.js +325 -0
  31. package/dist/editor/markdown-editor.js.map +1 -0
  32. package/dist/editor/markdown-extensions.d.ts +71 -0
  33. package/dist/editor/markdown-extensions.js +172 -0
  34. package/dist/editor/markdown-extensions.js.map +1 -0
  35. package/dist/editor/markdown-syntax.d.ts +87 -0
  36. package/dist/editor/markdown-syntax.js +58 -0
  37. package/dist/editor/markdown-syntax.js.map +1 -0
  38. package/dist/editor/slash-commands.d.ts +62 -0
  39. package/dist/editor/slash-commands.js +186 -0
  40. package/dist/editor/slash-commands.js.map +1 -0
  41. package/dist/editor/suggestion-menu.d.ts +38 -0
  42. package/dist/editor/suggestion-menu.js +144 -0
  43. package/dist/editor/suggestion-menu.js.map +1 -0
  44. package/dist/editor/suggestion-store.d.ts +47 -0
  45. package/dist/editor/suggestion-store.js +49 -0
  46. package/dist/editor/suggestion-store.js.map +1 -0
  47. package/dist/i18n/locale-provider.d.ts +1 -0
  48. package/dist/i18n/messages.d.ts +2 -0
  49. package/dist/i18n/messages.js +3 -0
  50. package/dist/i18n/messages.js.map +1 -1
  51. package/dist/i18n/namespaces/markdown-editor.d.ts +6 -0
  52. package/dist/i18n/namespaces/markdown-editor.js +127 -0
  53. package/dist/i18n/namespaces/markdown-editor.js.map +1 -0
  54. package/dist/index.d.ts +1 -0
  55. package/package.json +80 -2
@@ -0,0 +1,34 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import * as React from 'react';
3
+ import { MarkdownEditorLabels } from './markdown-editor-constants.js';
4
+
5
+ /**
6
+ * Thanh định dạng NỔI — hiện khi bôi chọn chữ, như Linear: không có thanh công cụ cố định chiếm chỗ
7
+ * trên đầu tài liệu, định dạng ở ngay chỗ đang chọn.
8
+ *
9
+ * Ba trạng thái trong cùng một bong bóng:
10
+ * · đang chọn chữ → nút định dạng (dấu nội dòng + đổi khối);
11
+ * · con trỏ đứng TRONG một liên kết (không chọn gì) → địa chỉ + Mở / Sửa / Gỡ;
12
+ * · đang sửa liên kết (nút Liên kết, hoặc ⌘K) → ô nhập địa chỉ.
13
+ *
14
+ * Vị trí do `BubbleMenu` của Tiptap tính (Floating UI). Nó gắn vào CHA của vùng soạn chứ không vào
15
+ * `document.body`, nên ở trong `Dialog` vẫn nằm trong lớp của hộp thoại.
16
+ */
17
+
18
+ /**
19
+ * Chuẩn hoá địa chỉ người dùng gõ vào ô liên kết. Trả `null` khi KHÔNG nhận.
20
+ *
21
+ * Nhận: `http(s):`, `mailto:`, `tel:`, đường dẫn trong app (`/tasks/CV-12`, `#muc`), và hai dạng gõ
22
+ * tắt — tên miền trần (`comitor.ai/docs` → `https://…`), email trần (→ `mailto:`). Từ chối MỌI lược
23
+ * đồ khác: `javascript:`, `data:`, `vbscript:` là ba cách biến một liên kết thành mã chạy được.
24
+ */
25
+ declare function normalizeLinkHref(input: string): string | null;
26
+ interface BubbleToolbarProps {
27
+ editor: Editor;
28
+ labels: MarkdownEditorLabels;
29
+ /** Tăng mỗi lần ⌘/Ctrl+K — mở ô liên kết. Số đếm thay vì cờ để hai lần ⌘K liền nhau vẫn là hai lần. */
30
+ linkRequest: number;
31
+ }
32
+ declare function BubbleToolbar({ editor, labels, linkRequest }: BubbleToolbarProps): React.JSX.Element | null;
33
+
34
+ export { BubbleToolbar, type BubbleToolbarProps, normalizeLinkHref };
@@ -0,0 +1,391 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { NodeSelection } from "@tiptap/pm/state";
4
+ import { useEditorState } from "@tiptap/react";
5
+ import { BubbleMenu } from "@tiptap/react/menus";
6
+ import {
7
+ Bold,
8
+ Check,
9
+ Code,
10
+ ExternalLink,
11
+ Heading1,
12
+ Heading2,
13
+ Heading3,
14
+ Italic,
15
+ Link2,
16
+ Link2Off,
17
+ List,
18
+ ListChecks,
19
+ ListOrdered,
20
+ Pencil,
21
+ SquareCode,
22
+ Strikethrough,
23
+ TextQuote
24
+ } from "lucide-react";
25
+ import * as React from "react";
26
+ import { KeyboardHint } from "../components/composite/keyboard-hint.js";
27
+ import { Separator } from "../components/ui/separator.js";
28
+ import { Toggle } from "../components/ui/toggle.js";
29
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
30
+ import { cn } from "../lib/cn.js";
31
+ import { useAsRef } from "../lib/use-as-ref.js";
32
+ function normalizeLinkHref(input) {
33
+ const value = input.trim();
34
+ if (!value) return null;
35
+ if (/^(?:https?:\/\/|mailto:|tel:)/i.test(value)) return value;
36
+ if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return null;
37
+ if (/^[/#]/.test(value)) return value;
38
+ if (/^[^\s@/]+@[^\s@/]+\.[^\s@/]+$/.test(value)) return `mailto:${value}`;
39
+ if (/^[^\s/]+\.[^\s/.]{2,}(?:[/?#]\S*)?$/.test(value)) return `https://${value}`;
40
+ return null;
41
+ }
42
+ const MARK_ACTIONS = [
43
+ {
44
+ id: "bold",
45
+ icon: Bold,
46
+ label: "bold",
47
+ keys: "mod+b",
48
+ isActive: (editor) => editor.isActive("bold"),
49
+ run: (editor) => editor.chain().focus().toggleBold().run()
50
+ },
51
+ {
52
+ id: "italic",
53
+ icon: Italic,
54
+ label: "italic",
55
+ keys: "mod+i",
56
+ isActive: (editor) => editor.isActive("italic"),
57
+ run: (editor) => editor.chain().focus().toggleItalic().run()
58
+ },
59
+ {
60
+ id: "strike",
61
+ icon: Strikethrough,
62
+ label: "strike",
63
+ keys: "mod+shift+s",
64
+ isActive: (editor) => editor.isActive("strike"),
65
+ run: (editor) => editor.chain().focus().toggleStrike().run()
66
+ },
67
+ {
68
+ id: "code",
69
+ icon: Code,
70
+ label: "code",
71
+ keys: "mod+e",
72
+ isActive: (editor) => editor.isActive("code"),
73
+ run: (editor) => editor.chain().focus().toggleCode().run()
74
+ }
75
+ ];
76
+ const BLOCK_ACTIONS = [
77
+ {
78
+ id: "heading1",
79
+ icon: Heading1,
80
+ label: "heading1",
81
+ keys: "mod+alt+1",
82
+ isActive: (editor) => editor.isActive("heading", { level: 1 }),
83
+ run: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run()
84
+ },
85
+ {
86
+ id: "heading2",
87
+ icon: Heading2,
88
+ label: "heading2",
89
+ keys: "mod+alt+2",
90
+ isActive: (editor) => editor.isActive("heading", { level: 2 }),
91
+ run: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run()
92
+ },
93
+ {
94
+ id: "heading3",
95
+ icon: Heading3,
96
+ label: "heading3",
97
+ keys: "mod+alt+3",
98
+ isActive: (editor) => editor.isActive("heading", { level: 3 }),
99
+ run: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run()
100
+ },
101
+ {
102
+ id: "bulletList",
103
+ icon: List,
104
+ label: "bulletList",
105
+ keys: "mod+shift+8",
106
+ isActive: (editor) => editor.isActive("bulletList"),
107
+ run: (editor) => editor.chain().focus().toggleBulletList().run()
108
+ },
109
+ {
110
+ id: "orderedList",
111
+ icon: ListOrdered,
112
+ label: "orderedList",
113
+ keys: "mod+shift+7",
114
+ isActive: (editor) => editor.isActive("orderedList"),
115
+ run: (editor) => editor.chain().focus().toggleOrderedList().run()
116
+ },
117
+ {
118
+ id: "taskList",
119
+ icon: ListChecks,
120
+ label: "taskList",
121
+ keys: "mod+shift+9",
122
+ isActive: (editor) => editor.isActive("taskList"),
123
+ run: (editor) => editor.chain().focus().toggleTaskList().run()
124
+ },
125
+ {
126
+ id: "blockquote",
127
+ icon: TextQuote,
128
+ label: "blockquote",
129
+ keys: "mod+shift+b",
130
+ isActive: (editor) => editor.isActive("blockquote"),
131
+ run: (editor) => editor.chain().focus().toggleBlockquote().run()
132
+ },
133
+ {
134
+ id: "codeBlock",
135
+ icon: SquareCode,
136
+ label: "codeBlock",
137
+ keys: "mod+alt+c",
138
+ isActive: (editor) => editor.isActive("codeBlock"),
139
+ run: (editor) => editor.chain().focus().toggleCodeBlock().run()
140
+ }
141
+ ];
142
+ function readToolbarState(editor) {
143
+ const active = {};
144
+ for (const action of [...MARK_ACTIONS, ...BLOCK_ACTIONS]) active[action.id] = action.isActive(editor);
145
+ const href = editor.getAttributes("link").href;
146
+ return {
147
+ active,
148
+ link: editor.isActive("link"),
149
+ href: typeof href === "string" ? href : "",
150
+ empty: editor.state.selection.empty,
151
+ inTable: editor.isActive("table")
152
+ };
153
+ }
154
+ function sameToolbarState(a, b) {
155
+ if (!a || !b) return a === b;
156
+ if (a.link !== b.link || a.href !== b.href || a.empty !== b.empty || a.inTable !== b.inTable) return false;
157
+ return Object.keys(a.active).every((key) => a.active[key] === b.active[key]);
158
+ }
159
+ const BUTTON_CLASS = "h-7 min-w-7 px-1.5";
160
+ function ToolbarButton({
161
+ icon: Icon,
162
+ label,
163
+ keys,
164
+ pressed,
165
+ onClick,
166
+ className
167
+ }) {
168
+ const button = pressed === void 0 ? /* @__PURE__ */ jsx(
169
+ "button",
170
+ {
171
+ type: "button",
172
+ "aria-label": label,
173
+ onMouseDown: (event) => event.preventDefault(),
174
+ onClick,
175
+ className: cn(
176
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors duration-150 ease-out hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0",
177
+ BUTTON_CLASS,
178
+ className
179
+ ),
180
+ children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" })
181
+ }
182
+ ) : /* @__PURE__ */ jsx(
183
+ Toggle,
184
+ {
185
+ "aria-label": label,
186
+ pressed,
187
+ onPressedChange: onClick,
188
+ onMouseDown: (event) => event.preventDefault(),
189
+ className: cn(BUTTON_CLASS, className),
190
+ children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" })
191
+ }
192
+ );
193
+ return /* @__PURE__ */ jsxs(Tooltip, { delayDuration: 400, children: [
194
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: button }),
195
+ /* @__PURE__ */ jsxs(TooltipContent, { side: "top", sideOffset: 6, className: "flex items-center gap-2", children: [
196
+ label,
197
+ keys && /* @__PURE__ */ jsx(KeyboardHint, { keys, compact: true })
198
+ ] })
199
+ ] });
200
+ }
201
+ function BubbleToolbar({ editor, labels, linkRequest }) {
202
+ const state = useEditorState({
203
+ editor,
204
+ selector: ({ editor: current }) => readToolbarState(current),
205
+ equalityFn: sameToolbarState
206
+ });
207
+ const [editingLink, setEditingLink] = React.useState(false);
208
+ const editingLinkRef = useAsRef(editingLink);
209
+ const [draft, setDraft] = React.useState("");
210
+ const [invalid, setInvalid] = React.useState(false);
211
+ const inputRef = React.useRef(null);
212
+ const pluginKey = "comitorBubbleToolbar";
213
+ const showBubble = React.useCallback(() => {
214
+ editor.view.dispatch(editor.state.tr.setMeta(pluginKey, "show"));
215
+ editor.view.dispatch(editor.state.tr.setMeta(pluginKey, "updatePosition"));
216
+ }, [editor]);
217
+ const openLinkEditor = React.useCallback(() => {
218
+ const href = editor.getAttributes("link").href;
219
+ setDraft(typeof href === "string" ? href : "");
220
+ setInvalid(false);
221
+ setEditingLink(true);
222
+ }, [editor]);
223
+ React.useEffect(() => {
224
+ if (linkRequest === 0) return;
225
+ openLinkEditor();
226
+ }, [linkRequest, openLinkEditor]);
227
+ React.useEffect(() => {
228
+ if (!editingLink) return;
229
+ showBubble();
230
+ const frame = requestAnimationFrame(() => inputRef.current?.select());
231
+ return () => cancelAnimationFrame(frame);
232
+ }, [editingLink, showBubble]);
233
+ const closeLinkEditor = (refocus) => {
234
+ setEditingLink(false);
235
+ setInvalid(false);
236
+ if (refocus) editor.commands.focus();
237
+ };
238
+ const applyLink = () => {
239
+ const href = normalizeLinkHref(draft);
240
+ if (draft.trim() === "") {
241
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
242
+ closeLinkEditor(false);
243
+ return;
244
+ }
245
+ if (!href) {
246
+ setInvalid(true);
247
+ return;
248
+ }
249
+ const chain = editor.chain().focus().extendMarkRange("link");
250
+ if (editor.state.selection.empty && !editor.isActive("link")) {
251
+ chain.insertContent({ type: "text", text: draft.trim(), marks: [{ type: "link", attrs: { href } }] }).run();
252
+ } else {
253
+ chain.setLink({ href }).run();
254
+ }
255
+ closeLinkEditor(false);
256
+ };
257
+ const removeLink = () => {
258
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
259
+ closeLinkEditor(false);
260
+ };
261
+ const shouldShow = React.useCallback(
262
+ ({ editor: current, element, view, state: editorState, from, to }) => {
263
+ if (!current.isEditable) return false;
264
+ if (!(view.hasFocus() || element.contains(document.activeElement))) return false;
265
+ if (editingLinkRef.current) return true;
266
+ const { selection } = editorState;
267
+ if (selection instanceof NodeSelection || current.isActive("codeBlock")) return false;
268
+ if (selection.empty) return current.isActive("link");
269
+ return editorState.doc.textBetween(from, to, " ").trim().length > 0;
270
+ },
271
+ [editingLinkRef]
272
+ );
273
+ const options = React.useMemo(
274
+ () => ({
275
+ // `fixed`: không bị cắt bởi khung cuộn chứa trình soạn (`DialogBody`, cột chi tiết cuộn riêng).
276
+ strategy: "fixed",
277
+ placement: "top",
278
+ offset: 8,
279
+ flip: true,
280
+ shift: { padding: 8 },
281
+ onHide: () => setEditingLink(false)
282
+ }),
283
+ []
284
+ );
285
+ if (!state) return null;
286
+ return /* @__PURE__ */ jsx(
287
+ BubbleMenu,
288
+ {
289
+ editor,
290
+ pluginKey,
291
+ updateDelay: 100,
292
+ shouldShow,
293
+ options,
294
+ className: "z-50 rounded-lg border bg-popover p-1 text-popover-foreground shadow-overlay",
295
+ children: editingLink ? /* @__PURE__ */ jsxs(
296
+ "form",
297
+ {
298
+ className: "flex flex-col gap-1",
299
+ onSubmit: (event) => {
300
+ event.preventDefault();
301
+ applyLink();
302
+ },
303
+ children: [
304
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
305
+ /* @__PURE__ */ jsx(
306
+ "input",
307
+ {
308
+ ref: inputRef,
309
+ type: "text",
310
+ inputMode: "url",
311
+ "aria-label": labels.linkInput,
312
+ "aria-invalid": invalid || void 0,
313
+ placeholder: labels.linkPlaceholder,
314
+ value: draft,
315
+ onChange: (event) => {
316
+ setDraft(event.target.value);
317
+ setInvalid(false);
318
+ },
319
+ onKeyDown: (event) => {
320
+ if (event.key === "Escape") {
321
+ event.preventDefault();
322
+ event.stopPropagation();
323
+ closeLinkEditor(true);
324
+ }
325
+ },
326
+ className: "h-7 w-60 rounded-md border border-input bg-transparent px-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50 aria-invalid:border-destructive-ink dark:bg-input-fill"
327
+ }
328
+ ),
329
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Check, label: labels.linkApply, onClick: applyLink }),
330
+ state.link && /* @__PURE__ */ jsx(ToolbarButton, { icon: Link2Off, label: labels.linkRemove, onClick: removeLink })
331
+ ] }),
332
+ invalid && /* @__PURE__ */ jsx("p", { role: "alert", className: "px-1 pb-0.5 text-xs text-destructive-ink", children: labels.linkInvalid })
333
+ ]
334
+ }
335
+ ) : state.empty && state.link ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
336
+ /* @__PURE__ */ jsxs(
337
+ "a",
338
+ {
339
+ href: state.href,
340
+ target: "_blank",
341
+ rel: "noopener noreferrer nofollow",
342
+ onMouseDown: (event) => event.preventDefault(),
343
+ className: "flex max-w-60 items-center gap-1.5 truncate rounded-md px-1.5 py-1 text-sm text-link-ink underline-offset-4 hover:text-link-ink-hover hover:underline",
344
+ children: [
345
+ /* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": "true", className: "size-3.5 shrink-0" }),
346
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: state.href }),
347
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.linkOpen })
348
+ ]
349
+ }
350
+ ),
351
+ /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" }),
352
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Pencil, label: labels.linkEdit, keys: "mod+k", onClick: openLinkEditor }),
353
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Link2Off, label: labels.linkRemove, onClick: removeLink })
354
+ ] }) : /* @__PURE__ */ jsxs("div", { role: "toolbar", "aria-label": labels.toolbar, className: "flex items-center gap-0.5", children: [
355
+ MARK_ACTIONS.map((action) => /* @__PURE__ */ jsx(
356
+ ToolbarButton,
357
+ {
358
+ icon: action.icon,
359
+ label: labels[action.label],
360
+ keys: action.keys,
361
+ pressed: Boolean(state.active[action.id]),
362
+ onClick: () => action.run(editor)
363
+ },
364
+ action.id
365
+ )),
366
+ /* @__PURE__ */ jsx(ToolbarButton, { icon: Link2, label: labels.link, keys: "mod+k", pressed: state.link, onClick: openLinkEditor }),
367
+ !state.inTable && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 max-sm:hidden", children: [
368
+ /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" }),
369
+ BLOCK_ACTIONS.map((action, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
370
+ (index === 3 || index === 6) && /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" }),
371
+ /* @__PURE__ */ jsx(
372
+ ToolbarButton,
373
+ {
374
+ icon: action.icon,
375
+ label: labels[action.label],
376
+ keys: action.keys,
377
+ pressed: Boolean(state.active[action.id]),
378
+ onClick: () => action.run(editor)
379
+ }
380
+ )
381
+ ] }, action.id))
382
+ ] })
383
+ ] })
384
+ }
385
+ );
386
+ }
387
+ export {
388
+ BubbleToolbar,
389
+ normalizeLinkHref
390
+ };
391
+ //# sourceMappingURL=bubble-toolbar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/editor/bubble-toolbar.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Thanh định dạng NỔI — hiện khi bôi chọn chữ, như Linear: không có thanh công cụ cố định chiếm chỗ\n * trên đầu tài liệu, định dạng ở ngay chỗ đang chọn.\n *\n * Ba trạng thái trong cùng một bong bóng:\n * · đang chọn chữ → nút định dạng (dấu nội dòng + đổi khối);\n * · con trỏ đứng TRONG một liên kết (không chọn gì) → địa chỉ + Mở / Sửa / Gỡ;\n * · đang sửa liên kết (nút Liên kết, hoặc ⌘K) → ô nhập địa chỉ.\n *\n * Vị trí do `BubbleMenu` của Tiptap tính (Floating UI). Nó gắn vào CHA của vùng soạn chứ không vào\n * `document.body`, nên ở trong `Dialog` vẫn nằm trong lớp của hộp thoại.\n */\n\nimport type { Editor } from \"@tiptap/core\";\nimport { NodeSelection } from \"@tiptap/pm/state\";\nimport { useEditorState } from \"@tiptap/react\";\nimport { BubbleMenu, type BubbleMenuProps } from \"@tiptap/react/menus\";\nimport {\n Bold,\n Check,\n Code,\n ExternalLink,\n Heading1,\n Heading2,\n Heading3,\n Italic,\n Link2,\n Link2Off,\n List,\n ListChecks,\n ListOrdered,\n type LucideIcon,\n Pencil,\n SquareCode,\n Strikethrough,\n TextQuote\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { KeyboardHint } from \"../components/composite/keyboard-hint.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Toggle } from \"../components/ui/toggle.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useAsRef } from \"../lib/use-as-ref.js\";\nimport type { MarkdownEditorLabels } from \"./markdown-editor-constants.js\";\n\n/**\n * Chuẩn hoá địa chỉ người dùng gõ vào ô liên kết. Trả `null` khi KHÔNG nhận.\n *\n * Nhận: `http(s):`, `mailto:`, `tel:`, đường dẫn trong app (`/tasks/CV-12`, `#muc`), và hai dạng gõ\n * tắt — tên miền trần (`comitor.ai/docs` → `https://…`), email trần (→ `mailto:`). Từ chối MỌI lược\n * đồ khác: `javascript:`, `data:`, `vbscript:` là ba cách biến một liên kết thành mã chạy được.\n */\nexport function normalizeLinkHref(input: string): string | null {\n const value = input.trim();\n if (!value) return null;\n if (/^(?:https?:\\/\\/|mailto:|tel:)/i.test(value)) return value;\n if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return null;\n if (/^[/#]/.test(value)) return value;\n if (/^[^\\s@/]+@[^\\s@/]+\\.[^\\s@/]+$/.test(value)) return `mailto:${value}`;\n if (/^[^\\s/]+\\.[^\\s/.]{2,}(?:[/?#]\\S*)?$/.test(value)) return `https://${value}`;\n return null;\n}\n\n/** Khoá nhãn mang CHUỖI (loại `slashKeywords` và hàm `taskCheckbox`). */\ntype TextLabelKey = {\n [K in keyof MarkdownEditorLabels]: MarkdownEditorLabels[K] extends string ? K : never;\n}[keyof MarkdownEditorLabels];\n\ninterface ToolbarAction {\n id: string;\n icon: LucideIcon;\n label: TextLabelKey;\n /** Phím tắt cho tooltip — cú pháp của `KeyboardHint` (`mod+b`). Mặc định Tiptap, trừ ⌘K của gói. */\n keys?: string;\n isActive: (editor: Editor) => boolean;\n run: (editor: Editor) => void;\n}\n\nconst MARK_ACTIONS: readonly ToolbarAction[] = [\n {\n id: \"bold\",\n icon: Bold,\n label: \"bold\",\n keys: \"mod+b\",\n isActive: (editor) => editor.isActive(\"bold\"),\n run: (editor) => editor.chain().focus().toggleBold().run()\n },\n {\n id: \"italic\",\n icon: Italic,\n label: \"italic\",\n keys: \"mod+i\",\n isActive: (editor) => editor.isActive(\"italic\"),\n run: (editor) => editor.chain().focus().toggleItalic().run()\n },\n {\n id: \"strike\",\n icon: Strikethrough,\n label: \"strike\",\n keys: \"mod+shift+s\",\n isActive: (editor) => editor.isActive(\"strike\"),\n run: (editor) => editor.chain().focus().toggleStrike().run()\n },\n {\n id: \"code\",\n icon: Code,\n label: \"code\",\n keys: \"mod+e\",\n isActive: (editor) => editor.isActive(\"code\"),\n run: (editor) => editor.chain().focus().toggleCode().run()\n }\n];\n\n/*\n * Đổi khối. Ẩn ở màn hẹp (thanh sẽ rộng hơn 375px) và trong bảng (ô bảng chỉ chứa một đoạn văn —\n * xem `ComitorTableCell`): cả hai chỗ đó vẫn còn cú pháp gõ tắt và menu `/`.\n */\nconst BLOCK_ACTIONS: readonly ToolbarAction[] = [\n {\n id: \"heading1\",\n icon: Heading1,\n label: \"heading1\",\n keys: \"mod+alt+1\",\n isActive: (editor) => editor.isActive(\"heading\", { level: 1 }),\n run: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run()\n },\n {\n id: \"heading2\",\n icon: Heading2,\n label: \"heading2\",\n keys: \"mod+alt+2\",\n isActive: (editor) => editor.isActive(\"heading\", { level: 2 }),\n run: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run()\n },\n {\n id: \"heading3\",\n icon: Heading3,\n label: \"heading3\",\n keys: \"mod+alt+3\",\n isActive: (editor) => editor.isActive(\"heading\", { level: 3 }),\n run: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run()\n },\n {\n id: \"bulletList\",\n icon: List,\n label: \"bulletList\",\n keys: \"mod+shift+8\",\n isActive: (editor) => editor.isActive(\"bulletList\"),\n run: (editor) => editor.chain().focus().toggleBulletList().run()\n },\n {\n id: \"orderedList\",\n icon: ListOrdered,\n label: \"orderedList\",\n keys: \"mod+shift+7\",\n isActive: (editor) => editor.isActive(\"orderedList\"),\n run: (editor) => editor.chain().focus().toggleOrderedList().run()\n },\n {\n id: \"taskList\",\n icon: ListChecks,\n label: \"taskList\",\n keys: \"mod+shift+9\",\n isActive: (editor) => editor.isActive(\"taskList\"),\n run: (editor) => editor.chain().focus().toggleTaskList().run()\n },\n {\n id: \"blockquote\",\n icon: TextQuote,\n label: \"blockquote\",\n keys: \"mod+shift+b\",\n isActive: (editor) => editor.isActive(\"blockquote\"),\n run: (editor) => editor.chain().focus().toggleBlockquote().run()\n },\n {\n id: \"codeBlock\",\n icon: SquareCode,\n label: \"codeBlock\",\n keys: \"mod+alt+c\",\n isActive: (editor) => editor.isActive(\"codeBlock\"),\n run: (editor) => editor.chain().focus().toggleCodeBlock().run()\n }\n];\n\ninterface ToolbarState {\n active: Record<string, boolean>;\n link: boolean;\n href: string;\n empty: boolean;\n inTable: boolean;\n}\n\nfunction readToolbarState(editor: Editor): ToolbarState {\n const active: Record<string, boolean> = {};\n for (const action of [...MARK_ACTIONS, ...BLOCK_ACTIONS]) active[action.id] = action.isActive(editor);\n const href = editor.getAttributes(\"link\").href;\n return {\n active,\n link: editor.isActive(\"link\"),\n href: typeof href === \"string\" ? href : \"\",\n empty: editor.state.selection.empty,\n inTable: editor.isActive(\"table\")\n };\n}\n\n/** So nông — `useEditorState` chỉ render lại khi kết quả KHÁC, mà mỗi lần đọc là một object mới. */\nfunction sameToolbarState(a: ToolbarState | null, b: ToolbarState | null): boolean {\n if (!a || !b) return a === b;\n if (a.link !== b.link || a.href !== b.href || a.empty !== b.empty || a.inTable !== b.inTable) return false;\n return Object.keys(a.active).every((key) => a.active[key] === b.active[key]);\n}\n\n/*\n * Nút nhỏ hơn `Toggle size=\"sm\"` một bậc (28px): mười ba nút của Linear-style toolbar phải nằm gọn\n * trên một dòng ở màn laptop. Mousedown bị chặn để focus Ở LẠI vùng soạn — mất focus là mất vùng chọn\n * mà nút sắp áp định dạng lên.\n */\nconst BUTTON_CLASS = \"h-7 min-w-7 px-1.5\";\n\nfunction ToolbarButton({\n icon: Icon,\n label,\n keys,\n pressed,\n onClick,\n className\n}: {\n icon: LucideIcon;\n label: string;\n keys?: string;\n pressed?: boolean;\n onClick: () => void;\n className?: string;\n}) {\n const button =\n pressed === undefined ? (\n <button\n type=\"button\"\n aria-label={label}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClick}\n className={cn(\n \"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors duration-150 ease-out hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0\",\n BUTTON_CLASS,\n className\n )}\n >\n <Icon aria-hidden=\"true\" />\n </button>\n ) : (\n <Toggle\n aria-label={label}\n pressed={pressed}\n onPressedChange={onClick}\n onMouseDown={(event) => event.preventDefault()}\n className={cn(BUTTON_CLASS, className)}\n >\n <Icon aria-hidden=\"true\" />\n </Toggle>\n );\n\n return (\n <Tooltip delayDuration={400}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent side=\"top\" sideOffset={6} className=\"flex items-center gap-2\">\n {label}\n {keys && <KeyboardHint keys={keys} compact />}\n </TooltipContent>\n </Tooltip>\n );\n}\n\nexport interface BubbleToolbarProps {\n editor: Editor;\n labels: MarkdownEditorLabels;\n /** Tăng mỗi lần ⌘/Ctrl+K — mở ô liên kết. Số đếm thay vì cờ để hai lần ⌘K liền nhau vẫn là hai lần. */\n linkRequest: number;\n}\n\nexport function BubbleToolbar({ editor, labels, linkRequest }: BubbleToolbarProps) {\n const state = useEditorState({\n editor,\n selector: ({ editor: current }) => readToolbarState(current),\n equalityFn: sameToolbarState\n });\n const [editingLink, setEditingLink] = React.useState(false);\n const editingLinkRef = useAsRef(editingLink);\n const [draft, setDraft] = React.useState(\"\");\n const [invalid, setInvalid] = React.useState(false);\n const inputRef = React.useRef<HTMLInputElement>(null);\n const pluginKey = \"comitorBubbleToolbar\";\n\n /*\n * Hai giao dịch, theo đúng thứ tự: meta `\"show\"` của plugin gọi `updatePosition()` TRƯỚC `show()`,\n * mà `updatePosition()` bỏ qua khi bong bóng đang ẩn — nên một mình `\"show\"` hiện bong bóng ở toạ\n * độ CŨ (lần hiện trước, hoặc góc trái trên). `\"updatePosition\"` sau đó mới tính đúng chỗ.\n */\n const showBubble = React.useCallback(() => {\n editor.view.dispatch(editor.state.tr.setMeta(pluginKey, \"show\"));\n editor.view.dispatch(editor.state.tr.setMeta(pluginKey, \"updatePosition\"));\n }, [editor]);\n\n const openLinkEditor = React.useCallback(() => {\n const href = editor.getAttributes(\"link\").href;\n setDraft(typeof href === \"string\" ? href : \"\");\n setInvalid(false);\n setEditingLink(true);\n }, [editor]);\n\n // ⌘K từ vùng soạn. Bỏ qua giá trị đầu (0) — đó là lúc mount, không phải một lần bấm.\n React.useEffect(() => {\n if (linkRequest === 0) return;\n openLinkEditor();\n }, [linkRequest, openLinkEditor]);\n\n // Vừa vào chế độ sửa: bắt plugin hiện bong bóng (kể cả khi không chọn gì) rồi đưa focus vào ô nhập.\n React.useEffect(() => {\n if (!editingLink) return;\n showBubble();\n const frame = requestAnimationFrame(() => inputRef.current?.select());\n return () => cancelAnimationFrame(frame);\n }, [editingLink, showBubble]);\n\n const closeLinkEditor = (refocus: boolean) => {\n setEditingLink(false);\n setInvalid(false);\n if (refocus) editor.commands.focus();\n };\n\n const applyLink = () => {\n const href = normalizeLinkHref(draft);\n if (draft.trim() === \"\") {\n editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n closeLinkEditor(false);\n return;\n }\n if (!href) {\n setInvalid(true);\n return;\n }\n const chain = editor.chain().focus().extendMarkRange(\"link\");\n if (editor.state.selection.empty && !editor.isActive(\"link\")) {\n // Không chọn chữ nào: chèn chính địa chỉ làm chữ của liên kết.\n chain.insertContent({ type: \"text\", text: draft.trim(), marks: [{ type: \"link\", attrs: { href } }] }).run();\n } else {\n chain.setLink({ href }).run();\n }\n closeLinkEditor(false);\n };\n\n const removeLink = () => {\n editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n closeLinkEditor(false);\n };\n\n /*\n * Ổn định qua các lượt render: `BubbleMenu` gửi lại TOÀN BỘ tuỳ chọn vào plugin mỗi khi một prop\n * đổi định danh. `shouldShow` đọc cờ sửa-liên-kết qua ref vì cùng lý do.\n */\n const shouldShow = React.useCallback<NonNullable<BubbleMenuProps[\"shouldShow\"]>>(\n ({ editor: current, element, view, state: editorState, from, to }) => {\n if (!current.isEditable) return false;\n if (!(view.hasFocus() || element.contains(document.activeElement))) return false;\n if (editingLinkRef.current) return true;\n const { selection } = editorState;\n if (selection instanceof NodeSelection || current.isActive(\"codeBlock\")) return false;\n if (selection.empty) return current.isActive(\"link\");\n return editorState.doc.textBetween(from, to, \" \").trim().length > 0;\n },\n [editingLinkRef]\n );\n\n const options = React.useMemo<BubbleMenuProps[\"options\"]>(\n () => ({\n // `fixed`: không bị cắt bởi khung cuộn chứa trình soạn (`DialogBody`, cột chi tiết cuộn riêng).\n strategy: \"fixed\",\n placement: \"top\",\n offset: 8,\n flip: true,\n shift: { padding: 8 },\n onHide: () => setEditingLink(false)\n }),\n []\n );\n\n if (!state) return null;\n\n return (\n <BubbleMenu\n editor={editor}\n pluginKey={pluginKey}\n updateDelay={100}\n shouldShow={shouldShow}\n options={options}\n className=\"z-50 rounded-lg border bg-popover p-1 text-popover-foreground shadow-overlay\"\n >\n {editingLink ? (\n <form\n className=\"flex flex-col gap-1\"\n onSubmit={(event) => {\n event.preventDefault();\n applyLink();\n }}\n >\n <div className=\"flex items-center gap-1\">\n <input\n ref={inputRef}\n type=\"text\"\n inputMode=\"url\"\n aria-label={labels.linkInput}\n aria-invalid={invalid || undefined}\n placeholder={labels.linkPlaceholder}\n value={draft}\n onChange={(event) => {\n setDraft(event.target.value);\n setInvalid(false);\n }}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n // Escape ở đây chỉ đóng ô liên kết — không để nó lan tới `Dialog` bao ngoài.\n event.stopPropagation();\n closeLinkEditor(true);\n }\n }}\n className=\"h-7 w-60 rounded-md border border-input bg-transparent px-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50 aria-invalid:border-destructive-ink dark:bg-input-fill\"\n />\n <ToolbarButton icon={Check} label={labels.linkApply} onClick={applyLink} />\n {state.link && <ToolbarButton icon={Link2Off} label={labels.linkRemove} onClick={removeLink} />}\n </div>\n {invalid && (\n <p role=\"alert\" className=\"px-1 pb-0.5 text-xs text-destructive-ink\">\n {labels.linkInvalid}\n </p>\n )}\n </form>\n ) : state.empty && state.link ? (\n <div className=\"flex items-center gap-1\">\n <a\n href={state.href}\n target=\"_blank\"\n rel=\"noopener noreferrer nofollow\"\n onMouseDown={(event) => event.preventDefault()}\n className=\"flex max-w-60 items-center gap-1.5 truncate rounded-md px-1.5 py-1 text-sm text-link-ink underline-offset-4 hover:text-link-ink-hover hover:underline\"\n >\n <ExternalLink aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n <span className=\"truncate\">{state.href}</span>\n <span className=\"sr-only\">{labels.linkOpen}</span>\n </a>\n <Separator orientation=\"vertical\" className=\"mx-0.5 h-5\" />\n <ToolbarButton icon={Pencil} label={labels.linkEdit} keys=\"mod+k\" onClick={openLinkEditor} />\n <ToolbarButton icon={Link2Off} label={labels.linkRemove} onClick={removeLink} />\n </div>\n ) : (\n <div role=\"toolbar\" aria-label={labels.toolbar} className=\"flex items-center gap-0.5\">\n {MARK_ACTIONS.map((action) => (\n <ToolbarButton\n key={action.id}\n icon={action.icon}\n label={labels[action.label]}\n keys={action.keys}\n pressed={Boolean(state.active[action.id])}\n onClick={() => action.run(editor)}\n />\n ))}\n <ToolbarButton icon={Link2} label={labels.link} keys=\"mod+k\" pressed={state.link} onClick={openLinkEditor} />\n {!state.inTable && (\n <div className=\"flex items-center gap-0.5 max-sm:hidden\">\n <Separator orientation=\"vertical\" className=\"mx-0.5 h-5\" />\n {BLOCK_ACTIONS.map((action, index) => (\n <React.Fragment key={action.id}>\n {(index === 3 || index === 6) && <Separator orientation=\"vertical\" className=\"mx-0.5 h-5\" />}\n <ToolbarButton\n icon={action.icon}\n label={labels[action.label]}\n keys={action.keys}\n pressed={Boolean(state.active[action.id])}\n onClick={() => action.run(editor)}\n />\n </React.Fragment>\n ))}\n </div>\n )}\n </div>\n )}\n </BubbleMenu>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,57 @@
1
+ import { Extension, Editor } from '@tiptap/core';
2
+
3
+ /**
4
+ * Ảnh dán / kéo thả / chọn từ menu `/` → tải qua `onImageUpload` của app → chèn vào tài liệu.
5
+ *
6
+ * Gói KHÔNG biết kho ảnh ở đâu: bucket của module Comitor là riêng tư, URL hiện được là URL đã ký
7
+ * — nên đường tải là của app (route của app ký và ghi), gói chỉ trao `File` và nhận lại `src` để
8
+ * LƯU. Cùng nguyên tắc với `/uploader` (`XHRUpload` về endpoint của app).
9
+ *
10
+ * ── ẢNH ĐANG TẢI LÀ DECORATION, KHÔNG PHẢI NODE ────────────────────────────────────────────────
11
+ * Trong lúc tải, ảnh hiện NGAY bằng `blob:` của chính tệp — nhưng dưới dạng một widget decoration
12
+ * (khuôn "upload" của ProseMirror), không phải một node trong tài liệu. Node ảnh thật chỉ được chèn
13
+ * MỘT lần, khi app đã trả `src`.
14
+ *
15
+ * Bản đầu chèn một node ảnh TẠM rồi đổi `src` khi xong (giao dịch đổi không vào lịch sử). Nó qua
16
+ * mọi phép thử lúc gõ, và hỏng ở lịch sử hoàn tác: ⌘Z gỡ ảnh, rồi ⌘⇧Z làm lại ĐÚNG bước chèn node
17
+ * tạm — `src` là `blob:` đã thu hồi, cờ "đang tải" còn nguyên: một ảnh vỡ kẹt mãi, và vì cờ đó
18
+ * nên nó cũng không bao giờ vào markdown. Với decoration thì tài liệu không bao giờ chứa thứ gì tạm:
19
+ * lịch sử chỉ có một bước, "chèn ảnh thật".
20
+ */
21
+
22
+ /** Thứ `onImageUpload` trả về: `src` để LƯU, hoặc kèm `alt`/`title`. */
23
+ type MarkdownEditorImageUploadResult = string | {
24
+ src: string;
25
+ alt?: string;
26
+ title?: string;
27
+ };
28
+ interface ImageUploadHandlers {
29
+ upload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;
30
+ onError: (error: unknown, file: File) => void;
31
+ /** Đổi `src` lưu → `src` hiện (`resolveImageSrc`), để tải trước ảnh thật trước khi chèn. */
32
+ resolveSrc?: (src: string) => string;
33
+ /** Nhãn trợ năng của ảnh đang tải (`role="img"` của widget). */
34
+ uploadingLabel: string;
35
+ }
36
+ /** Ảnh đang tải của MỘT trình soạn — `MarkdownEditorHandle.isUploading()` đọc nó. */
37
+ type PendingUploads = Set<string>;
38
+ /** Tệp ảnh trong một lần dán/thả. Tệp khác (PDF, zip…) bỏ qua: trình soạn chỉ nhúng ảnh. */
39
+ declare function imageFilesFrom(data: DataTransfer | null): File[];
40
+ /**
41
+ * Hiện widget cho từng tệp và tải chúng. Xong tệp nào thì gỡ widget và chèn node ảnh thật vào ĐÚNG
42
+ * chỗ widget đang đứng (vị trí đã đi theo mọi chỉnh sửa trong lúc chờ), trong CÙNG một giao dịch —
43
+ * một bước hoàn tác. Không dời con trỏ: người dùng có thể đang gõ ở chỗ khác.
44
+ */
45
+ declare function insertImageFiles(editor: Editor, files: readonly File[], position: number | undefined, handlers: ImageUploadHandlers, pending: PendingUploads): void;
46
+ /**
47
+ * Bắt ảnh khi DÁN và khi THẢ, và giữ các widget đang tải. Không có `onImageUpload` thì plugin đứng
48
+ * ngoài (trả `false`) — dán/thả đi đường mặc định của Tiptap, tức ảnh dạng tệp không được nhận
49
+ * (không có chỗ nào để lưu nó).
50
+ *
51
+ * Kéo thả NỘI BỘ (di chuyển một ảnh đã có trong tài liệu, `moved === true`) thì không đụng tới.
52
+ */
53
+ declare function createImageUploadExtension(handlers: {
54
+ readonly current: ImageUploadHandlers;
55
+ }, pending: PendingUploads): Extension;
56
+
57
+ export { type ImageUploadHandlers, type MarkdownEditorImageUploadResult, type PendingUploads, createImageUploadExtension, imageFilesFrom, insertImageFiles };