@comitor/ui 3.4.0 → 3.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 (63) hide show
  1. package/README.md +19 -5
  2. package/dist/components/ui/item.d.ts +1 -1
  3. package/dist/editor/bubble-toolbar.d.ts +53 -0
  4. package/dist/editor/bubble-toolbar.js +231 -0
  5. package/dist/editor/bubble-toolbar.js.map +1 -0
  6. package/dist/editor/editor-core.d.ts +28 -0
  7. package/dist/editor/editor-core.js +354 -0
  8. package/dist/editor/editor-core.js.map +1 -0
  9. package/dist/editor/editor-types.d.ts +3 -0
  10. package/dist/editor/editor-types.js +1 -0
  11. package/dist/editor/editor-types.js.map +1 -0
  12. package/dist/editor/fixed-toolbar.d.ts +19 -0
  13. package/dist/editor/fixed-toolbar.js +106 -0
  14. package/dist/editor/fixed-toolbar.js.map +1 -0
  15. package/dist/editor/image-upload.d.ts +3 -0
  16. package/dist/editor/image-upload.js +137 -0
  17. package/dist/editor/image-upload.js.map +1 -0
  18. package/dist/editor/index.d.ts +6 -0
  19. package/dist/editor/index.js +11 -0
  20. package/dist/editor/index.js.map +1 -0
  21. package/dist/editor/markdown-editor-classes.d.ts +16 -0
  22. package/dist/editor/markdown-editor-classes.js +119 -0
  23. package/dist/editor/markdown-editor-classes.js.map +1 -0
  24. package/dist/editor/markdown-editor-constants.d.ts +98 -0
  25. package/dist/editor/markdown-editor-constants.js +6 -0
  26. package/dist/editor/markdown-editor-constants.js.map +1 -0
  27. package/dist/editor/markdown-editor.d.ts +48 -0
  28. package/dist/editor/markdown-editor.js +33 -0
  29. package/dist/editor/markdown-editor.js.map +1 -0
  30. package/dist/editor/markdown-extensions.d.ts +96 -0
  31. package/dist/editor/markdown-extensions.js +291 -0
  32. package/dist/editor/markdown-extensions.js.map +1 -0
  33. package/dist/editor/markdown-syntax.d.ts +87 -0
  34. package/dist/editor/markdown-syntax.js +58 -0
  35. package/dist/editor/markdown-syntax.js.map +1 -0
  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 +65 -0
  40. package/dist/editor/slash-commands.js +191 -0
  41. package/dist/editor/slash-commands.js.map +1 -0
  42. package/dist/editor/suggestion-menu.d.ts +38 -0
  43. package/dist/editor/suggestion-menu.js +144 -0
  44. package/dist/editor/suggestion-menu.js.map +1 -0
  45. package/dist/editor/suggestion-store.d.ts +47 -0
  46. package/dist/editor/suggestion-store.js +49 -0
  47. package/dist/editor/suggestion-store.js.map +1 -0
  48. package/dist/editor/toolbar-actions.d.ts +44 -0
  49. package/dist/editor/toolbar-actions.js +173 -0
  50. package/dist/editor/toolbar-actions.js.map +1 -0
  51. package/dist/editor/toolbar-button.d.ts +15 -0
  52. package/dist/editor/toolbar-button.js +47 -0
  53. package/dist/editor/toolbar-button.js.map +1 -0
  54. package/dist/editor-types-BMST7-VR.d.ts +171 -0
  55. package/dist/i18n/locale-provider.d.ts +1 -0
  56. package/dist/i18n/messages.d.ts +2 -0
  57. package/dist/i18n/messages.js +3 -0
  58. package/dist/i18n/messages.js.map +1 -1
  59. package/dist/i18n/namespaces/markdown-editor.d.ts +6 -0
  60. package/dist/i18n/namespaces/markdown-editor.js +133 -0
  61. package/dist/i18n/namespaces/markdown-editor.js.map +1 -0
  62. package/dist/index.d.ts +1 -0
  63. package/package.json +79 -2
@@ -0,0 +1,49 @@
1
+ import { exitSuggestion } from "@tiptap/suggestion";
2
+ class SuggestionStore {
3
+ constructor(pluginKey, toSelected) {
4
+ this.pluginKey = pluginKey;
5
+ this.toSelected = toSelected;
6
+ }
7
+ pluginKey;
8
+ toSelected;
9
+ snapshot = null;
10
+ listeners = /* @__PURE__ */ new Set();
11
+ /**
12
+ * Bàn phím trong lúc menu mở: ProseMirror nhận phím (con trỏ vẫn ở trình soạn), plugin chuyển cho
13
+ * hàm này. Menu đăng ký nó khi mount. Trả `true` = đã xử lý, ProseMirror không làm gì thêm.
14
+ */
15
+ keyHandler = null;
16
+ subscribe = (listener) => {
17
+ this.listeners.add(listener);
18
+ return () => {
19
+ this.listeners.delete(listener);
20
+ };
21
+ };
22
+ getSnapshot = () => this.snapshot;
23
+ getServerSnapshot = () => null;
24
+ set(next) {
25
+ this.snapshot = next;
26
+ for (const listener of this.listeners) listener();
27
+ }
28
+ fromProps(props) {
29
+ return {
30
+ items: props.items,
31
+ query: props.query,
32
+ loading: props.loading,
33
+ clientRect: () => props.clientRect?.() ?? null,
34
+ select: (item) => props.command(this.toSelected(item)),
35
+ exit: () => exitSuggestion(props.editor.view, this.pluginKey)
36
+ };
37
+ }
38
+ /** Truyền thẳng vào `render` của `Suggestion` / `Mention.suggestion`. */
39
+ render = () => ({
40
+ onStart: (props) => this.set(this.fromProps(props)),
41
+ onUpdate: (props) => this.set(this.fromProps(props)),
42
+ onExit: () => this.set(null),
43
+ onKeyDown: ({ event }) => this.keyHandler?.(event) ?? false
44
+ });
45
+ }
46
+ export {
47
+ SuggestionStore
48
+ };
49
+ //# sourceMappingURL=suggestion-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/editor/suggestion-store.ts"],"sourcesContent":["/**\n * Cầu nối giữa plugin gợi ý của Tiptap (`@tiptap/suggestion` — vòng đời onStart/onUpdate/onExit,\n * sống trong ProseMirror) và một component React (`SuggestionMenu`).\n *\n * Không dùng `ReactRenderer` + `props.mount()` như ví dụ của Tiptap: cách đó gắn danh sách vào\n * `document.body` ngoài mọi lớp Radix, và trong một `Dialog` (form tạo việc) thì Radix coi cú bấm\n * vào danh sách là bấm RA NGOÀI hộp thoại — hộp thoại đóng ngay giữa lúc chọn khối. Ở đây plugin\n * chỉ ĐẨY trạng thái vào một kho nhỏ; menu là cây React bình thường của trình soạn, mở bằng `Popover`\n * của gói — một lớp Radix lồng đúng thứ bậc, nên Escape đóng menu chứ không đóng hộp thoại.\n */\n\nimport type { PluginKey } from \"@tiptap/pm/state\";\nimport { exitSuggestion, type SuggestionOptions, type SuggestionProps } from \"@tiptap/suggestion\";\n\nexport interface SuggestionSnapshot<Item> {\n items: Item[];\n /** Chữ gõ sau ký tự kích hoạt */\n query: string;\n /** `true` khi `items()` bất đồng bộ chưa trả về */\n loading: boolean;\n /** Khung của đoạn đang gõ (`/que…`) — neo cho menu. `null` khi plugin chưa vẽ xong decoration. */\n clientRect: () => DOMRect | null;\n select: (item: Item) => void;\n /** Thoát hẳn chế độ gợi ý (gỡ decoration) — dùng khi menu bị đóng từ PHÍA MENU (Escape, bấm ra ngoài). */\n exit: () => void;\n}\n\nexport class SuggestionStore<Item, Selected = Item> {\n private snapshot: SuggestionSnapshot<Item> | null = null;\n private readonly listeners = new Set<() => void>();\n\n /**\n * Bàn phím trong lúc menu mở: ProseMirror nhận phím (con trỏ vẫn ở trình soạn), plugin chuyển cho\n * hàm này. Menu đăng ký nó khi mount. Trả `true` = đã xử lý, ProseMirror không làm gì thêm.\n */\n keyHandler: ((event: KeyboardEvent) => boolean) | null = null;\n\n constructor(\n private readonly pluginKey: PluginKey,\n private readonly toSelected: (item: Item) => Selected\n ) {}\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = () => this.snapshot;\n\n getServerSnapshot = () => null;\n\n private set(next: SuggestionSnapshot<Item> | null) {\n this.snapshot = next;\n for (const listener of this.listeners) listener();\n }\n\n private fromProps(props: SuggestionProps<Item, Selected>): SuggestionSnapshot<Item> {\n return {\n items: props.items,\n query: props.query,\n loading: props.loading,\n clientRect: () => props.clientRect?.() ?? null,\n select: (item) => props.command(this.toSelected(item)),\n exit: () => exitSuggestion(props.editor.view, this.pluginKey)\n };\n }\n\n /** Truyền thẳng vào `render` của `Suggestion` / `Mention.suggestion`. */\n render: NonNullable<SuggestionOptions<Item, Selected>[\"render\"]> = () => ({\n onStart: (props) => this.set(this.fromProps(props)),\n onUpdate: (props) => this.set(this.fromProps(props)),\n onExit: () => this.set(null),\n onKeyDown: ({ event }) => this.keyHandler?.(event) ?? false\n });\n}\n"],"mappings":"AAYA,SAAS,sBAAoE;AAetE,MAAM,gBAAuC;AAAA,EAUlD,YACmB,WACA,YACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EAXX,WAA4C;AAAA,EACnC,YAAY,oBAAI,IAAgB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjD,aAAyD;AAAA,EAOzD,YAAY,CAAC,aAAyB;AACpC,SAAK,UAAU,IAAI,QAAQ;AAC3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,cAAc,MAAM,KAAK;AAAA,EAEzB,oBAAoB,MAAM;AAAA,EAElB,IAAI,MAAuC;AACjD,SAAK,WAAW;AAChB,eAAW,YAAY,KAAK,UAAW,UAAS;AAAA,EAClD;AAAA,EAEQ,UAAU,OAAkE;AAClF,WAAO;AAAA,MACL,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,MACf,YAAY,MAAM,MAAM,aAAa,KAAK;AAAA,MAC1C,QAAQ,CAAC,SAAS,MAAM,QAAQ,KAAK,WAAW,IAAI,CAAC;AAAA,MACrD,MAAM,MAAM,eAAe,MAAM,OAAO,MAAM,KAAK,SAAS;AAAA,IAC9D;AAAA,EACF;AAAA;AAAA,EAGA,SAAmE,OAAO;AAAA,IACxE,SAAS,CAAC,UAAU,KAAK,IAAI,KAAK,UAAU,KAAK,CAAC;AAAA,IAClD,UAAU,CAAC,UAAU,KAAK,IAAI,KAAK,UAAU,KAAK,CAAC;AAAA,IACnD,QAAQ,MAAM,KAAK,IAAI,IAAI;AAAA,IAC3B,WAAW,CAAC,EAAE,MAAM,MAAM,KAAK,aAAa,KAAK,KAAK;AAAA,EACxD;AACF;","names":[]}
@@ -0,0 +1,44 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import { LucideIcon } from 'lucide-react';
3
+ import { f as EditorFormat } from '../editor-types-BMST7-VR.js';
4
+ import { MarkdownEditorLabels } from './markdown-editor-constants.js';
5
+
6
+ /**
7
+ * Hành động của thanh công cụ — MỘT bảng cho cả thanh nổi (`bubble-toolbar.tsx`) lẫn thanh cố định
8
+ * (`fixed-toolbar.tsx`). Hai bảng riêng là hai nơi để lệch: nút In đậm ở thanh này có phím tắt, ở
9
+ * thanh kia thì không.
10
+ *
11
+ * File thuần (không `"use client"`): dữ liệu — icon là component, `run` là hàm. Chữ hiển thị không
12
+ * nằm ở đây; `label` là KHOÁ của namespace `markdownEditor`.
13
+ */
14
+
15
+ /** Khoá nhãn mang CHUỖI (loại `slashKeywords` và hàm `taskCheckbox`). */
16
+ type TextLabelKey = {
17
+ [K in keyof MarkdownEditorLabels]: MarkdownEditorLabels[K] extends string ? K : never;
18
+ }[keyof MarkdownEditorLabels];
19
+ interface ToolbarAction {
20
+ id: string;
21
+ icon: LucideIcon;
22
+ label: TextLabelKey;
23
+ /** Phím tắt cho tooltip — cú pháp của `KeyboardHint` (`mod+b`). Mặc định của Tiptap. */
24
+ keys?: string;
25
+ /**
26
+ * Chỉ có ở những định dạng này. Bỏ trống = có ở cả hai. Gạch chân chỉ ở HTML: markdown không có cú
27
+ * pháp cho nó (lý do ở `createSchemaExtensions`).
28
+ */
29
+ formats?: readonly EditorFormat[];
30
+ /** Có thì nút là nút BẬT/TẮT (`aria-pressed`); không có thì là nút lệnh. */
31
+ isActive?: (editor: Editor) => boolean;
32
+ /** `false` = nút bị khoá (hoàn tác khi chưa có gì để hoàn tác, chèn bảng khi đang ở trong bảng). */
33
+ canRun?: (editor: Editor) => boolean;
34
+ run: (editor: Editor) => void;
35
+ }
36
+ declare const HISTORY_ACTIONS: readonly ToolbarAction[];
37
+ declare const MARK_ACTIONS: readonly ToolbarAction[];
38
+ declare const BLOCK_ACTIONS: readonly ToolbarAction[];
39
+ /** Chèn — chỉ thanh cố định có (thanh nổi chỉ hiện khi đã bôi chọn chữ, không phải lúc để chèn khối). */
40
+ declare const INSERT_ACTIONS: readonly ToolbarAction[];
41
+ /** Các hành động có ở `format`. */
42
+ declare function actionsFor(actions: readonly ToolbarAction[], format: EditorFormat): ToolbarAction[];
43
+
44
+ export { BLOCK_ACTIONS, HISTORY_ACTIONS, INSERT_ACTIONS, MARK_ACTIONS, type TextLabelKey, type ToolbarAction, actionsFor };
@@ -0,0 +1,173 @@
1
+ import {
2
+ Bold,
3
+ Code,
4
+ Heading1,
5
+ Heading2,
6
+ Heading3,
7
+ Italic,
8
+ List,
9
+ ListChecks,
10
+ ListOrdered,
11
+ Minus,
12
+ Redo2,
13
+ SquareCode,
14
+ Strikethrough,
15
+ Table,
16
+ TextQuote,
17
+ Underline,
18
+ Undo2
19
+ } from "lucide-react";
20
+ const HISTORY_ACTIONS = [
21
+ {
22
+ id: "undo",
23
+ icon: Undo2,
24
+ label: "undo",
25
+ keys: "mod+z",
26
+ canRun: (editor) => editor.can().undo(),
27
+ run: (editor) => editor.chain().focus().undo().run()
28
+ },
29
+ {
30
+ id: "redo",
31
+ icon: Redo2,
32
+ label: "redo",
33
+ keys: "mod+shift+z",
34
+ canRun: (editor) => editor.can().redo(),
35
+ run: (editor) => editor.chain().focus().redo().run()
36
+ }
37
+ ];
38
+ const MARK_ACTIONS = [
39
+ {
40
+ id: "bold",
41
+ icon: Bold,
42
+ label: "bold",
43
+ keys: "mod+b",
44
+ isActive: (editor) => editor.isActive("bold"),
45
+ run: (editor) => editor.chain().focus().toggleBold().run()
46
+ },
47
+ {
48
+ id: "italic",
49
+ icon: Italic,
50
+ label: "italic",
51
+ keys: "mod+i",
52
+ isActive: (editor) => editor.isActive("italic"),
53
+ run: (editor) => editor.chain().focus().toggleItalic().run()
54
+ },
55
+ {
56
+ id: "underline",
57
+ icon: Underline,
58
+ label: "underline",
59
+ keys: "mod+u",
60
+ formats: ["html"],
61
+ isActive: (editor) => editor.isActive("underline"),
62
+ run: (editor) => editor.chain().focus().toggleUnderline().run()
63
+ },
64
+ {
65
+ id: "strike",
66
+ icon: Strikethrough,
67
+ label: "strike",
68
+ keys: "mod+shift+s",
69
+ isActive: (editor) => editor.isActive("strike"),
70
+ run: (editor) => editor.chain().focus().toggleStrike().run()
71
+ },
72
+ {
73
+ id: "code",
74
+ icon: Code,
75
+ label: "code",
76
+ keys: "mod+e",
77
+ isActive: (editor) => editor.isActive("code"),
78
+ run: (editor) => editor.chain().focus().toggleCode().run()
79
+ }
80
+ ];
81
+ const BLOCK_ACTIONS = [
82
+ {
83
+ id: "heading1",
84
+ icon: Heading1,
85
+ label: "heading1",
86
+ keys: "mod+alt+1",
87
+ isActive: (editor) => editor.isActive("heading", { level: 1 }),
88
+ run: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run()
89
+ },
90
+ {
91
+ id: "heading2",
92
+ icon: Heading2,
93
+ label: "heading2",
94
+ keys: "mod+alt+2",
95
+ isActive: (editor) => editor.isActive("heading", { level: 2 }),
96
+ run: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run()
97
+ },
98
+ {
99
+ id: "heading3",
100
+ icon: Heading3,
101
+ label: "heading3",
102
+ keys: "mod+alt+3",
103
+ isActive: (editor) => editor.isActive("heading", { level: 3 }),
104
+ run: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run()
105
+ },
106
+ {
107
+ id: "bulletList",
108
+ icon: List,
109
+ label: "bulletList",
110
+ keys: "mod+shift+8",
111
+ isActive: (editor) => editor.isActive("bulletList"),
112
+ run: (editor) => editor.chain().focus().toggleBulletList().run()
113
+ },
114
+ {
115
+ id: "orderedList",
116
+ icon: ListOrdered,
117
+ label: "orderedList",
118
+ keys: "mod+shift+7",
119
+ isActive: (editor) => editor.isActive("orderedList"),
120
+ run: (editor) => editor.chain().focus().toggleOrderedList().run()
121
+ },
122
+ {
123
+ id: "taskList",
124
+ icon: ListChecks,
125
+ label: "taskList",
126
+ keys: "mod+shift+9",
127
+ isActive: (editor) => editor.isActive("taskList"),
128
+ run: (editor) => editor.chain().focus().toggleTaskList().run()
129
+ },
130
+ {
131
+ id: "blockquote",
132
+ icon: TextQuote,
133
+ label: "blockquote",
134
+ keys: "mod+shift+b",
135
+ isActive: (editor) => editor.isActive("blockquote"),
136
+ run: (editor) => editor.chain().focus().toggleBlockquote().run()
137
+ },
138
+ {
139
+ id: "codeBlock",
140
+ icon: SquareCode,
141
+ label: "codeBlock",
142
+ keys: "mod+alt+c",
143
+ isActive: (editor) => editor.isActive("codeBlock"),
144
+ run: (editor) => editor.chain().focus().toggleCodeBlock().run()
145
+ }
146
+ ];
147
+ const INSERT_ACTIONS = [
148
+ {
149
+ id: "table",
150
+ icon: Table,
151
+ label: "table",
152
+ // Không lồng bảng trong bảng: GFM không có, và một bảng trong ô bảng hiếm khi là điều người soạn muốn.
153
+ canRun: (editor) => !editor.isActive("table"),
154
+ run: (editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
155
+ },
156
+ {
157
+ id: "divider",
158
+ icon: Minus,
159
+ label: "divider",
160
+ run: (editor) => editor.chain().focus().setHorizontalRule().run()
161
+ }
162
+ ];
163
+ function actionsFor(actions, format) {
164
+ return actions.filter((action) => !action.formats || action.formats.includes(format));
165
+ }
166
+ export {
167
+ BLOCK_ACTIONS,
168
+ HISTORY_ACTIONS,
169
+ INSERT_ACTIONS,
170
+ MARK_ACTIONS,
171
+ actionsFor
172
+ };
173
+ //# sourceMappingURL=toolbar-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/editor/toolbar-actions.ts"],"sourcesContent":["/**\n * Hành động của thanh công cụ — MỘT bảng cho cả thanh nổi (`bubble-toolbar.tsx`) lẫn thanh cố định\n * (`fixed-toolbar.tsx`). Hai bảng riêng là hai nơi để lệch: nút In đậm ở thanh này có phím tắt, ở\n * thanh kia thì không.\n *\n * File thuần (không `\"use client\"`): dữ liệu — icon là component, `run` là hàm. Chữ hiển thị không\n * nằm ở đây; `label` là KHOÁ của namespace `markdownEditor`.\n */\n\nimport type { Editor } from \"@tiptap/core\";\nimport {\n Bold,\n Code,\n Heading1,\n Heading2,\n Heading3,\n Italic,\n List,\n ListChecks,\n ListOrdered,\n type LucideIcon,\n Minus,\n Redo2,\n SquareCode,\n Strikethrough,\n Table,\n TextQuote,\n Underline,\n Undo2\n} from \"lucide-react\";\nimport type { EditorFormat } from \"./editor-types.js\";\nimport type { MarkdownEditorLabels } from \"./markdown-editor-constants.js\";\n\n/** Khoá nhãn mang CHUỖI (loại `slashKeywords` và hàm `taskCheckbox`). */\nexport type TextLabelKey = {\n [K in keyof MarkdownEditorLabels]: MarkdownEditorLabels[K] extends string ? K : never;\n}[keyof MarkdownEditorLabels];\n\nexport interface 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 của Tiptap. */\n keys?: string;\n /**\n * Chỉ có ở những định dạng này. Bỏ trống = có ở cả hai. Gạch chân chỉ ở HTML: markdown không có cú\n * pháp cho nó (lý do ở `createSchemaExtensions`).\n */\n formats?: readonly EditorFormat[];\n /** Có thì nút là nút BẬT/TẮT (`aria-pressed`); không có thì là nút lệnh. */\n isActive?: (editor: Editor) => boolean;\n /** `false` = nút bị khoá (hoàn tác khi chưa có gì để hoàn tác, chèn bảng khi đang ở trong bảng). */\n canRun?: (editor: Editor) => boolean;\n run: (editor: Editor) => void;\n}\n\nexport const HISTORY_ACTIONS: readonly ToolbarAction[] = [\n {\n id: \"undo\",\n icon: Undo2,\n label: \"undo\",\n keys: \"mod+z\",\n canRun: (editor) => editor.can().undo(),\n run: (editor) => editor.chain().focus().undo().run()\n },\n {\n id: \"redo\",\n icon: Redo2,\n label: \"redo\",\n keys: \"mod+shift+z\",\n canRun: (editor) => editor.can().redo(),\n run: (editor) => editor.chain().focus().redo().run()\n }\n];\n\nexport const 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: \"underline\",\n icon: Underline,\n label: \"underline\",\n keys: \"mod+u\",\n formats: [\"html\"],\n isActive: (editor) => editor.isActive(\"underline\"),\n run: (editor) => editor.chain().focus().toggleUnderline().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. Thanh nổi ẩn nhóm này ở màn hẹp, và ở trong bảng của trình soạn MARKDOWN (ô bảng GFM chỉ\n * chứa một đoạn văn — xem `ComitorTableCell`); ô bảng HTML chứa được khối nên nhóm vẫn hiện.\n */\nexport const 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\n/** Chèn — chỉ thanh cố định có (thanh nổi chỉ hiện khi đã bôi chọn chữ, không phải lúc để chèn khối). */\nexport const INSERT_ACTIONS: readonly ToolbarAction[] = [\n {\n id: \"table\",\n icon: Table,\n label: \"table\",\n // Không lồng bảng trong bảng: GFM không có, và một bảng trong ô bảng hiếm khi là điều người soạn muốn.\n canRun: (editor) => !editor.isActive(\"table\"),\n run: (editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()\n },\n {\n id: \"divider\",\n icon: Minus,\n label: \"divider\",\n run: (editor) => editor.chain().focus().setHorizontalRule().run()\n }\n];\n\n/** Các hành động có ở `format`. */\nexport function actionsFor(actions: readonly ToolbarAction[], format: EditorFormat): ToolbarAction[] {\n return actions.filter((action) => !action.formats || action.formats.includes(format));\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+ import { LucideIcon } from 'lucide-react';
3
+
4
+ interface ToolbarButtonProps {
5
+ icon: LucideIcon;
6
+ label: string;
7
+ keys?: string;
8
+ pressed?: boolean;
9
+ disabled?: boolean;
10
+ onClick: () => void;
11
+ className?: string;
12
+ }
13
+ declare function ToolbarButton({ icon: Icon, label, keys, pressed, disabled, onClick, className }: ToolbarButtonProps): React.JSX.Element;
14
+
15
+ export { ToolbarButton, type ToolbarButtonProps };
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { KeyboardHint } from "../components/composite/keyboard-hint.js";
4
+ import { Toggle } from "../components/ui/toggle.js";
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
6
+ import { cn } from "../lib/cn.js";
7
+ const BUTTON_CLASS = "h-7 min-w-7 px-1.5";
8
+ function ToolbarButton({ icon: Icon, label, keys, pressed, disabled, onClick, className }) {
9
+ const button = pressed === void 0 ? /* @__PURE__ */ jsx(
10
+ "button",
11
+ {
12
+ type: "button",
13
+ "aria-label": label,
14
+ disabled,
15
+ onMouseDown: (event) => event.preventDefault(),
16
+ onClick,
17
+ className: cn(
18
+ "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 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
19
+ BUTTON_CLASS,
20
+ className
21
+ ),
22
+ children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" })
23
+ }
24
+ ) : /* @__PURE__ */ jsx(
25
+ Toggle,
26
+ {
27
+ "aria-label": label,
28
+ pressed,
29
+ disabled,
30
+ onPressedChange: onClick,
31
+ onMouseDown: (event) => event.preventDefault(),
32
+ className: cn(BUTTON_CLASS, className),
33
+ children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" })
34
+ }
35
+ );
36
+ return /* @__PURE__ */ jsxs(Tooltip, { delayDuration: 400, children: [
37
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: disabled ? /* @__PURE__ */ jsx("span", { className: "inline-flex", children: button }) : button }),
38
+ /* @__PURE__ */ jsxs(TooltipContent, { side: "top", sideOffset: 6, className: "flex items-center gap-2", children: [
39
+ label,
40
+ keys && /* @__PURE__ */ jsx(KeyboardHint, { keys, compact: true })
41
+ ] })
42
+ ] });
43
+ }
44
+ export {
45
+ ToolbarButton
46
+ };
47
+ //# sourceMappingURL=toolbar-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/editor/toolbar-button.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Nút của thanh công cụ — dùng chung cho thanh nổi và thanh cố định.\n *\n * Nhỏ hơn `Toggle size=\"sm\"` một bậc (28px): mười mấy nút phải nằm gọn trên một dòng ở màn laptop.\n * Mousedown bị chặn để focus Ở LẠI vùng soạn — mất focus là mất vùng chọn mà nút sắp áp định dạng lên.\n * Nút có `pressed` là nút BẬT/TẮT (`Toggle`, `aria-pressed`); không có là nút lệnh.\n */\n\nimport type { LucideIcon } from \"lucide-react\";\nimport { KeyboardHint } from \"../components/composite/keyboard-hint.js\";\nimport { Toggle } from \"../components/ui/toggle.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\n\nconst BUTTON_CLASS = \"h-7 min-w-7 px-1.5\";\n\nexport interface ToolbarButtonProps {\n icon: LucideIcon;\n label: string;\n keys?: string;\n pressed?: boolean;\n disabled?: boolean;\n onClick: () => void;\n className?: string;\n}\n\nexport function ToolbarButton({ icon: Icon, label, keys, pressed, disabled, onClick, className }: ToolbarButtonProps) {\n const button =\n pressed === undefined ? (\n <button\n type=\"button\"\n aria-label={label}\n disabled={disabled}\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 disabled:pointer-events-none disabled:opacity-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 disabled={disabled}\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 {/*\n Nút đang khoá không phát sự kiện chuột, nên tooltip của nó neo vào một `span` bọc ngoài. Nút\n bình thường thì CHÍNH nó là trigger — để tooltip cũng hiện khi focus bằng bàn phím.\n */}\n <TooltipTrigger asChild>{disabled ? <span className=\"inline-flex\">{button}</span> : 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"],"mappings":";AA2CQ,cAsBF,YAtBE;AAhCR,SAAS,oBAAoB;AAC7B,SAAS,cAAc;AACvB,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AAEnB,MAAM,eAAe;AAYd,SAAS,cAAc,EAAE,MAAM,MAAM,OAAO,MAAM,SAAS,UAAU,SAAS,UAAU,GAAuB;AACpH,QAAM,SACJ,YAAY,SACV;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ;AAAA,MACA,aAAa,CAAC,UAAU,MAAM,eAAe;AAAA,MAC7C;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,QAAK,eAAY,QAAO;AAAA;AAAA,EAC3B,IAEA;AAAA,IAAC;AAAA;AAAA,MACC,cAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA,MACjB,aAAa,CAAC,UAAU,MAAM,eAAe;AAAA,MAC7C,WAAW,GAAG,cAAc,SAAS;AAAA,MAErC,8BAAC,QAAK,eAAY,QAAO;AAAA;AAAA,EAC3B;AAGJ,SACE,qBAAC,WAAQ,eAAe,KAKtB;AAAA,wBAAC,kBAAe,SAAO,MAAE,qBAAW,oBAAC,UAAK,WAAU,eAAe,kBAAO,IAAU,QAAO;AAAA,IAC3F,qBAAC,kBAAe,MAAK,OAAM,YAAY,GAAG,WAAU,2BACjD;AAAA;AAAA,MACA,QAAQ,oBAAC,gBAAa,MAAY,SAAO,MAAC;AAAA,OAC7C;AAAA,KACF;AAEJ;","names":[]}
@@ -0,0 +1,171 @@
1
+ import { Extension, Editor, AnyExtension } from '@tiptap/core';
2
+ import { MarkdownEditorLabels } from './editor/markdown-editor-constants.js';
3
+
4
+ /**
5
+ * Ả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.
6
+ *
7
+ * 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ý
8
+ * — 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` để
9
+ * LƯU. Cùng nguyên tắc với `/uploader` (`XHRUpload` về endpoint của app).
10
+ *
11
+ * ── ẢNH ĐANG TẢI LÀ DECORATION, KHÔNG PHẢI NODE ────────────────────────────────────────────────
12
+ * 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
13
+ * (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
14
+ * MỘT lần, khi app đã trả `src`.
15
+ *
16
+ * 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
17
+ * 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
18
+ * 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ờ đó
19
+ * 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:
20
+ * lịch sử chỉ có một bước, "chèn ảnh thật".
21
+ */
22
+
23
+ /** Thứ `onImageUpload` trả về: `src` để LƯU, hoặc kèm `alt`/`title`. */
24
+ type MarkdownEditorImageUploadResult = string | {
25
+ src: string;
26
+ alt?: string;
27
+ title?: string;
28
+ };
29
+ interface ImageUploadHandlers {
30
+ upload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;
31
+ onError: (error: unknown, file: File) => void;
32
+ /** Đổi `src` lưu → `src` hiện (`resolveImageSrc`), để tải trước ảnh thật trước khi chèn. */
33
+ resolveSrc?: (src: string) => string;
34
+ /** Nhãn trợ năng của ảnh đang tải (`role="img"` của widget). */
35
+ uploadingLabel: string;
36
+ /** Định dạng lưu — ô bảng markdown không nhận ảnh (xem `blockInsertPosition`). Mặc định `markdown`. */
37
+ format?: EditorFormat;
38
+ }
39
+ /** Ảnh đang tải của MỘT trình soạn — `MarkdownEditorHandle.isUploading()` đọc nó. */
40
+ type PendingUploads = Set<string>;
41
+ /** 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. */
42
+ declare function imageFilesFrom(data: DataTransfer | null): File[];
43
+ /**
44
+ * 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
45
+ * 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 —
46
+ * 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.
47
+ */
48
+ declare function insertImageFiles(editor: Editor, files: readonly File[], position: number | undefined, handlers: ImageUploadHandlers, pending: PendingUploads): void;
49
+ /**
50
+ * Bắt ảnh khi DÁN và khi THẢ, và giữ các widget đang tải. Không có `onImageUpload` thì plugin đứng
51
+ * 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
52
+ * (không có chỗ nào để lưu nó).
53
+ *
54
+ * 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.
55
+ */
56
+ declare function createImageUploadExtension(handlers: {
57
+ readonly current: ImageUploadHandlers;
58
+ }, pending: PendingUploads): Extension;
59
+
60
+ /**
61
+ * Kiểu dùng CHUNG cho hai trình soạn của entry — `MarkdownEditor` (lưu markdown) và `RichTextEditor`
62
+ * (lưu HTML). Chỉ kiểu, không mã chạy.
63
+ *
64
+ * Tên `MarkdownEditor…` của các kiểu nhắc tên / biến thể là tên đã publish ở `3.5.0`, trước khi có
65
+ * trình soạn thứ hai. Giữ nguyên (đổi tên là breaking) — cả hai trình soạn dùng chung chúng.
66
+ */
67
+
68
+ /** Định dạng LƯU — thứ `value`/`onChange` mang. Quyết định cả lược đồ (xem `createSchemaExtensions`). */
69
+ type EditorFormat = "markdown" | "html";
70
+ /**
71
+ * Thanh công cụ:
72
+ * · `bubble` — kiểu Linear: không chiếm chỗ, định dạng hiện ngay chỗ bôi chọn;
73
+ * · `fixed` — thanh dính đầu khung soạn, mọi thao tác luôn thấy được: kiểu CMS, cho người soạn
74
+ * không thuộc phím tắt hay cú pháp gõ tắt. Ở chế độ này bong bóng chỉ còn phần LIÊN KẾT.
75
+ */
76
+ type EditorToolbarMode = "bubble" | "fixed";
77
+ /** Một người trong danh sách `@`. */
78
+ interface MarkdownEditorMentionItem {
79
+ /** Định danh LƯU vào nội dung — với module Comitor là id người dùng của Account */
80
+ id: string;
81
+ /** Tên hiển thị lúc chọn — cũng là ảnh chụp tên ghi kèm trong nội dung */
82
+ label: string;
83
+ /** Dòng phụ mờ bên phải (email, chức danh) — cũng được dùng để lọc */
84
+ description?: string;
85
+ avatarUrl?: string | null;
86
+ }
87
+ /**
88
+ * Nguồn danh sách `@`:
89
+ * · MẢNG — gói tự lọc theo tên/dòng phụ, không dấu (cùng luật `Combobox`), tối đa 50 người;
90
+ * · HÀM — app tự tìm (gọi API danh bạ…). `signal` bị huỷ khi người dùng gõ tiếp; gói chờ 150ms
91
+ * sau phím cuối mới gọi. (Kiểu nguồn xét LÚC MOUNT — độ trễ là tuỳ chọn dựng của plugin.)
92
+ */
93
+ type MarkdownEditorMentionSource = readonly MarkdownEditorMentionItem[] | ((query: string, options: {
94
+ signal: AbortSignal;
95
+ }) => readonly MarkdownEditorMentionItem[] | Promise<readonly MarkdownEditorMentionItem[]>);
96
+ interface MarkdownEditorMentions {
97
+ items: MarkdownEditorMentionSource;
98
+ /**
99
+ * Tên SỐNG theo id, dùng lúc VẼ. Nội dung giữ tên chụp lúc nhắc; người đó đổi tên ở Account thì
100
+ * hàm này cho tên mới mà không phải sửa nội dung. `undefined` → rơi về tên chụp.
101
+ */
102
+ resolveLabel?: (id: string) => string | undefined;
103
+ }
104
+ type MarkdownEditorVariant = "outline" | "ghost";
105
+ /** Phần điều khiển chung của hai handle — mỗi trình soạn thêm cặp đọc/ghi theo định dạng của nó. */
106
+ interface EditorHandleBase {
107
+ focus: (position?: "start" | "end") => void;
108
+ /** Xoá trắng — vd sau khi gửi bình luận. Có gọi `onChange("")`. */
109
+ clear: () => void;
110
+ isEmpty: () => boolean;
111
+ /**
112
+ * Còn ảnh đang tải lên không. Ảnh chưa tải xong KHÔNG có trong giá trị — nút Gửi nên chờ (hoặc
113
+ * khoá) khi hàm này trả `true`, nếu không nội dung đi mà thiếu ảnh.
114
+ */
115
+ isUploading: () => boolean;
116
+ /** Bản Tiptap bên dưới — lối thoát cho nhu cầu gói chưa phủ. `null` trước lần render đầu ở client. */
117
+ readonly editor: Editor | null;
118
+ }
119
+ /** Prop CHUNG của hai trình soạn — mọi thứ trừ cặp giá trị (`value`/`defaultValue`/`onChange`) và `ref`. */
120
+ interface EditorBaseProps {
121
+ /**
122
+ * ⌘/Ctrl+Enter (ngoài danh sách việc — ở đó phím này tick mục). Cho ô bình luận; không truyền thì
123
+ * phím giữ nghĩa mặc định.
124
+ */
125
+ onSubmit?: () => void;
126
+ onFocus?: () => void;
127
+ onBlur?: () => void;
128
+ /** Chữ mờ khi rỗng. Mặc định theo `ComitorLocaleProvider`. */
129
+ placeholder?: string;
130
+ /** `false` = chế độ ĐỌC: không con trỏ, không menu, liên kết bấm mở được. Đổi lúc chạy được. */
131
+ editable?: boolean;
132
+ autoFocus?: boolean;
133
+ /**
134
+ * `outline` (mặc định) — khung như `Textarea`, cho biểu mẫu và ô bình luận.
135
+ * `ghost` — không khung, không đệm: tài liệu nằm thẳng trên trang, kiểu mô tả việc của Linear.
136
+ */
137
+ variant?: MarkdownEditorVariant;
138
+ /** Bật `@`. Không truyền thì gõ `@` là chữ thường — nhưng nhắc tên CÓ SẴN trong nội dung vẫn hiện đúng. */
139
+ mentions?: MarkdownEditorMentions;
140
+ /**
141
+ * Bật ảnh: dán, kéo thả, mục "Ảnh" của menu `/` (và nút ảnh của thanh cố định). Nhận `File`, trả
142
+ * `src` để LƯU — khoá trong kho, hoặc một route ổn định của app; KHÔNG trả URL đã ký có hạn (xem
143
+ * `resolveImageSrc`).
144
+ */
145
+ onImageUpload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;
146
+ /** Lỗi tải ảnh. Mặc định: toast `imageUploadFailed` (cần `Toaster` ở gốc app). */
147
+ onImageUploadError?: (error: unknown, file: File) => void;
148
+ /** Đổi `src` ĐÃ LƯU thành địa chỉ HIỂN THỊ (vd URL đã ký) lúc vẽ. Giá trị lưu vẫn giữ `src` gốc. */
149
+ resolveImageSrc?: (src: string) => string;
150
+ /**
151
+ * Extension Tiptap thêm vào (vd node `KEY-n` của module). Chỉ đọc LÚC MOUNT — đổi danh sách là
152
+ * phải dựng lại trình soạn (mất lịch sử hoàn tác), nên muốn đổi thì đổi `key` của component.
153
+ * Phải cùng phiên bản `@tiptap/*` với gói.
154
+ */
155
+ extensions?: readonly AnyExtension[];
156
+ labels?: Partial<MarkdownEditorLabels>;
157
+ /** Có `name` thì kèm một `<input type="hidden">` mang giá trị — để trình soạn nằm trong `<form>` thật. */
158
+ name?: string;
159
+ /** Đặt lên vùng soạn. Có `<label for={id}>` trong trang thì gói tự nối nó làm tên trợ năng. */
160
+ id?: string;
161
+ "aria-label"?: string;
162
+ "aria-labelledby"?: string;
163
+ "aria-describedby"?: string;
164
+ "aria-invalid"?: boolean | "true" | "false";
165
+ "aria-required"?: boolean | "true" | "false";
166
+ className?: string;
167
+ /** Lớp thêm cho vùng soạn (phần tử ProseMirror) — vd `min-h-40`, `max-h-96 overflow-y-auto`. */
168
+ contentClassName?: string;
169
+ }
170
+
171
+ export { type EditorHandleBase as E, type ImageUploadHandlers as I, type MarkdownEditorImageUploadResult as M, type PendingUploads as P, type EditorToolbarMode as a, type MarkdownEditorMentionItem as b, type MarkdownEditorMentionSource as c, type MarkdownEditorMentions as d, type MarkdownEditorVariant as e, type EditorFormat as f, type EditorBaseProps as g, createImageUploadExtension as h, imageFilesFrom as i, insertImageFiles as j };
@@ -13,6 +13,7 @@ import './namespaces/route-tabs.js';
13
13
  import './namespaces/rating.js';
14
14
  import './namespaces/pagination.js';
15
15
  import './namespaces/mention.js';
16
+ import '../editor/markdown-editor-constants.js';
16
17
  import './namespaces/inline-edit.js';
17
18
  import './namespaces/filter-chips.js';
18
19
  import '../uploader/file-upload-constants.js';
@@ -8,6 +8,7 @@ import { RouteTabsMessages } from './namespaces/route-tabs.js';
8
8
  import { RatingMessages } from './namespaces/rating.js';
9
9
  import { PaginationMessages } from './namespaces/pagination.js';
10
10
  import { MentionMessages } from './namespaces/mention.js';
11
+ import { MarkdownEditorLabels } from '../editor/markdown-editor-constants.js';
11
12
  import { InlineEditMessages } from './namespaces/inline-edit.js';
12
13
  import { FilterChipsMessages } from './namespaces/filter-chips.js';
13
14
  import { FileUploadLabels, ImageUploadFieldLabels } from '../uploader/file-upload-constants.js';
@@ -72,6 +73,7 @@ declare const comitorMessagesEn: {
72
73
  inlineEdit: InlineEditMessages;
73
74
  kanban: KanbanLabels;
74
75
  keyValue: KeyValueLabels;
76
+ markdownEditor: MarkdownEditorLabels;
75
77
  mention: MentionMessages;
76
78
  pagination: PaginationMessages;
77
79
  phoneInput: PhoneInputLabels;
@@ -23,6 +23,7 @@ import { imageUploadFieldEn, imageUploadFieldVi } from "./namespaces/image-uploa
23
23
  import { inlineEditEn, inlineEditVi } from "./namespaces/inline-edit.js";
24
24
  import { kanbanEn, kanbanVi } from "./namespaces/kanban.js";
25
25
  import { keyValueEn, keyValueVi } from "./namespaces/key-value.js";
26
+ import { markdownEditorEn, markdownEditorVi } from "./namespaces/markdown-editor.js";
26
27
  import { mentionEn, mentionVi } from "./namespaces/mention.js";
27
28
  import { paginationEn, paginationVi } from "./namespaces/pagination.js";
28
29
  import { phoneInputEn, phoneInputVi } from "./namespaces/phone-input.js";
@@ -64,6 +65,7 @@ const comitorMessagesEn = {
64
65
  inlineEdit: inlineEditEn,
65
66
  kanban: kanbanEn,
66
67
  keyValue: keyValueEn,
68
+ markdownEditor: markdownEditorEn,
67
69
  mention: mentionEn,
68
70
  pagination: paginationEn,
69
71
  phoneInput: phoneInputEn,
@@ -106,6 +108,7 @@ const comitorMessagesVi = {
106
108
  inlineEdit: inlineEditVi,
107
109
  kanban: kanbanVi,
108
110
  keyValue: keyValueVi,
111
+ markdownEditor: markdownEditorVi,
109
112
  mention: mentionVi,
110
113
  pagination: paginationVi,
111
114
  phoneInput: phoneInputVi,