@comitor/ui 3.5.0 → 3.6.1

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 (51) hide show
  1. package/README.md +4 -2
  2. package/dist/data-grid/data-grid-cell-variants.js +6 -1
  3. package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
  4. package/dist/data-grid/types.d.ts +10 -0
  5. package/dist/data-grid/use-data-grid.d.ts +2 -2
  6. package/dist/data-grid/use-data-grid.js +30 -13
  7. package/dist/data-grid/use-data-grid.js.map +1 -1
  8. package/dist/editor/bubble-toolbar.d.ts +20 -1
  9. package/dist/editor/bubble-toolbar.js +12 -172
  10. package/dist/editor/bubble-toolbar.js.map +1 -1
  11. package/dist/editor/editor-core.d.ts +28 -0
  12. package/dist/editor/editor-core.js +354 -0
  13. package/dist/editor/editor-core.js.map +1 -0
  14. package/dist/editor/editor-types.d.ts +3 -0
  15. package/dist/editor/editor-types.js +1 -0
  16. package/dist/editor/editor-types.js.map +1 -0
  17. package/dist/editor/fixed-toolbar.d.ts +19 -0
  18. package/dist/editor/fixed-toolbar.js +106 -0
  19. package/dist/editor/fixed-toolbar.js.map +1 -0
  20. package/dist/editor/image-upload.d.ts +3 -57
  21. package/dist/editor/image-upload.js +3 -2
  22. package/dist/editor/image-upload.js.map +1 -1
  23. package/dist/editor/index.d.ts +3 -2
  24. package/dist/editor/index.js +4 -4
  25. package/dist/editor/index.js.map +1 -1
  26. package/dist/editor/markdown-editor-classes.js +6 -4
  27. package/dist/editor/markdown-editor-classes.js.map +1 -1
  28. package/dist/editor/markdown-editor-constants.d.ts +8 -2
  29. package/dist/editor/markdown-editor-constants.js.map +1 -1
  30. package/dist/editor/markdown-editor.d.ts +14 -91
  31. package/dist/editor/markdown-editor.js +23 -315
  32. package/dist/editor/markdown-editor.js.map +1 -1
  33. package/dist/editor/markdown-extensions.d.ts +40 -15
  34. package/dist/editor/markdown-extensions.js +143 -24
  35. package/dist/editor/markdown-extensions.js.map +1 -1
  36. package/dist/editor/rich-text-editor.d.ts +48 -0
  37. package/dist/editor/rich-text-editor.js +33 -0
  38. package/dist/editor/rich-text-editor.js.map +1 -0
  39. package/dist/editor/slash-commands.d.ts +4 -1
  40. package/dist/editor/slash-commands.js +7 -2
  41. package/dist/editor/slash-commands.js.map +1 -1
  42. package/dist/editor/toolbar-actions.d.ts +44 -0
  43. package/dist/editor/toolbar-actions.js +173 -0
  44. package/dist/editor/toolbar-actions.js.map +1 -0
  45. package/dist/editor/toolbar-button.d.ts +15 -0
  46. package/dist/editor/toolbar-button.js +47 -0
  47. package/dist/editor/toolbar-button.js.map +1 -0
  48. package/dist/editor-types-BMST7-VR.d.ts +171 -0
  49. package/dist/i18n/namespaces/markdown-editor.js +6 -0
  50. package/dist/i18n/namespaces/markdown-editor.js.map +1 -1
  51. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { Editor } from '@tiptap/core';
2
2
  import * as React from 'react';
3
+ import { f as EditorFormat } from '../editor-types-BMST7-VR.js';
3
4
  import { MarkdownEditorLabels } from './markdown-editor-constants.js';
4
5
 
5
6
  /**
@@ -13,6 +14,18 @@ import { MarkdownEditorLabels } from './markdown-editor-constants.js';
13
14
  *
14
15
  * 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
16
  * `document.body`, nên ở trong `Dialog` vẫn nằm trong lớp của hộp thoại.
17
+ *
18
+ * ⚠ `strategy` PHẢI là `absolute` (mặc định của Tiptap), không phải `fixed`. `BubbleMenu` của
19
+ * `@tiptap/react` gán sẵn `position: absolute` cho phần tử lúc dựng, và Floating UI đo theo vị trí
20
+ * ĐANG CÓ của phần tử: lần hiện ĐẦU TIÊN, nó tính toạ độ theo khung `relative` của trình soạn rồi plugin
21
+ * áp chúng như toạ độ MÀN HÌNH (`fixed`) — bong bóng lệch đúng bằng khoảng cách từ khung tới góc màn
22
+ * hình, tới lần cập nhật sau mới về chỗ. `3.5.0` để `fixed` và lỗi đó có thật (đo ở harness: liên kết
23
+ * mở ở `left: -9px` thay vì sát chữ). `absolute` còn đi theo chữ khi một khung cuộn BÊN TRONG cuộn
24
+ * (`DialogBody`, cột chi tiết) — `fixed` thì đứng yên, vì plugin chỉ nghe cuộn của `window`. Bị khung
25
+ * cuộn cắt thì `flip` lật bong bóng xuống dưới vùng chọn.
26
+ *
27
+ * Trình soạn có thanh CỐ ĐỊNH (`toolbar="fixed"`) vẫn dùng bong bóng này với `formatting={false}`:
28
+ * nút định dạng đã ở thanh trên, nhưng ô sửa liên kết và phần xem trước liên kết chỉ có ở đây.
16
29
  */
17
30
 
18
31
  /**
@@ -26,9 +39,15 @@ declare function normalizeLinkHref(input: string): string | null;
26
39
  interface BubbleToolbarProps {
27
40
  editor: Editor;
28
41
  labels: MarkdownEditorLabels;
42
+ format: EditorFormat;
43
+ /**
44
+ * `false` khi trình soạn có thanh cố định (`toolbar="fixed"`): bong bóng không hiện nút định dạng
45
+ * nữa (đã có ở thanh trên), chỉ còn phần LIÊN KẾT — xem trước, sửa, gỡ.
46
+ */
47
+ formatting: boolean;
29
48
  /** 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
49
  linkRequest: number;
31
50
  }
32
- declare function BubbleToolbar({ editor, labels, linkRequest }: BubbleToolbarProps): React.JSX.Element | null;
51
+ declare function BubbleToolbar({ editor, labels, format, formatting, linkRequest }: BubbleToolbarProps): React.JSX.Element | null;
33
52
 
34
53
  export { BubbleToolbar, type BubbleToolbarProps, normalizeLinkHref };
@@ -3,32 +3,12 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { NodeSelection } from "@tiptap/pm/state";
4
4
  import { useEditorState } from "@tiptap/react";
5
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";
6
+ import { Check, ExternalLink, Link2, Link2Off, Pencil } from "lucide-react";
25
7
  import * as React from "react";
26
- import { KeyboardHint } from "../components/composite/keyboard-hint.js";
27
8
  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
9
  import { useAsRef } from "../lib/use-as-ref.js";
10
+ import { actionsFor, BLOCK_ACTIONS, MARK_ACTIONS } from "./toolbar-actions.js";
11
+ import { ToolbarButton } from "./toolbar-button.js";
32
12
  function normalizeLinkHref(input) {
33
13
  const value = input.trim();
34
14
  if (!value) return null;
@@ -39,109 +19,9 @@ function normalizeLinkHref(input) {
39
19
  if (/^[^\s/]+\.[^\s/.]{2,}(?:[/?#]\S*)?$/.test(value)) return `https://${value}`;
40
20
  return null;
41
21
  }
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
22
  function readToolbarState(editor) {
143
23
  const active = {};
144
- for (const action of [...MARK_ACTIONS, ...BLOCK_ACTIONS]) active[action.id] = action.isActive(editor);
24
+ for (const action of [...MARK_ACTIONS, ...BLOCK_ACTIONS]) active[action.id] = action.isActive?.(editor) ?? false;
145
25
  const href = editor.getAttributes("link").href;
146
26
  return {
147
27
  active,
@@ -156,49 +36,7 @@ function sameToolbarState(a, b) {
156
36
  if (a.link !== b.link || a.href !== b.href || a.empty !== b.empty || a.inTable !== b.inTable) return false;
157
37
  return Object.keys(a.active).every((key) => a.active[key] === b.active[key]);
158
38
  }
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 }) {
39
+ function BubbleToolbar({ editor, labels, format, formatting, linkRequest }) {
202
40
  const state = useEditorState({
203
41
  editor,
204
42
  selector: ({ editor: current }) => readToolbarState(current),
@@ -206,6 +44,7 @@ function BubbleToolbar({ editor, labels, linkRequest }) {
206
44
  });
207
45
  const [editingLink, setEditingLink] = React.useState(false);
208
46
  const editingLinkRef = useAsRef(editingLink);
47
+ const formattingRef = useAsRef(formatting);
209
48
  const [draft, setDraft] = React.useState("");
210
49
  const [invalid, setInvalid] = React.useState(false);
211
50
  const inputRef = React.useRef(null);
@@ -266,14 +105,15 @@ function BubbleToolbar({ editor, labels, linkRequest }) {
266
105
  const { selection } = editorState;
267
106
  if (selection instanceof NodeSelection || current.isActive("codeBlock")) return false;
268
107
  if (selection.empty) return current.isActive("link");
108
+ if (!formattingRef.current) return false;
269
109
  return editorState.doc.textBetween(from, to, " ").trim().length > 0;
270
110
  },
271
- [editingLinkRef]
111
+ [editingLinkRef, formattingRef]
272
112
  );
273
113
  const options = React.useMemo(
274
114
  () => ({
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",
115
+ // `absolute`, không `fixed` — lý do ở đầu file.
116
+ strategy: "absolute",
277
117
  placement: "top",
278
118
  offset: 8,
279
119
  flip: true,
@@ -352,7 +192,7 @@ function BubbleToolbar({ editor, labels, linkRequest }) {
352
192
  /* @__PURE__ */ jsx(ToolbarButton, { icon: Pencil, label: labels.linkEdit, keys: "mod+k", onClick: openLinkEditor }),
353
193
  /* @__PURE__ */ jsx(ToolbarButton, { icon: Link2Off, label: labels.linkRemove, onClick: removeLink })
354
194
  ] }) : /* @__PURE__ */ jsxs("div", { role: "toolbar", "aria-label": labels.toolbar, className: "flex items-center gap-0.5", children: [
355
- MARK_ACTIONS.map((action) => /* @__PURE__ */ jsx(
195
+ actionsFor(MARK_ACTIONS, format).map((action) => /* @__PURE__ */ jsx(
356
196
  ToolbarButton,
357
197
  {
358
198
  icon: action.icon,
@@ -364,7 +204,7 @@ function BubbleToolbar({ editor, labels, linkRequest }) {
364
204
  action.id
365
205
  )),
366
206
  /* @__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: [
207
+ !(format === "markdown" && state.inTable) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 max-sm:hidden", children: [
368
208
  /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" }),
369
209
  BLOCK_ACTIONS.map((action, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
370
210
  (index === 3 || index === 6) && /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" }),
@@ -1 +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":[]}
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 * ⚠ `strategy` PHẢI là `absolute` (mặc định của Tiptap), không phải `fixed`. `BubbleMenu` của\n * `@tiptap/react` gán sẵn `position: absolute` cho phần tử lúc dựng, và Floating UI đo theo vị trí\n * ĐANG CÓ của phần tử: lần hiện ĐẦU TIÊN, nó tính toạ độ theo khung `relative` của trình soạn rồi plugin\n * áp chúng như toạ độ MÀN HÌNH (`fixed`) — bong bóng lệch đúng bằng khoảng cách từ khung tới góc màn\n * hình, tới lần cập nhật sau mới về chỗ. `3.5.0` để `fixed` và lỗi đó có thật (đo ở harness: liên kết\n * mở ở `left: -9px` thay vì sát chữ). `absolute` còn đi theo chữ khi một khung cuộn BÊN TRONG cuộn\n * (`DialogBody`, cột chi tiết) — `fixed` thì đứng yên, vì plugin chỉ nghe cuộn của `window`. Bị khung\n * cuộn cắt thì `flip` lật bong bóng xuống dưới vùng chọn.\n *\n * Trình soạn có thanh CỐ ĐỊNH (`toolbar=\"fixed\"`) vẫn dùng bong bóng này với `formatting={false}`:\n * nút định dạng đã ở thanh trên, nhưng ô sửa liên kết và phần xem trước liên kết chỉ có ở đây.\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 { Check, ExternalLink, Link2, Link2Off, Pencil } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { useAsRef } from \"../lib/use-as-ref.js\";\nimport type { EditorFormat } from \"./editor-types.js\";\nimport type { MarkdownEditorLabels } from \"./markdown-editor-constants.js\";\nimport { actionsFor, BLOCK_ACTIONS, MARK_ACTIONS } from \"./toolbar-actions.js\";\nimport { ToolbarButton } from \"./toolbar-button.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\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) ?? false;\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\nexport interface BubbleToolbarProps {\n editor: Editor;\n labels: MarkdownEditorLabels;\n format: EditorFormat;\n /**\n * `false` khi trình soạn có thanh cố định (`toolbar=\"fixed\"`): bong bóng không hiện nút định dạng\n * nữa (đã có ở thanh trên), chỉ còn phần LIÊN KẾT — xem trước, sửa, gỡ.\n */\n formatting: boolean;\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, format, formatting, 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 formattingRef = useAsRef(formatting);\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 if (!formattingRef.current) return false;\n return editorState.doc.textBetween(from, to, \" \").trim().length > 0;\n },\n [editingLinkRef, formattingRef]\n );\n\n const options = React.useMemo<BubbleMenuProps[\"options\"]>(\n () => ({\n // `absolute`, không `fixed` — lý do ở đầu file.\n strategy: \"absolute\",\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 {actionsFor(MARK_ACTIONS, format).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 {!(format === \"markdown\" && 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,28 @@
1
+ import * as React from 'react';
2
+ import { g as EditorBaseProps, f as EditorFormat, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-BMST7-VR.js';
3
+ import '@tiptap/core';
4
+ import './markdown-editor-constants.js';
5
+
6
+ /** Handle chung, đọc/ghi theo định dạng của trình soạn — mỗi lớp ngoài đặt tên lại cho cặp này. */
7
+ interface EditorCoreHandle extends EditorHandleBase {
8
+ getValue: () => string;
9
+ setValue: (value: string) => void;
10
+ }
11
+ interface EditorCoreProps<THandle> extends EditorBaseProps {
12
+ format: EditorFormat;
13
+ toolbar: EditorToolbarMode;
14
+ value?: string;
15
+ defaultValue?: string;
16
+ onChange?: (value: string) => void;
17
+ /** `data-slot` của khung ngoài — mỗi trình soạn một tên, để CSS/test của app nhắm đúng. */
18
+ slot: string;
19
+ handleRef?: React.Ref<THandle>;
20
+ /**
21
+ * Dựng handle công khai từ handle chung. Hàm ở cấp MODULE (định danh ổn định), và KHÔNG trải `core`
22
+ * ra (`{ ...core }`): trải là đọc getter `editor` đúng một lần — lúc nó còn `null`.
23
+ */
24
+ createHandle: (core: EditorCoreHandle) => THandle;
25
+ }
26
+ declare function EditorCore<THandle>({ format, toolbar, value, defaultValue, onChange, onSubmit, onFocus, onBlur, placeholder, editable, autoFocus, variant, mentions, onImageUpload, onImageUploadError, resolveImageSrc, extensions: extraExtensions, labels: labelsProp, name, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, className, contentClassName, slot, handleRef, createHandle }: EditorCoreProps<THandle>): React.JSX.Element;
27
+
28
+ export { EditorCore, type EditorCoreHandle, type EditorCoreProps };