@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 +1 @@
1
- {"version":3,"sources":["../../src/editor/image-upload.ts"],"sourcesContent":["/**\n * Ả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.\n *\n * 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ý\n * — 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` để\n * LƯU. Cùng nguyên tắc với `/uploader` (`XHRUpload` về endpoint của app).\n *\n * ── ẢNH ĐANG TẢI LÀ DECORATION, KHÔNG PHẢI NODE ────────────────────────────────────────────────\n * 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\n * (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\n * MỘT lần, khi app đã trả `src`.\n *\n * 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\n * 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\n * 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ờ đó\n * 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:\n * lịch sử chỉ có một bước, \"chèn ảnh thật\".\n */\n\nimport { type Editor, Extension } from \"@tiptap/core\";\nimport { type EditorState, Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { Decoration, DecorationSet } from \"@tiptap/pm/view\";\n\n/** Thứ `onImageUpload` trả về: `src` để LƯU, hoặc kèm `alt`/`title`. */\nexport type MarkdownEditorImageUploadResult = string | { src: string; alt?: string; title?: string };\n\nexport interface ImageUploadHandlers {\n upload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;\n onError: (error: unknown, file: File) => void;\n /** Đổi `src` lưu → `src` hiện (`resolveImageSrc`), để tải trước ảnh thật trước khi chèn. */\n resolveSrc?: (src: string) => string;\n /** Nhãn trợ năng của ảnh đang tải (`role=\"img\"` của widget). */\n uploadingLabel: string;\n}\n\n/** Ảnh đang tải của MỘT trình soạn — `MarkdownEditorHandle.isUploading()` đọc nó. */\nexport type PendingUploads = Set<string>;\n\ntype UploadMeta = { add: { id: string; pos: number; src: string } } | { remove: { id: string } };\n\nconst UPLOAD_PLUGIN_KEY = new PluginKey<DecorationSet>(\"comitorImageUpload\");\n\n/** 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. */\nexport function imageFilesFrom(data: DataTransfer | null): File[] {\n return Array.from(data?.files ?? []).filter((file) => file.type.startsWith(\"image/\"));\n}\n\n/**\n * `crypto.randomUUID` chỉ có ở NGỮ CẢNH AN TOÀN — mở trang dev bằng IP mạng LAN qua http thì nó\n * `undefined`, và một `TypeError` giữa lúc dán ảnh là mất luôn tấm ảnh. Id này chỉ cần duy nhất\n * trong một tài liệu đang mở.\n */\nfunction createUploadId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Vị trí chèn một ảnh (node KHỐI) gần `position` nhất. Trong bảng thì ra SAU bảng: ô bảng chỉ chứa\n * một đoạn văn (`ComitorTableCell`), chèn khối vào đó là một giao dịch không hợp lệ.\n */\nfunction blockInsertPosition(state: EditorState, position: number): number {\n const $pos = state.doc.resolve(Math.max(0, Math.min(position, state.doc.content.size)));\n for (let depth = $pos.depth; depth > 0; depth--) {\n if ($pos.node(depth).type.name === \"table\") return $pos.after(depth);\n }\n return $pos.pos;\n}\n\n/** Vị trí HIỆN TẠI của widget đang tải — đã đi theo mọi giao dịch từ lúc dán. `null` = đã bị xoá cùng vùng chữ. */\nfunction findPlaceholder(state: EditorState, id: string): number | null {\n const found = UPLOAD_PLUGIN_KEY.getState(state)?.find(undefined, undefined, (spec) => spec.id === id);\n return found?.[0]?.from ?? null;\n}\n\n/**\n * Widget ảnh đang tải. Lớp kiểu nằm ở `markdown-editor-classes.ts` (`[data-upload-preview]`) — DOM\n * này không qua JSX nên không gắn `className` có nghĩa được ở đây ngoài một thuộc tính để nhắm.\n */\nfunction createPreview(src: string, label: string): HTMLElement {\n const wrapper = document.createElement(\"span\");\n wrapper.setAttribute(\"data-upload-preview\", \"\");\n wrapper.setAttribute(\"role\", \"img\");\n wrapper.setAttribute(\"aria-label\", label);\n const image = document.createElement(\"img\");\n image.src = src;\n image.alt = \"\";\n wrapper.append(image);\n return wrapper;\n}\n\n/**\n * Tải trước ảnh thật rồi mới chèn — không thì ảnh nháy TRẮNG giữa lúc widget đã gỡ còn ảnh mạng chưa\n * về. Lỗi tải trước hay quá 5 giây thì vẫn chèn: thà một ảnh vỡ thấy được còn hơn treo mãi ở trạng\n * thái \"đang tải\".\n */\nfunction preload(src: string): Promise<void> {\n if (typeof window === \"undefined\" || typeof window.Image !== \"function\") return Promise.resolve();\n const image = new window.Image();\n image.src = src;\n return Promise.race([image.decode().catch(() => undefined), new Promise<void>((done) => setTimeout(done, 5000))]);\n}\n\n/**\n * 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\n * 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 —\n * 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.\n */\nexport function insertImageFiles(\n editor: Editor,\n files: readonly File[],\n position: number | undefined,\n handlers: ImageUploadHandlers,\n pending: PendingUploads\n): void {\n const upload = handlers.upload;\n if (!upload || !editor.isEditable) return;\n\n const at = position ?? editor.state.selection.from;\n for (const file of files) {\n const id = createUploadId();\n const previewSrc = URL.createObjectURL(file);\n pending.add(id);\n const meta: UploadMeta = { add: { id, pos: at, src: previewSrc } };\n editor.view.dispatch(editor.state.tr.setMeta(UPLOAD_PLUGIN_KEY, meta));\n\n const settle = () => {\n pending.delete(id);\n URL.revokeObjectURL(previewSrc);\n };\n const removePreview = () => {\n if (editor.isDestroyed) return;\n const removal: UploadMeta = { remove: { id } };\n editor.view.dispatch(editor.state.tr.setMeta(UPLOAD_PLUGIN_KEY, removal));\n };\n\n upload(file)\n .then(async (result) => {\n const attrs = typeof result === \"string\" ? { src: result } : result;\n await preload(handlers.resolveSrc?.(attrs.src) ?? attrs.src);\n if (editor.isDestroyed) return settle();\n const pos = findPlaceholder(editor.state, id);\n if (pos === null) {\n // Vùng chữ chứa widget đã bị xoá trong lúc chờ: người dùng đã bỏ tấm ảnh này.\n removePreview();\n return settle();\n }\n const removal: UploadMeta = { remove: { id } };\n editor\n .chain()\n .command(({ tr }) => {\n tr.setMeta(UPLOAD_PLUGIN_KEY, removal);\n return true;\n })\n .insertContentAt(\n blockInsertPosition(editor.state, pos),\n { type: \"image\", attrs: { src: attrs.src, alt: attrs.alt ?? file.name, title: attrs.title ?? null } },\n { updateSelection: false }\n )\n .run();\n settle();\n })\n .catch((error: unknown) => {\n removePreview();\n settle();\n handlers.onError(error, file);\n });\n }\n}\n\n/**\n * Bắt ảnh khi DÁN và khi THẢ, và giữ các widget đang tải. Không có `onImageUpload` thì plugin đứng\n * 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\n * (không có chỗ nào để lưu nó).\n *\n * 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.\n */\nexport function createImageUploadExtension(\n handlers: { readonly current: ImageUploadHandlers },\n pending: PendingUploads\n): Extension {\n return Extension.create({\n name: \"comitorImageUpload\",\n addProseMirrorPlugins() {\n const editor = this.editor;\n return [\n new Plugin<DecorationSet>({\n key: UPLOAD_PLUGIN_KEY,\n state: {\n init: () => DecorationSet.empty,\n apply(tr, set) {\n let next = set.map(tr.mapping, tr.doc);\n const meta = tr.getMeta(UPLOAD_PLUGIN_KEY) as UploadMeta | undefined;\n if (meta && \"add\" in meta) {\n const { id, pos, src } = meta.add;\n const label = handlers.current.uploadingLabel;\n // `side: -1`: chữ gõ tiếp ngay tại chỗ dán đi SAU widget — như thể con trỏ đã đứng\n // sau ảnh. Mặc định (0) đẩy widget sang phải, và câu gõ tiếp nằm trước ảnh.\n const widget = Decoration.widget(pos, () => createPreview(src, label), { id, key: id, side: -1 });\n next = next.add(tr.doc, [widget]);\n } else if (meta && \"remove\" in meta) {\n next = next.remove(next.find(undefined, undefined, (spec) => spec.id === meta.remove.id));\n }\n return next;\n }\n },\n props: {\n decorations: (state) => UPLOAD_PLUGIN_KEY.getState(state),\n handlePaste: (view, event) => {\n if (!handlers.current.upload || !view.editable) return false;\n const files = imageFilesFrom(event.clipboardData);\n if (files.length === 0) return false;\n insertImageFiles(editor, files, undefined, handlers.current, pending);\n return true;\n },\n handleDrop: (view, event, _slice, moved) => {\n if (moved || !handlers.current.upload || !view.editable) return false;\n const files = imageFilesFrom(event.dataTransfer);\n if (files.length === 0) return false;\n const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });\n insertImageFiles(editor, files, coordinates?.pos, handlers.current, pending);\n return true;\n }\n }\n })\n ];\n }\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/editor/image-upload.ts"],"sourcesContent":["/**\n * Ả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.\n *\n * 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ý\n * — 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` để\n * LƯU. Cùng nguyên tắc với `/uploader` (`XHRUpload` về endpoint của app).\n *\n * ── ẢNH ĐANG TẢI LÀ DECORATION, KHÔNG PHẢI NODE ────────────────────────────────────────────────\n * 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\n * (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\n * MỘT lần, khi app đã trả `src`.\n *\n * 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\n * 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\n * 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ờ đó\n * 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:\n * lịch sử chỉ có một bước, \"chèn ảnh thật\".\n */\n\nimport { type Editor, Extension } from \"@tiptap/core\";\nimport { type EditorState, Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { Decoration, DecorationSet } from \"@tiptap/pm/view\";\nimport type { EditorFormat } from \"./editor-types.js\";\n\n/** Thứ `onImageUpload` trả về: `src` để LƯU, hoặc kèm `alt`/`title`. */\nexport type MarkdownEditorImageUploadResult = string | { src: string; alt?: string; title?: string };\n\nexport interface ImageUploadHandlers {\n upload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;\n onError: (error: unknown, file: File) => void;\n /** Đổi `src` lưu → `src` hiện (`resolveImageSrc`), để tải trước ảnh thật trước khi chèn. */\n resolveSrc?: (src: string) => string;\n /** Nhãn trợ năng của ảnh đang tải (`role=\"img\"` của widget). */\n uploadingLabel: string;\n /** Định dạng lưu — ô bảng markdown không nhận ảnh (xem `blockInsertPosition`). Mặc định `markdown`. */\n format?: EditorFormat;\n}\n\n/** Ảnh đang tải của MỘT trình soạn — `MarkdownEditorHandle.isUploading()` đọc nó. */\nexport type PendingUploads = Set<string>;\n\ntype UploadMeta = { add: { id: string; pos: number; src: string } } | { remove: { id: string } };\n\nconst UPLOAD_PLUGIN_KEY = new PluginKey<DecorationSet>(\"comitorImageUpload\");\n\n/** 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. */\nexport function imageFilesFrom(data: DataTransfer | null): File[] {\n return Array.from(data?.files ?? []).filter((file) => file.type.startsWith(\"image/\"));\n}\n\n/**\n * `crypto.randomUUID` chỉ có ở NGỮ CẢNH AN TOÀN — mở trang dev bằng IP mạng LAN qua http thì nó\n * `undefined`, và một `TypeError` giữa lúc dán ảnh là mất luôn tấm ảnh. Id này chỉ cần duy nhất\n * trong một tài liệu đang mở.\n */\nfunction createUploadId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Vị trí chèn một ảnh (node KHỐI) gần `position` nhất. Trong bảng của trình soạn MARKDOWN thì ra SAU\n * bảng: ô bảng ở đó chỉ chứa một đoạn văn (`ComitorTableCell`), chèn khối vào là một giao dịch không\n * hợp lệ. Ô bảng HTML nhận khối, nên ảnh vào đúng chỗ.\n */\nfunction blockInsertPosition(state: EditorState, position: number, format: EditorFormat): number {\n const $pos = state.doc.resolve(Math.max(0, Math.min(position, state.doc.content.size)));\n if (format === \"html\") return $pos.pos;\n for (let depth = $pos.depth; depth > 0; depth--) {\n if ($pos.node(depth).type.name === \"table\") return $pos.after(depth);\n }\n return $pos.pos;\n}\n\n/** Vị trí HIỆN TẠI của widget đang tải — đã đi theo mọi giao dịch từ lúc dán. `null` = đã bị xoá cùng vùng chữ. */\nfunction findPlaceholder(state: EditorState, id: string): number | null {\n const found = UPLOAD_PLUGIN_KEY.getState(state)?.find(undefined, undefined, (spec) => spec.id === id);\n return found?.[0]?.from ?? null;\n}\n\n/**\n * Widget ảnh đang tải. Lớp kiểu nằm ở `markdown-editor-classes.ts` (`[data-upload-preview]`) — DOM\n * này không qua JSX nên không gắn `className` có nghĩa được ở đây ngoài một thuộc tính để nhắm.\n */\nfunction createPreview(src: string, label: string): HTMLElement {\n const wrapper = document.createElement(\"span\");\n wrapper.setAttribute(\"data-upload-preview\", \"\");\n wrapper.setAttribute(\"role\", \"img\");\n wrapper.setAttribute(\"aria-label\", label);\n const image = document.createElement(\"img\");\n image.src = src;\n image.alt = \"\";\n wrapper.append(image);\n return wrapper;\n}\n\n/**\n * Tải trước ảnh thật rồi mới chèn — không thì ảnh nháy TRẮNG giữa lúc widget đã gỡ còn ảnh mạng chưa\n * về. Lỗi tải trước hay quá 5 giây thì vẫn chèn: thà một ảnh vỡ thấy được còn hơn treo mãi ở trạng\n * thái \"đang tải\".\n */\nfunction preload(src: string): Promise<void> {\n if (typeof window === \"undefined\" || typeof window.Image !== \"function\") return Promise.resolve();\n const image = new window.Image();\n image.src = src;\n return Promise.race([image.decode().catch(() => undefined), new Promise<void>((done) => setTimeout(done, 5000))]);\n}\n\n/**\n * 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\n * 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 —\n * 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.\n */\nexport function insertImageFiles(\n editor: Editor,\n files: readonly File[],\n position: number | undefined,\n handlers: ImageUploadHandlers,\n pending: PendingUploads\n): void {\n const upload = handlers.upload;\n if (!upload || !editor.isEditable) return;\n\n const at = position ?? editor.state.selection.from;\n for (const file of files) {\n const id = createUploadId();\n const previewSrc = URL.createObjectURL(file);\n pending.add(id);\n const meta: UploadMeta = { add: { id, pos: at, src: previewSrc } };\n editor.view.dispatch(editor.state.tr.setMeta(UPLOAD_PLUGIN_KEY, meta));\n\n const settle = () => {\n pending.delete(id);\n URL.revokeObjectURL(previewSrc);\n };\n const removePreview = () => {\n if (editor.isDestroyed) return;\n const removal: UploadMeta = { remove: { id } };\n editor.view.dispatch(editor.state.tr.setMeta(UPLOAD_PLUGIN_KEY, removal));\n };\n\n upload(file)\n .then(async (result) => {\n const attrs = typeof result === \"string\" ? { src: result } : result;\n await preload(handlers.resolveSrc?.(attrs.src) ?? attrs.src);\n if (editor.isDestroyed) return settle();\n const pos = findPlaceholder(editor.state, id);\n if (pos === null) {\n // Vùng chữ chứa widget đã bị xoá trong lúc chờ: người dùng đã bỏ tấm ảnh này.\n removePreview();\n return settle();\n }\n const removal: UploadMeta = { remove: { id } };\n editor\n .chain()\n .command(({ tr }) => {\n tr.setMeta(UPLOAD_PLUGIN_KEY, removal);\n return true;\n })\n .insertContentAt(\n blockInsertPosition(editor.state, pos, handlers.format ?? \"markdown\"),\n { type: \"image\", attrs: { src: attrs.src, alt: attrs.alt ?? file.name, title: attrs.title ?? null } },\n { updateSelection: false }\n )\n .run();\n settle();\n })\n .catch((error: unknown) => {\n removePreview();\n settle();\n handlers.onError(error, file);\n });\n }\n}\n\n/**\n * Bắt ảnh khi DÁN và khi THẢ, và giữ các widget đang tải. Không có `onImageUpload` thì plugin đứng\n * 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\n * (không có chỗ nào để lưu nó).\n *\n * 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.\n */\nexport function createImageUploadExtension(\n handlers: { readonly current: ImageUploadHandlers },\n pending: PendingUploads\n): Extension {\n return Extension.create({\n name: \"comitorImageUpload\",\n addProseMirrorPlugins() {\n const editor = this.editor;\n return [\n new Plugin<DecorationSet>({\n key: UPLOAD_PLUGIN_KEY,\n state: {\n init: () => DecorationSet.empty,\n apply(tr, set) {\n let next = set.map(tr.mapping, tr.doc);\n const meta = tr.getMeta(UPLOAD_PLUGIN_KEY) as UploadMeta | undefined;\n if (meta && \"add\" in meta) {\n const { id, pos, src } = meta.add;\n const label = handlers.current.uploadingLabel;\n // `side: -1`: chữ gõ tiếp ngay tại chỗ dán đi SAU widget — như thể con trỏ đã đứng\n // sau ảnh. Mặc định (0) đẩy widget sang phải, và câu gõ tiếp nằm trước ảnh.\n const widget = Decoration.widget(pos, () => createPreview(src, label), { id, key: id, side: -1 });\n next = next.add(tr.doc, [widget]);\n } else if (meta && \"remove\" in meta) {\n next = next.remove(next.find(undefined, undefined, (spec) => spec.id === meta.remove.id));\n }\n return next;\n }\n },\n props: {\n decorations: (state) => UPLOAD_PLUGIN_KEY.getState(state),\n handlePaste: (view, event) => {\n if (!handlers.current.upload || !view.editable) return false;\n const files = imageFilesFrom(event.clipboardData);\n if (files.length === 0) return false;\n insertImageFiles(editor, files, undefined, handlers.current, pending);\n return true;\n },\n handleDrop: (view, event, _slice, moved) => {\n if (moved || !handlers.current.upload || !view.editable) return false;\n const files = imageFilesFrom(event.dataTransfer);\n if (files.length === 0) return false;\n const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });\n insertImageFiles(editor, files, coordinates?.pos, handlers.current, pending);\n return true;\n }\n }\n })\n ];\n }\n });\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,6 @@
1
- export { MarkdownEditorImageUploadResult } from './image-upload.js';
2
- export { MarkdownEditor, MarkdownEditorHandle, MarkdownEditorMentionItem, MarkdownEditorMentionSource, MarkdownEditorMentions, MarkdownEditorProps, MarkdownEditorVariant } from './markdown-editor.js';
1
+ export { E as EditorHandleBase, a as EditorToolbarMode, M as MarkdownEditorImageUploadResult, b as MarkdownEditorMentionItem, c as MarkdownEditorMentionSource, d as MarkdownEditorMentions, e as MarkdownEditorVariant } from '../editor-types-BMST7-VR.js';
2
+ export { MarkdownEditor, MarkdownEditorHandle, MarkdownEditorProps } from './markdown-editor.js';
3
3
  export { DEFAULT_MARKDOWN_EDITOR_LABELS, MarkdownEditorBlockId, MarkdownEditorLabels } from './markdown-editor-constants.js';
4
+ export { RichTextEditor, RichTextEditorHandle, RichTextEditorProps } from './rich-text-editor.js';
4
5
  import '@tiptap/core';
5
6
  import 'react';
@@ -1,11 +1,11 @@
1
- import {
2
- MarkdownEditor
3
- } from "./markdown-editor.js";
1
+ import { MarkdownEditor } from "./markdown-editor.js";
4
2
  import {
5
3
  DEFAULT_MARKDOWN_EDITOR_LABELS
6
4
  } from "./markdown-editor-constants.js";
5
+ import { RichTextEditor } from "./rich-text-editor.js";
7
6
  export {
8
7
  DEFAULT_MARKDOWN_EDITOR_LABELS,
9
- MarkdownEditor
8
+ MarkdownEditor,
9
+ RichTextEditor
10
10
  };
11
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/editor/index.ts"],"sourcesContent":["/**\n * Entry `@comitor/ui/editor` — trình soạn văn bản giàu định dạng kiểu Linear, lưu bằng markdown.\n *\n * ── VÌ SAO LÀ MỘT ENTRY RIÊNG ────────────────────────────────────────────────────────────────\n * Cùng lý do `/data-grid` và `/uploader`: trình soạn kéo Tiptap/ProseMirror (~184KB gzip, cộng ~53KB\n * bảng tô màu mã) qua các peer TUỲ CHỌN, mà `tsup` chạy `bundle: false` — để nó ở entry `.` là bắt\n * mọi app cài Tiptap chỉ để dùng `Button`. App không `import` từ đây thì không có một byte nào.\n *\n * ── VÌ SAO TIPTAP LÀ PEER, KHÔNG PHẢI DEPENDENCY ────────────────────────────────────────────\n * Mọi gói `@tiptap/*` ghim `@tiptap/core` + `@tiptap/pm` bằng phiên bản CHÍNH XÁC. App sẽ tự viết\n * extension (vd node `KEY-n` của module Tasks) bằng `@tiptap/core` của nó — nếu gói giữ một bản\n * riêng thì trang có hai bản ProseMirror, và node tạo bằng bản này không lắp được vào tài liệu của\n * bản kia. Cùng loại \"context singleton\" với `next-themes` và `@tanstack/react-table`.\n *\n * Cài ở app — MỌI gói `@tiptap/*` CÙNG một phiên bản:\n * ```bash\n * pnpm add @tiptap/core @tiptap/pm @tiptap/react @tiptap/starter-kit @tiptap/markdown \\\n * @tiptap/extensions @tiptap/extension-list @tiptap/extension-table @tiptap/extension-image \\\n * @tiptap/extension-mention @tiptap/suggestion @tiptap/extension-paragraph \\\n * @tiptap/extension-code-block-lowlight lowlight highlight.js\n * ```\n *\n * Tài liệu: docs/editor.md của repo `comitor-ui`.\n */\n\nexport type { MarkdownEditorImageUploadResult } from \"./image-upload.js\";\nexport {\n MarkdownEditor,\n type MarkdownEditorHandle,\n type MarkdownEditorMentionItem,\n type MarkdownEditorMentionSource,\n type MarkdownEditorMentions,\n type MarkdownEditorProps,\n type MarkdownEditorVariant\n} from \"./markdown-editor.js\";\nexport {\n DEFAULT_MARKDOWN_EDITOR_LABELS,\n type MarkdownEditorBlockId,\n type MarkdownEditorLabels\n} from \"./markdown-editor-constants.js\";\n"],"mappings":"AA0BA;AAAA,EACE;AAAA,OAOK;AACP;AAAA,EACE;AAAA,OAGK;","names":[]}
1
+ {"version":3,"sources":["../../src/editor/index.ts"],"sourcesContent":["/**\n * Entry `@comitor/ui/editor` — hai trình soạn văn bản giàu định dạng trên cùng một lõi Tiptap:\n * · `MarkdownEditor` — LƯU markdown, thanh định dạng nổi kiểu Linear (mô tả việc, bình luận);\n * · `RichTextEditor` — LƯU HTML, thanh công cụ cố định kiểu CMS (bài viết, trang tin).\n * Chọn theo thứ CỘT LƯU mang. Cả hai nhận `toolbar=\"bubble\" | \"fixed\"`.\n *\n * ── VÌ SAO LÀ MỘT ENTRY RIÊNG ────────────────────────────────────────────────────────────────\n * Cùng lý do `/data-grid` và `/uploader`: trình soạn kéo Tiptap/ProseMirror (~184KB gzip, cộng ~53KB\n * bảng tô màu mã) qua các peer TUỲ CHỌN, mà `tsup` chạy `bundle: false` — để nó ở entry `.` là bắt\n * mọi app cài Tiptap chỉ để dùng `Button`. App không `import` từ đây thì không có một byte nào.\n *\n * ── VÌ SAO TIPTAP LÀ PEER, KHÔNG PHẢI DEPENDENCY ────────────────────────────────────────────\n * Mọi gói `@tiptap/*` ghim `@tiptap/core` + `@tiptap/pm` bằng phiên bản CHÍNH XÁC. App sẽ tự viết\n * extension (vd node `KEY-n` của module Tasks) bằng `@tiptap/core` của nó — nếu gói giữ một bản\n * riêng thì trang có hai bản ProseMirror, và node tạo bằng bản này không lắp được vào tài liệu của\n * bản kia. Cùng loại \"context singleton\" với `next-themes` và `@tanstack/react-table`.\n *\n * Cài ở app — MỌI gói `@tiptap/*` CÙNG một phiên bản (`RichTextEditor` không dùng `@tiptap/markdown`,\n * nhưng hai trình soạn chung một lược đồ nên gói vẫn nhập nó — cài đủ cả danh sách):\n * ```bash\n * pnpm add @tiptap/core @tiptap/pm @tiptap/react @tiptap/starter-kit @tiptap/markdown \\\n * @tiptap/extensions @tiptap/extension-list @tiptap/extension-table @tiptap/extension-image \\\n * @tiptap/extension-mention @tiptap/suggestion @tiptap/extension-paragraph \\\n * @tiptap/extension-code-block-lowlight lowlight highlight.js\n * ```\n *\n * Tài liệu: docs/editor.md của repo `comitor-ui`.\n */\n\nexport type {\n EditorHandleBase,\n EditorToolbarMode,\n MarkdownEditorMentionItem,\n MarkdownEditorMentionSource,\n MarkdownEditorMentions,\n MarkdownEditorVariant\n} from \"./editor-types.js\";\nexport type { MarkdownEditorImageUploadResult } from \"./image-upload.js\";\nexport { MarkdownEditor, type MarkdownEditorHandle, type MarkdownEditorProps } from \"./markdown-editor.js\";\nexport {\n DEFAULT_MARKDOWN_EDITOR_LABELS,\n type MarkdownEditorBlockId,\n type MarkdownEditorLabels\n} from \"./markdown-editor-constants.js\";\nexport { RichTextEditor, type RichTextEditorHandle, type RichTextEditorProps } from \"./rich-text-editor.js\";\n"],"mappings":"AAsCA,SAAS,sBAA2E;AACpF;AAAA,EACE;AAAA,OAGK;AACP,SAAS,sBAA2E;","names":[]}
@@ -54,7 +54,8 @@ const TASK_CHECKBOX = [
54
54
  const BLOCKS = [
55
55
  "[&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground",
56
56
  "[&_blockquote>*+*]:mt-2",
57
- "[&_hr]:my-1 [&_hr]:border-border",
57
+ // Đường kẻ và lưới bảng (dưới) dùng `--input`, không `--border` — lý do ở chỗ khai `TABLES`.
58
+ "[&_hr]:my-1 [&_hr]:border-input",
58
59
  // Khối mã XUỐNG DÒNG (`pre-wrap`) thay vì cuộn ngang: trong trình soạn, một thanh cuộn ngang lồng
59
60
  // trong khung cuộn dọc là chỗ con trỏ biến mất khỏi tầm nhìn khi gõ. Đó cũng là CSS Tiptap chèn sẵn
60
61
  // (`.ProseMirror pre`) — bản trước ghi `whitespace-pre` ở đây, và nó thua luật đó mà không ai thấy.
@@ -79,9 +80,10 @@ const SYNTAX = [
79
80
  const TABLES = [
80
81
  "[&_.tableWrapper]:overflow-x-auto",
81
82
  "[&_table]:w-full [&_table]:border-collapse [&_table]:text-left",
82
- "[&_td]:border [&_td]:border-border [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top",
83
- "[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:align-top [&_th]:font-medium",
84
- "[&_td>p]:my-0 [&_th>p]:my-0 [&_.selectedCell]:bg-accent [&_.selectedCell]:text-accent-foreground"
83
+ "[&_td]:border [&_td]:border-input [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top",
84
+ "[&_th]:border [&_th]:border-input [&_th]:bg-muted [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:align-top [&_th]:font-medium",
85
+ "[&_td>p]:my-0 [&_th>p]:my-0 [&_td>*+*]:mt-2 [&_th>*+*]:mt-2",
86
+ "[&_.selectedCell]:bg-accent [&_.selectedCell]:text-accent-foreground"
85
87
  ];
86
88
  const MEDIA = [
87
89
  "[&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/editor/markdown-editor-classes.ts"],"sourcesContent":["/**\n * Kiểu chữ của vùng soạn — file thuần, không `\"use client\"`.\n *\n * DOM bên trong do ProseMirror dựng (không phải JSX của gói), nên không có `className` nào gắn được\n * lên từng `<h2>`, `<li>`, `<pre>`: mọi kiểu đi qua biến thể con cháu `[&_h2]:…` đặt trên PHẦN TỬ\n * GỐC của ProseMirror. Tailwind của app vẫn sinh được chúng vì chuỗi nằm nguyên văn trong `dist/`\n * (`@source \"./dist\"` của `styles.css`) — nên KHÔNG dựng chuỗi lớp động ở đây.\n *\n * Màu chỉ đi qua token VAI TRÒ, đúng hợp đồng của gói: chữ thường kế thừa `--foreground`, liên kết\n * `--link-ink`, mã `--muted`, nhắc tên `--info/15` (cùng khối của `Mention`), tô màu cú pháp các bậc\n * `-ink` — chúng là bậc dành cho CHỮ trên nền trang/muted ở cả hai bảng màu.\n */\n\n/*\n * Bố cục khối: khoảng cách giữa các khối CẤP ĐẦU bằng `> * + *` (không margin hai đầu tài liệu),\n * tiêu đề cách xa hơn phía trên để nhóm với phần nó mở. Cỡ chữ theo `Textarea`: 16px ở màn hẹp —\n * dưới 16px là Safari iOS tự phóng to trang khi chạm vào vùng soạn — 14px từ `md`.\n */\nconst LAYOUT = [\n \"min-w-0 text-base leading-relaxed text-foreground outline-none md:text-sm\",\n \"[&>*+*]:mt-3 [&>*:first-child]:mt-0\",\n \"[&>h1]:mt-6 [&>h2]:mt-5 [&>h3]:mt-4\"\n];\n\nconst HEADINGS = [\n \"[&_h1]:text-xl [&_h1]:leading-snug [&_h1]:font-semibold [&_h1]:tracking-tight\",\n \"[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold\",\n \"[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold\",\n \"[&_h4]:font-semibold [&_h5]:font-semibold [&_h6]:font-semibold\"\n];\n\nconst INLINE = [\n \"[&_strong]:font-semibold\",\n // Liên kết: khuôn của `Button variant=\"link\"` / `FieldDescription` — bậc `--link-ink`, gạch dưới.\n \"[&_a]:text-link-ink [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-link-ink-hover\",\n \"[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5\",\n \"[&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.875em]\",\n // Nhắc tên: cùng cặp với khối `@Tên` của `Mention` (nền `info/15`, chữ thường) — đã đo ở mention.tsx.\n \"[&_[data-type=mention]]:rounded-sm [&_[data-type=mention]]:bg-info/15 [&_[data-type=mention]]:px-0.5\",\n \"[&_[data-type=mention]]:py-px [&_[data-type=mention]]:font-medium [&_[data-type=mention]]:text-foreground\"\n];\n\n/*\n * Danh sách thường. Danh sách việc (`ul[data-type=taskList]`) bỏ chấm đầu dòng và tự vẽ ô tick: DOM\n * do `TaskItem` dựng là `li[data-checked] > label > input + span(sr-only)` cạnh `div` nội dung.\n * ⚠ `<li>` KHÔNG mang `data-type=\"taskItem\"`: thuộc tính đó chỉ có trong `renderHTML`, còn node view\n * (thứ thật sự vẽ trong trình soạn) không gắn nó — đã đo trên 3.31.4, selector `li[data-type=…]`\n * trượt hết và ô tick nằm một dòng, chữ rơi xuống dòng dưới. Nhắm qua `ul` cha.\n */\nconst LISTS = [\n \"[&_ul:not([data-type=taskList])]:list-disc [&_ol]:list-decimal\",\n \"[&_ul:not([data-type=taskList])]:pl-6 [&_ol]:pl-6 [&_li]:marker:text-muted-foreground\",\n \"[&_li+li]:mt-1 [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_li>p]:my-0\",\n \"[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0.5\",\n \"[&_[data-type=taskList]>li]:flex [&_[data-type=taskList]>li]:items-start [&_[data-type=taskList]>li]:gap-2\",\n \"[&_[data-type=taskList]>li>label]:flex [&_[data-type=taskList]>li>label]:h-[1.625em] [&_[data-type=taskList]>li>label]:items-center\",\n \"[&_[data-type=taskList]>li>label]:select-none [&_[data-type=taskList]>li>div]:min-w-0 [&_[data-type=taskList]>li>div]:flex-1\",\n // Soạn được: con trỏ tay trên ô tick. Chỉ đọc: ô không nhận chuột — Tiptap để nó bấm được rồi tự lật\n // lại (không có `onReadOnlyChecked`), nên một cú bấm \"không ăn\" là thứ duy nhất người dùng thấy.\n \"[&[contenteditable=true]_[data-type=taskList]>li>label>input]:cursor-pointer\",\n \"[&[contenteditable=false]_[data-type=taskList]>li>label]:pointer-events-none\",\n // Mục đã xong: chữ nhạt + gạch — CHỈ đoạn của chính mục, không lan xuống danh sách con chưa xong.\n \"[&_li[data-checked=true]>div>p]:text-muted-foreground [&_li[data-checked=true]>div>p]:line-through\"\n];\n\n/*\n * Ô tick vẽ bằng CSS trên chính `<input>` (`appearance-none`), dấu tick là một chữ L xoay 45° vẽ\n * bằng viền của `::before` — không ảnh, không SVG, không mã màu: mọi màu là token của `Checkbox`\n * (`--choice-edge` viền nghỉ, `--control-on` nền khi chọn, `--control-check` dấu tick).\n */\nconst TASK_CHECKBOX = [\n \"[&_[data-type=taskList]>li>label>input]:relative [&_[data-type=taskList]>li>label>input]:m-0 [&_[data-type=taskList]>li>label>input]:grid\",\n \"[&_[data-type=taskList]>li>label>input]:size-4 [&_[data-type=taskList]>li>label>input]:shrink-0 [&_[data-type=taskList]>li>label>input]:appearance-none\",\n \"[&_[data-type=taskList]>li>label>input]:place-content-center [&_[data-type=taskList]>li>label>input]:rounded-[4px]\",\n \"[&_[data-type=taskList]>li>label>input]:border [&_[data-type=taskList]>li>label>input]:border-choice-edge\",\n \"[&_[data-type=taskList]>li>label>input]:bg-transparent [&_[data-type=taskList]>li>label>input]:transition-colors\",\n \"[&_[data-type=taskList]>li>label>input:checked]:border-control-on [&_[data-type=taskList]>li>label>input:checked]:bg-control-on\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-2 [&_[data-type=taskList]>li>label>input:focus-visible]:outline-offset-2\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-ring\",\n // Dấu tick CHỈ tồn tại khi đã tick (`content` chỉ có ở `:checked`) — không ẩn bằng `opacity-0`: một\n // dấu vô hình vẫn là một nét mà cổng màu phải đo, và đo ra 1,00:1.\n \"[&_[data-type=taskList]>li>label>input:checked]:before:h-2 [&_[data-type=taskList]>li>label>input:checked]:before:w-1\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:-translate-y-px [&_[data-type=taskList]>li>label>input:checked]:before:rotate-45\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-r-2 [&_[data-type=taskList]>li>label>input:checked]:before:border-b-2\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-control-check\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:content-['']\"\n];\n\n/*\n * Trích dẫn: vạch trái bằng `--muted-foreground` ĐẶC. `--border` đúng vai trò trang trí nhưng 1,16:1\n * ở light — gần như vô hình, trích dẫn chỉ còn là một đoạn chữ xám thụt vào. Bản `/40` dễ nhìn hơn\n * nhưng 1,71:1 trên `--muted` (light) / 1,87:1 (dark): cổng chặn ở bảng tương phản cao. Bản đặc qua\n * 3:1 ở cả bốn tổ hợp bảng màu × theme (đo bằng `pnpm lint:a11y`).\n */\nconst BLOCKS = [\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground\",\n \"[&_blockquote>*+*]:mt-2\",\n \"[&_hr]:my-1 [&_hr]:border-border\",\n // Khối mã XUỐNG DÒNG (`pre-wrap`) thay vì cuộn ngang: trong trình soạn, một thanh cuộn ngang lồng\n // trong khung cuộn dọc là chỗ con trỏ biến mất khỏi tầm nhìn khi gõ. Đó cũng là CSS Tiptap chèn sẵn\n // (`.ProseMirror pre`) — bản trước ghi `whitespace-pre` ở đây, và nó thua luật đó mà không ai thấy.\n \"[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:px-4 [&_pre]:py-3\",\n \"[&_pre]:font-mono [&_pre]:text-[0.8125rem] [&_pre]:leading-relaxed [&_pre]:whitespace-pre-wrap\",\n \"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit\"\n];\n\n/*\n * Tô màu cú pháp (lớp `hljs-*` do lowlight sinh). Phân vai kiểu bảng màu sáng quen thuộc của các\n * trình xem mã: từ khoá đỏ, chuỗi xanh đậm, hằng/số xanh ngọc, tên hàm cam, chú thích nhạt + nghiêng.\n * Toàn bậc `-ink`, nên đổi sang dark hay bảng tương phản cao là token tự đổi theo.\n */\nconst SYNTAX = [\n \"[&_.hljs-comment]:text-muted-foreground [&_.hljs-comment]:italic [&_.hljs-quote]:text-muted-foreground\",\n \"[&_.hljs-keyword]:text-red-ink [&_.hljs-selector-tag]:text-red-ink [&_.hljs-doctag]:text-red-ink\",\n \"[&_.hljs-meta_.hljs-keyword]:text-red-ink [&_.hljs-template-tag]:text-red-ink\",\n \"[&_.hljs-string]:text-navy-ink [&_.hljs-regexp]:text-navy-ink [&_.hljs-meta_.hljs-string]:text-navy-ink\",\n \"[&_.hljs-number]:text-teal-ink [&_.hljs-literal]:text-teal-ink [&_.hljs-attr]:text-teal-ink\",\n \"[&_.hljs-attribute]:text-teal-ink [&_.hljs-variable]:text-teal-ink [&_.hljs-template-variable]:text-teal-ink\",\n \"[&_.hljs-selector-attr]:text-teal-ink [&_.hljs-selector-class]:text-teal-ink [&_.hljs-selector-id]:text-teal-ink\",\n \"[&_.hljs-title]:text-orange-ink [&_.hljs-section]:text-orange-ink [&_.hljs-name]:text-orange-ink\",\n \"[&_.hljs-type]:text-orange-ink [&_.hljs-built_in]:text-orange-ink [&_.hljs-symbol]:text-orange-ink\",\n \"[&_.hljs-meta]:text-muted-foreground [&_.hljs-bullet]:text-orange-ink [&_.hljs-link]:underline\",\n \"[&_.hljs-addition]:text-success-ink [&_.hljs-deletion]:text-destructive-ink\",\n \"[&_.hljs-emphasis]:italic [&_.hljs-strong]:font-semibold\"\n];\n\n/*\n * Bảng: ô bảng chỉ chứa MỘT đoạn văn (xem `ComitorTableCell`), nên không cần luật cho khối lồng.\n * Cuộn ngang ở `.tableWrapper` (Tiptap bọc sẵn) — bảng rộng không được đẩy cả trang tràn ngang ở 375px.\n */\nconst TABLES = [\n \"[&_.tableWrapper]:overflow-x-auto\",\n \"[&_table]:w-full [&_table]:border-collapse [&_table]:text-left\",\n \"[&_td]:border [&_td]:border-border [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top\",\n \"[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:align-top [&_th]:font-medium\",\n \"[&_td>p]:my-0 [&_th>p]:my-0 [&_.selectedCell]:bg-accent [&_.selectedCell]:text-accent-foreground\"\n];\n\nconst MEDIA = [\n \"[&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border\",\n // Ảnh đang tải lên — một widget decoration, không phải node (xem `image-upload.ts`): đứng thành\n // một khối riêng, mờ và nhấp nháy tới khi ảnh thật được chèn vào chỗ nó.\n \"[&_[data-upload-preview]]:my-2 [&_[data-upload-preview]]:block [&_[data-upload-preview]]:w-fit\",\n \"[&_[data-upload-preview]>img]:animate-pulse [&_[data-upload-preview]>img]:opacity-60\",\n // Node được chọn bằng bấm (ảnh, đường kẻ): viền focus của gói, không phải nền.\n \"[&_.ProseMirror-selectednode]:outline-2 [&_.ProseMirror-selectednode]:outline-offset-2 [&_.ProseMirror-selectednode]:outline-ring\"\n];\n\n/*\n * Chữ mờ khi tài liệu rỗng — `Placeholder` gắn `.is-editor-empty` + `data-placeholder` lên đoạn\n * đầu. Đoạn đang gõ dở sau `/` hoặc `@` mang `.suggestion`: tô nền nhẹ để thấy chữ nào đang là lệnh.\n * Con trỏ khe (`gapcursor`, giữa hai khối không gõ được như bảng/ảnh): CSS mặc định Tiptap chèn\n * là viền `black` — vô hình ở dark, nên đổi sang `--foreground`.\n */\nconst AFFORDANCES = [\n \"[&_.is-editor-empty:first-child]:before:pointer-events-none [&_.is-editor-empty:first-child]:before:float-left\",\n \"[&_.is-editor-empty:first-child]:before:h-0 [&_.is-editor-empty:first-child]:before:text-muted-foreground\",\n \"[&_.is-editor-empty:first-child]:before:content-[attr(data-placeholder)]\",\n \"[&_.suggestion]:rounded-sm [&_.suggestion]:bg-muted\",\n \"[&_.ProseMirror-gapcursor]:after:border-foreground\"\n];\n\n/** Toàn bộ lớp của phần tử gốc ProseMirror (`.tiptap`). */\nexport const MARKDOWN_EDITOR_CONTENT_CLASS = [\n ...LAYOUT,\n ...HEADINGS,\n ...INLINE,\n ...LISTS,\n ...TASK_CHECKBOX,\n ...BLOCKS,\n ...SYNTAX,\n ...TABLES,\n ...MEDIA,\n ...AFFORDANCES\n].join(\" \");\n"],"mappings":"AAkBA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,WAAW;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,SAAS;AAAA,EACb;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF;AASA,MAAM,QAAQ;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAOA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AACF;AAOA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,QAAQ;AAAA,EACZ;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAQA,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,gCAAgC;AAAA,EAC3C,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL,EAAE,KAAK,GAAG;","names":[]}
1
+ {"version":3,"sources":["../../src/editor/markdown-editor-classes.ts"],"sourcesContent":["/**\n * Kiểu chữ của vùng soạn — file thuần, không `\"use client\"`.\n *\n * DOM bên trong do ProseMirror dựng (không phải JSX của gói), nên không có `className` nào gắn được\n * lên từng `<h2>`, `<li>`, `<pre>`: mọi kiểu đi qua biến thể con cháu `[&_h2]:…` đặt trên PHẦN TỬ\n * GỐC của ProseMirror. Tailwind của app vẫn sinh được chúng vì chuỗi nằm nguyên văn trong `dist/`\n * (`@source \"./dist\"` của `styles.css`) — nên KHÔNG dựng chuỗi lớp động ở đây.\n *\n * Màu chỉ đi qua token VAI TRÒ, đúng hợp đồng của gói: chữ thường kế thừa `--foreground`, liên kết\n * `--link-ink`, mã `--muted`, nhắc tên `--info/15` (cùng khối của `Mention`), tô màu cú pháp các bậc\n * `-ink` — chúng là bậc dành cho CHỮ trên nền trang/muted ở cả hai bảng màu.\n */\n\n/*\n * Bố cục khối: khoảng cách giữa các khối CẤP ĐẦU bằng `> * + *` (không margin hai đầu tài liệu),\n * tiêu đề cách xa hơn phía trên để nhóm với phần nó mở. Cỡ chữ theo `Textarea`: 16px ở màn hẹp —\n * dưới 16px là Safari iOS tự phóng to trang khi chạm vào vùng soạn — 14px từ `md`.\n */\nconst LAYOUT = [\n \"min-w-0 text-base leading-relaxed text-foreground outline-none md:text-sm\",\n \"[&>*+*]:mt-3 [&>*:first-child]:mt-0\",\n \"[&>h1]:mt-6 [&>h2]:mt-5 [&>h3]:mt-4\"\n];\n\nconst HEADINGS = [\n \"[&_h1]:text-xl [&_h1]:leading-snug [&_h1]:font-semibold [&_h1]:tracking-tight\",\n \"[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold\",\n \"[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold\",\n \"[&_h4]:font-semibold [&_h5]:font-semibold [&_h6]:font-semibold\"\n];\n\nconst INLINE = [\n \"[&_strong]:font-semibold\",\n // Liên kết: khuôn của `Button variant=\"link\"` / `FieldDescription` — bậc `--link-ink`, gạch dưới.\n \"[&_a]:text-link-ink [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-link-ink-hover\",\n \"[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5\",\n \"[&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.875em]\",\n // Nhắc tên: cùng cặp với khối `@Tên` của `Mention` (nền `info/15`, chữ thường) — đã đo ở mention.tsx.\n \"[&_[data-type=mention]]:rounded-sm [&_[data-type=mention]]:bg-info/15 [&_[data-type=mention]]:px-0.5\",\n \"[&_[data-type=mention]]:py-px [&_[data-type=mention]]:font-medium [&_[data-type=mention]]:text-foreground\"\n];\n\n/*\n * Danh sách thường. Danh sách việc (`ul[data-type=taskList]`) bỏ chấm đầu dòng và tự vẽ ô tick: DOM\n * do `TaskItem` dựng là `li[data-checked] > label > input + span(sr-only)` cạnh `div` nội dung.\n * ⚠ `<li>` KHÔNG mang `data-type=\"taskItem\"`: thuộc tính đó chỉ có trong `renderHTML`, còn node view\n * (thứ thật sự vẽ trong trình soạn) không gắn nó — đã đo trên 3.31.4, selector `li[data-type=…]`\n * trượt hết và ô tick nằm một dòng, chữ rơi xuống dòng dưới. Nhắm qua `ul` cha.\n */\nconst LISTS = [\n \"[&_ul:not([data-type=taskList])]:list-disc [&_ol]:list-decimal\",\n \"[&_ul:not([data-type=taskList])]:pl-6 [&_ol]:pl-6 [&_li]:marker:text-muted-foreground\",\n \"[&_li+li]:mt-1 [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_li>p]:my-0\",\n \"[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0.5\",\n \"[&_[data-type=taskList]>li]:flex [&_[data-type=taskList]>li]:items-start [&_[data-type=taskList]>li]:gap-2\",\n \"[&_[data-type=taskList]>li>label]:flex [&_[data-type=taskList]>li>label]:h-[1.625em] [&_[data-type=taskList]>li>label]:items-center\",\n \"[&_[data-type=taskList]>li>label]:select-none [&_[data-type=taskList]>li>div]:min-w-0 [&_[data-type=taskList]>li>div]:flex-1\",\n // Soạn được: con trỏ tay trên ô tick. Chỉ đọc: ô không nhận chuột — Tiptap để nó bấm được rồi tự lật\n // lại (không có `onReadOnlyChecked`), nên một cú bấm \"không ăn\" là thứ duy nhất người dùng thấy.\n \"[&[contenteditable=true]_[data-type=taskList]>li>label>input]:cursor-pointer\",\n \"[&[contenteditable=false]_[data-type=taskList]>li>label]:pointer-events-none\",\n // Mục đã xong: chữ nhạt + gạch — CHỈ đoạn của chính mục, không lan xuống danh sách con chưa xong.\n \"[&_li[data-checked=true]>div>p]:text-muted-foreground [&_li[data-checked=true]>div>p]:line-through\"\n];\n\n/*\n * Ô tick vẽ bằng CSS trên chính `<input>` (`appearance-none`), dấu tick là một chữ L xoay 45° vẽ\n * bằng viền của `::before` — không ảnh, không SVG, không mã màu: mọi màu là token của `Checkbox`\n * (`--choice-edge` viền nghỉ, `--control-on` nền khi chọn, `--control-check` dấu tick).\n */\nconst TASK_CHECKBOX = [\n \"[&_[data-type=taskList]>li>label>input]:relative [&_[data-type=taskList]>li>label>input]:m-0 [&_[data-type=taskList]>li>label>input]:grid\",\n \"[&_[data-type=taskList]>li>label>input]:size-4 [&_[data-type=taskList]>li>label>input]:shrink-0 [&_[data-type=taskList]>li>label>input]:appearance-none\",\n \"[&_[data-type=taskList]>li>label>input]:place-content-center [&_[data-type=taskList]>li>label>input]:rounded-[4px]\",\n \"[&_[data-type=taskList]>li>label>input]:border [&_[data-type=taskList]>li>label>input]:border-choice-edge\",\n \"[&_[data-type=taskList]>li>label>input]:bg-transparent [&_[data-type=taskList]>li>label>input]:transition-colors\",\n \"[&_[data-type=taskList]>li>label>input:checked]:border-control-on [&_[data-type=taskList]>li>label>input:checked]:bg-control-on\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-2 [&_[data-type=taskList]>li>label>input:focus-visible]:outline-offset-2\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-ring\",\n // Dấu tick CHỈ tồn tại khi đã tick (`content` chỉ có ở `:checked`) — không ẩn bằng `opacity-0`: một\n // dấu vô hình vẫn là một nét mà cổng màu phải đo, và đo ra 1,00:1.\n \"[&_[data-type=taskList]>li>label>input:checked]:before:h-2 [&_[data-type=taskList]>li>label>input:checked]:before:w-1\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:-translate-y-px [&_[data-type=taskList]>li>label>input:checked]:before:rotate-45\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-r-2 [&_[data-type=taskList]>li>label>input:checked]:before:border-b-2\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-control-check\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:content-['']\"\n];\n\n/*\n * Trích dẫn: vạch trái bằng `--muted-foreground` ĐẶC. `--border` đúng vai trò trang trí nhưng 1,16:1\n * ở light — gần như vô hình, trích dẫn chỉ còn là một đoạn chữ xám thụt vào. Bản `/40` dễ nhìn hơn\n * nhưng 1,71:1 trên `--muted` (light) / 1,87:1 (dark): cổng chặn ở bảng tương phản cao. Bản đặc qua\n * 3:1 ở cả bốn tổ hợp bảng màu × theme (đo bằng `pnpm lint:a11y`).\n */\nconst BLOCKS = [\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground\",\n \"[&_blockquote>*+*]:mt-2\",\n // Đường kẻ và lưới bảng (dưới) dùng `--input`, không `--border` — lý do ở chỗ khai `TABLES`.\n \"[&_hr]:my-1 [&_hr]:border-input\",\n // Khối mã XUỐNG DÒNG (`pre-wrap`) thay vì cuộn ngang: trong trình soạn, một thanh cuộn ngang lồng\n // trong khung cuộn dọc là chỗ con trỏ biến mất khỏi tầm nhìn khi gõ. Đó cũng là CSS Tiptap chèn sẵn\n // (`.ProseMirror pre`) — bản trước ghi `whitespace-pre` ở đây, và nó thua luật đó mà không ai thấy.\n \"[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:px-4 [&_pre]:py-3\",\n \"[&_pre]:font-mono [&_pre]:text-[0.8125rem] [&_pre]:leading-relaxed [&_pre]:whitespace-pre-wrap\",\n \"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit\"\n];\n\n/*\n * Tô màu cú pháp (lớp `hljs-*` do lowlight sinh). Phân vai kiểu bảng màu sáng quen thuộc của các\n * trình xem mã: từ khoá đỏ, chuỗi xanh đậm, hằng/số xanh ngọc, tên hàm cam, chú thích nhạt + nghiêng.\n * Toàn bậc `-ink`, nên đổi sang dark hay bảng tương phản cao là token tự đổi theo.\n */\nconst SYNTAX = [\n \"[&_.hljs-comment]:text-muted-foreground [&_.hljs-comment]:italic [&_.hljs-quote]:text-muted-foreground\",\n \"[&_.hljs-keyword]:text-red-ink [&_.hljs-selector-tag]:text-red-ink [&_.hljs-doctag]:text-red-ink\",\n \"[&_.hljs-meta_.hljs-keyword]:text-red-ink [&_.hljs-template-tag]:text-red-ink\",\n \"[&_.hljs-string]:text-navy-ink [&_.hljs-regexp]:text-navy-ink [&_.hljs-meta_.hljs-string]:text-navy-ink\",\n \"[&_.hljs-number]:text-teal-ink [&_.hljs-literal]:text-teal-ink [&_.hljs-attr]:text-teal-ink\",\n \"[&_.hljs-attribute]:text-teal-ink [&_.hljs-variable]:text-teal-ink [&_.hljs-template-variable]:text-teal-ink\",\n \"[&_.hljs-selector-attr]:text-teal-ink [&_.hljs-selector-class]:text-teal-ink [&_.hljs-selector-id]:text-teal-ink\",\n \"[&_.hljs-title]:text-orange-ink [&_.hljs-section]:text-orange-ink [&_.hljs-name]:text-orange-ink\",\n \"[&_.hljs-type]:text-orange-ink [&_.hljs-built_in]:text-orange-ink [&_.hljs-symbol]:text-orange-ink\",\n \"[&_.hljs-meta]:text-muted-foreground [&_.hljs-bullet]:text-orange-ink [&_.hljs-link]:underline\",\n \"[&_.hljs-addition]:text-success-ink [&_.hljs-deletion]:text-destructive-ink\",\n \"[&_.hljs-emphasis]:italic [&_.hljs-strong]:font-semibold\"\n];\n\n/*\n * Bảng. Ô bảng của `MarkdownEditor` chỉ chứa MỘT đoạn văn (xem `ComitorTableCell`); ô của\n * `RichTextEditor` chứa được nhiều khối (đoạn, danh sách, ảnh) — `> * + *` cho chúng một nhịp ngắn\n * hơn nhịp của tài liệu, như khối trong trích dẫn.\n * Cuộn ngang ở `.tableWrapper` (Tiptap bọc sẵn) — bảng rộng không được đẩy cả trang tràn ngang ở 375px.\n *\n * Lưới là `border-input`, không `border-border`: khung `outline` ở dark nằm trên `--input-fill` (`--input`\n * 30%), mà `--border` của dark (#1e2128) gần như TRÙNG màu nền đó — đo ở harness `3.6.0`: viền ô\n * rgb(30,33,40) trên nền ≈ rgb(30,34,42), cả lưới biến mất, bảng chỉ còn là chữ xếp cột. `--input` là bậc\n * viền của chính bề mặt nhập ấy; ở bảng màu sáng hai token cùng giá trị, nên light không đổi gì.\n */\nconst TABLES = [\n \"[&_.tableWrapper]:overflow-x-auto\",\n \"[&_table]:w-full [&_table]:border-collapse [&_table]:text-left\",\n \"[&_td]:border [&_td]:border-input [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top\",\n \"[&_th]:border [&_th]:border-input [&_th]:bg-muted [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:align-top [&_th]:font-medium\",\n \"[&_td>p]:my-0 [&_th>p]:my-0 [&_td>*+*]:mt-2 [&_th>*+*]:mt-2\",\n \"[&_.selectedCell]:bg-accent [&_.selectedCell]:text-accent-foreground\"\n];\n\nconst MEDIA = [\n \"[&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border\",\n // Ảnh đang tải lên — một widget decoration, không phải node (xem `image-upload.ts`): đứng thành\n // một khối riêng, mờ và nhấp nháy tới khi ảnh thật được chèn vào chỗ nó.\n \"[&_[data-upload-preview]]:my-2 [&_[data-upload-preview]]:block [&_[data-upload-preview]]:w-fit\",\n \"[&_[data-upload-preview]>img]:animate-pulse [&_[data-upload-preview]>img]:opacity-60\",\n // Node được chọn bằng bấm (ảnh, đường kẻ): viền focus của gói, không phải nền.\n \"[&_.ProseMirror-selectednode]:outline-2 [&_.ProseMirror-selectednode]:outline-offset-2 [&_.ProseMirror-selectednode]:outline-ring\"\n];\n\n/*\n * Chữ mờ khi tài liệu rỗng — `Placeholder` gắn `.is-editor-empty` + `data-placeholder` lên đoạn\n * đầu. Đoạn đang gõ dở sau `/` hoặc `@` mang `.suggestion`: tô nền nhẹ để thấy chữ nào đang là lệnh.\n * Con trỏ khe (`gapcursor`, giữa hai khối không gõ được như bảng/ảnh): CSS mặc định Tiptap chèn\n * là viền `black` — vô hình ở dark, nên đổi sang `--foreground`.\n */\nconst AFFORDANCES = [\n \"[&_.is-editor-empty:first-child]:before:pointer-events-none [&_.is-editor-empty:first-child]:before:float-left\",\n \"[&_.is-editor-empty:first-child]:before:h-0 [&_.is-editor-empty:first-child]:before:text-muted-foreground\",\n \"[&_.is-editor-empty:first-child]:before:content-[attr(data-placeholder)]\",\n \"[&_.suggestion]:rounded-sm [&_.suggestion]:bg-muted\",\n \"[&_.ProseMirror-gapcursor]:after:border-foreground\"\n];\n\n/** Toàn bộ lớp của phần tử gốc ProseMirror (`.tiptap`). */\nexport const MARKDOWN_EDITOR_CONTENT_CLASS = [\n ...LAYOUT,\n ...HEADINGS,\n ...INLINE,\n ...LISTS,\n ...TASK_CHECKBOX,\n ...BLOCKS,\n ...SYNTAX,\n ...TABLES,\n ...MEDIA,\n ...AFFORDANCES\n].join(\" \");\n"],"mappings":"AAkBA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,WAAW;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,SAAS;AAAA,EACb;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF;AASA,MAAM,QAAQ;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAOA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AACF;AAOA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAaA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,QAAQ;AAAA,EACZ;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAQA,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,gCAAgC;AAAA,EAC3C,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL,EAAE,KAAK,GAAG;","names":[]}
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Nhãn của `MarkdownEditor` — file RIÊNG, không `"use client"`.
2
+ * Nhãn của `MarkdownEditor` VÀ `RichTextEditor` (cùng namespace `markdownEditor` — tên có từ `3.5.0`,
3
+ * trước khi có trình soạn thứ hai; đổi tên là breaking) — file RIÊNG, không `"use client"`.
3
4
  *
4
5
  * Cùng khuôn `file-upload-constants.ts`: `verify:directives` chặn hằng dữ liệu thuần nằm trong file
5
6
  * `"use client"` (Server Component đọc nó sẽ ra `undefined`, không lỗi, không cảnh báo). File này
@@ -25,13 +26,18 @@ interface MarkdownEditorLabels {
25
26
  * của app → `<label for={id}>` tìm được trong trang → khoá này.
26
27
  */
27
28
  editorLabel: string;
28
- /** Tên của thanh định dạng nổi (`role="toolbar"`) */
29
+ /** Tên của thanh định dạng (`role="toolbar"`) — thanh nổi lẫn thanh cố định */
29
30
  toolbar: string;
30
31
  bold: string;
31
32
  italic: string;
33
+ /** Chỉ `RichTextEditor` có — markdown không có cú pháp gạch chân */
34
+ underline: string;
32
35
  strike: string;
33
36
  code: string;
34
37
  link: string;
38
+ /** Hai nút đầu của thanh cố định (`toolbar="fixed"`) */
39
+ undo: string;
40
+ redo: string;
35
41
  text: string;
36
42
  heading1: string;
37
43
  heading2: string;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/editor/markdown-editor-constants.ts"],"sourcesContent":["/**\n * Nhãn của `MarkdownEditor` — file RIÊNG, không `\"use client\"`.\n *\n * Cùng khuôn `file-upload-constants.ts`: `verify:directives` chặn hằng dữ liệu thuần nằm trong file\n * `\"use client\"` (Server Component đọc nó sẽ ra `undefined`, không lỗi, không cảnh báo). File này\n * KHÔNG được nhập `@tiptap/*`: namespace `markdownEditor` của `src/i18n` nhập KIỂU từ đây, và i18n\n * là tầng của entry CHÍNH — một import chạy được tới peer tuỳ chọn ở đây là kéo Tiptap vào mọi app.\n *\n * Chữ theo `ComitorLocaleProvider` (namespace `markdownEditor`, mặc định tiếng Anh, bảng tiếng Việt\n * dựng sẵn); override từng khoá qua prop `labels`.\n */\n\nimport { markdownEditorEn } from \"../i18n/namespaces/markdown-editor.js\";\n\n/**\n * Các khối chèn được bằng menu `/`. Cũng là khoá của `slashKeywords` — thêm một khối mà quên từ khoá\n * là typecheck đỏ, không phải một khối mà gõ tiếng Việt không bao giờ tìm ra.\n *\n * Năm khối `table*` cuối chỉ hiện khi con trỏ ĐANG ở trong bảng: chúng là thao tác trên bảng có sẵn,\n * không phải khối mới — nhưng menu `/` là chỗ duy nhất có trong tay khi con trỏ đứng ở một ô.\n */\nexport type MarkdownEditorBlockId =\n | \"text\"\n | \"heading1\"\n | \"heading2\"\n | \"heading3\"\n | \"bulletList\"\n | \"orderedList\"\n | \"taskList\"\n | \"blockquote\"\n | \"codeBlock\"\n | \"table\"\n | \"divider\"\n | \"image\"\n | \"tableAddRow\"\n | \"tableAddColumn\"\n | \"tableDeleteRow\"\n | \"tableDeleteColumn\"\n | \"tableDelete\";\n\nexport interface MarkdownEditorLabels {\n /** Chữ mờ khi tài liệu rỗng — prop `placeholder` thắng */\n placeholder: string;\n /**\n * Tên trợ năng của vùng soạn khi app KHÔNG đặt nhãn nào. Thứ tự: `aria-label` / `aria-labelledby`\n * của app → `<label for={id}>` tìm được trong trang → khoá này.\n */\n editorLabel: string;\n\n /** Tên của thanh định dạng nổi (`role=\"toolbar\"`) */\n toolbar: string;\n bold: string;\n italic: string;\n strike: string;\n code: string;\n link: string;\n\n /*\n * Tên khối — dùng CHUNG cho nút của thanh định dạng và mục của menu `/`: cùng một thao tác thì\n * cùng một tên ở mọi chỗ, nên không có cặp khoá \"toolbarHeading1\" / \"slashHeading1\" để lệch nhau.\n */\n text: string;\n heading1: string;\n heading2: string;\n heading3: string;\n bulletList: string;\n orderedList: string;\n taskList: string;\n blockquote: string;\n codeBlock: string;\n table: string;\n divider: string;\n image: string;\n tableAddRow: string;\n tableAddColumn: string;\n tableDeleteRow: string;\n tableDeleteColumn: string;\n tableDelete: string;\n\n /** Tên của danh sách khối (`role=\"listbox\"`) mở bằng `/` */\n slashMenu: string;\n /** Khi chữ gõ sau `/` không khớp khối nào */\n slashEmpty: string;\n /**\n * Từ đồng nghĩa để TÌM khối, cách nhau bằng dấu cách. So khớp không dấu, không phân biệt hoa\n * thường (cùng luật `Combobox`), cộng với tên khối và bí danh máy tiếng Anh (`h1`, `ul`, `todo`…)\n * luôn có sẵn — nên bảng tiếng Việt chỉ cần những chữ người Việt thật sự gõ (\"gạch đầu dòng\").\n */\n slashKeywords: Record<MarkdownEditorBlockId, string>;\n\n /** Nhãn (sr-only) của ô nhập địa chỉ liên kết */\n linkInput: string;\n linkPlaceholder: string;\n linkApply: string;\n linkEdit: string;\n linkOpen: string;\n linkRemove: string;\n /** Khi địa chỉ không phải `http(s)` / `mailto` / `tel` — gói từ chối `javascript:` và họ hàng */\n linkInvalid: string;\n\n /** Tên của danh sách người (`role=\"listbox\"`) mở bằng `@` */\n mentionMenu: string;\n mentionEmpty: string;\n mentionLoading: string;\n\n /** Tên trợ năng của ảnh đang tải lên (widget `role=\"img\"` hiện tới khi app trả `src`) */\n imageUploading: string;\n /** Toast mặc định khi `onImageUpload` ném lỗi mà app không truyền `onImageUploadError` */\n imageUploadFailed: string;\n\n /**\n * Tên trợ năng của ô tick trong danh sách việc. HÀM vì nó chèn nội dung của chính mục đó — trật tự\n * từ là việc của từng ngôn ngữ. `text` rỗng khi mục chưa có chữ.\n */\n taskCheckbox: (text: string) => string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định của `MarkdownEditor`. Bảng tiếng Việt: `comitorMessagesVi.markdownEditor`.\n * Component không đọc thẳng hằng này — nó đọc bảng của `ComitorLocaleProvider`; hằng ở đây để app\n * trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_MARKDOWN_EDITOR_LABELS: MarkdownEditorLabels = { ...markdownEditorEn };\n"],"mappings":"AAYA,SAAS,wBAAwB;AA8G1B,MAAM,iCAAuD,EAAE,GAAG,iBAAiB;","names":[]}
1
+ {"version":3,"sources":["../../src/editor/markdown-editor-constants.ts"],"sourcesContent":["/**\n * Nhãn của `MarkdownEditor` VÀ `RichTextEditor` (cùng namespace `markdownEditor` — tên có từ `3.5.0`,\n * trước khi có trình soạn thứ hai; đổi tên là breaking) — file RIÊNG, không `\"use client\"`.\n *\n * Cùng khuôn `file-upload-constants.ts`: `verify:directives` chặn hằng dữ liệu thuần nằm trong file\n * `\"use client\"` (Server Component đọc nó sẽ ra `undefined`, không lỗi, không cảnh báo). File này\n * KHÔNG được nhập `@tiptap/*`: namespace `markdownEditor` của `src/i18n` nhập KIỂU từ đây, và i18n\n * là tầng của entry CHÍNH — một import chạy được tới peer tuỳ chọn ở đây là kéo Tiptap vào mọi app.\n *\n * Chữ theo `ComitorLocaleProvider` (namespace `markdownEditor`, mặc định tiếng Anh, bảng tiếng Việt\n * dựng sẵn); override từng khoá qua prop `labels`.\n */\n\nimport { markdownEditorEn } from \"../i18n/namespaces/markdown-editor.js\";\n\n/**\n * Các khối chèn được bằng menu `/`. Cũng là khoá của `slashKeywords` — thêm một khối mà quên từ khoá\n * là typecheck đỏ, không phải một khối mà gõ tiếng Việt không bao giờ tìm ra.\n *\n * Năm khối `table*` cuối chỉ hiện khi con trỏ ĐANG ở trong bảng: chúng là thao tác trên bảng có sẵn,\n * không phải khối mới — nhưng menu `/` là chỗ duy nhất có trong tay khi con trỏ đứng ở một ô.\n */\nexport type MarkdownEditorBlockId =\n | \"text\"\n | \"heading1\"\n | \"heading2\"\n | \"heading3\"\n | \"bulletList\"\n | \"orderedList\"\n | \"taskList\"\n | \"blockquote\"\n | \"codeBlock\"\n | \"table\"\n | \"divider\"\n | \"image\"\n | \"tableAddRow\"\n | \"tableAddColumn\"\n | \"tableDeleteRow\"\n | \"tableDeleteColumn\"\n | \"tableDelete\";\n\nexport interface MarkdownEditorLabels {\n /** Chữ mờ khi tài liệu rỗng — prop `placeholder` thắng */\n placeholder: string;\n /**\n * Tên trợ năng của vùng soạn khi app KHÔNG đặt nhãn nào. Thứ tự: `aria-label` / `aria-labelledby`\n * của app → `<label for={id}>` tìm được trong trang → khoá này.\n */\n editorLabel: string;\n\n /** Tên của thanh định dạng (`role=\"toolbar\"`) — thanh nổi lẫn thanh cố định */\n toolbar: string;\n bold: string;\n italic: string;\n /** Chỉ `RichTextEditor` có — markdown không có cú pháp gạch chân */\n underline: string;\n strike: string;\n code: string;\n link: string;\n /** Hai nút đầu của thanh cố định (`toolbar=\"fixed\"`) */\n undo: string;\n redo: string;\n\n /*\n * Tên khối — dùng CHUNG cho nút của thanh định dạng và mục của menu `/`: cùng một thao tác thì\n * cùng một tên ở mọi chỗ, nên không có cặp khoá \"toolbarHeading1\" / \"slashHeading1\" để lệch nhau.\n */\n text: string;\n heading1: string;\n heading2: string;\n heading3: string;\n bulletList: string;\n orderedList: string;\n taskList: string;\n blockquote: string;\n codeBlock: string;\n table: string;\n divider: string;\n image: string;\n tableAddRow: string;\n tableAddColumn: string;\n tableDeleteRow: string;\n tableDeleteColumn: string;\n tableDelete: string;\n\n /** Tên của danh sách khối (`role=\"listbox\"`) mở bằng `/` */\n slashMenu: string;\n /** Khi chữ gõ sau `/` không khớp khối nào */\n slashEmpty: string;\n /**\n * Từ đồng nghĩa để TÌM khối, cách nhau bằng dấu cách. So khớp không dấu, không phân biệt hoa\n * thường (cùng luật `Combobox`), cộng với tên khối và bí danh máy tiếng Anh (`h1`, `ul`, `todo`…)\n * luôn có sẵn — nên bảng tiếng Việt chỉ cần những chữ người Việt thật sự gõ (\"gạch đầu dòng\").\n */\n slashKeywords: Record<MarkdownEditorBlockId, string>;\n\n /** Nhãn (sr-only) của ô nhập địa chỉ liên kết */\n linkInput: string;\n linkPlaceholder: string;\n linkApply: string;\n linkEdit: string;\n linkOpen: string;\n linkRemove: string;\n /** Khi địa chỉ không phải `http(s)` / `mailto` / `tel` — gói từ chối `javascript:` và họ hàng */\n linkInvalid: string;\n\n /** Tên của danh sách người (`role=\"listbox\"`) mở bằng `@` */\n mentionMenu: string;\n mentionEmpty: string;\n mentionLoading: string;\n\n /** Tên trợ năng của ảnh đang tải lên (widget `role=\"img\"` hiện tới khi app trả `src`) */\n imageUploading: string;\n /** Toast mặc định khi `onImageUpload` ném lỗi mà app không truyền `onImageUploadError` */\n imageUploadFailed: string;\n\n /**\n * Tên trợ năng của ô tick trong danh sách việc. HÀM vì nó chèn nội dung của chính mục đó — trật tự\n * từ là việc của từng ngôn ngữ. `text` rỗng khi mục chưa có chữ.\n */\n taskCheckbox: (text: string) => string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định của `MarkdownEditor`. Bảng tiếng Việt: `comitorMessagesVi.markdownEditor`.\n * Component không đọc thẳng hằng này — nó đọc bảng của `ComitorLocaleProvider`; hằng ở đây để app\n * trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_MARKDOWN_EDITOR_LABELS: MarkdownEditorLabels = { ...markdownEditorEn };\n"],"mappings":"AAaA,SAAS,wBAAwB;AAmH1B,MAAM,iCAAuD,EAAE,GAAG,iBAAiB;","names":[]}
@@ -1,7 +1,7 @@
1
- import { MarkdownEditorImageUploadResult } from './image-upload.js';
2
- import { AnyExtension, Editor } from '@tiptap/core';
3
1
  import * as React from 'react';
4
- import { MarkdownEditorLabels } from './markdown-editor-constants.js';
2
+ import { g as EditorBaseProps, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-BMST7-VR.js';
3
+ import '@tiptap/core';
4
+ import './markdown-editor-constants.js';
5
5
 
6
6
  /**
7
7
  * MarkdownEditor — trình soạn văn bản giàu định dạng kiểu Linear, LƯU bằng markdown.
@@ -11,6 +11,10 @@ import { MarkdownEditorLabels } from './markdown-editor-constants.js';
11
11
  * `@` mở danh sách người, bôi chọn chữ là hiện thanh định dạng. Giá trị vào/ra là CHUỖI markdown
12
12
  * (GFM) — thứ module lưu ở cột mô tả và thứ công cụ MCP đọc/ghi.
13
13
  *
14
+ * Cần lưu HTML (trang CMS, nội dung hiển thị lại ngoài trình soạn)? Đó là `RichTextEditor` — cùng
15
+ * lõi (`editor-core.tsx`), lược đồ rộng hơn. Chọn theo thứ CỘT LƯU mang, không theo giao diện: cả hai
16
+ * đều có `toolbar="bubble" | "fixed"`.
17
+ *
14
18
  * Dựng trên Tiptap 3 (ProseMirror — cùng nền với trình soạn của Linear). Lý do chọn, các phương án
15
19
  * đã loại, và những chỗ đã vá `@tiptap/markdown`: docs/editor.md.
16
20
  *
@@ -18,53 +22,14 @@ import { MarkdownEditorLabels } from './markdown-editor-constants.js';
18
22
  * (chúng ghim nhau bằng số chính xác). Lệnh cài ở `src/editor/index.ts`.
19
23
  */
20
24
 
21
- /** Một người trong danh sách `@`. */
22
- interface MarkdownEditorMentionItem {
23
- /** Định danh LƯU vào markdown (`[@Tên](mention:<id>)`) — với module Comitor là id người dùng của Account */
24
- id: string;
25
- /** Tên hiển thị lúc chọn — cũng là ảnh chụp tên ghi kèm trong markdown */
26
- label: string;
27
- /** Dòng phụ mờ bên phải (email, chức danh) — cũng được dùng để lọc */
28
- description?: string;
29
- avatarUrl?: string | null;
30
- }
31
- /**
32
- * Nguồn danh sách `@`:
33
- * · 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;
34
- * · 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
35
- * 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.)
36
- */
37
- type MarkdownEditorMentionSource = readonly MarkdownEditorMentionItem[] | ((query: string, options: {
38
- signal: AbortSignal;
39
- }) => readonly MarkdownEditorMentionItem[] | Promise<readonly MarkdownEditorMentionItem[]>);
40
- interface MarkdownEditorMentions {
41
- items: MarkdownEditorMentionSource;
42
- /**
43
- * Tên SỐNG theo id, dùng lúc VẼ. Markdown giữ tên chụp lúc nhắc; người đó đổi tên ở Account thì
44
- * 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.
45
- */
46
- resolveLabel?: (id: string) => string | undefined;
47
- }
48
25
  /** Điều khiển trình soạn từ ngoài — lấy qua prop `ref`. */
49
- interface MarkdownEditorHandle {
50
- focus: (position?: "start" | "end") => void;
26
+ interface MarkdownEditorHandle extends EditorHandleBase {
51
27
  /** Markdown hiện tại — cùng chuỗi mà `onChange` nhận. */
52
28
  getMarkdown: () => string;
53
29
  /** Thay toàn bộ nội dung. Có gọi `onChange`. */
54
30
  setMarkdown: (markdown: string) => void;
55
- /** Xoá trắng — vd sau khi gửi bình luận. Có gọi `onChange("")`. */
56
- clear: () => void;
57
- isEmpty: () => boolean;
58
- /**
59
- * Còn ảnh đang tải lên không. Ảnh chưa tải xong KHÔNG có trong markdown — nút Gửi nên chờ (hoặc
60
- * khoá) khi hàm này trả `true`, nếu không bình luận đi mà thiếu ảnh.
61
- */
62
- isUploading: () => boolean;
63
- /** 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. */
64
- readonly editor: Editor | null;
65
31
  }
66
- type MarkdownEditorVariant = "outline" | "ghost";
67
- interface MarkdownEditorProps {
32
+ interface MarkdownEditorProps extends EditorBaseProps {
68
33
  /** Markdown — điều khiển từ ngoài. Đổi giá trị KHÁC thứ trình soạn vừa phát là nạp lại nội dung. */
69
34
  value?: string;
70
35
  /** Markdown ban đầu khi KHÔNG điều khiển. */
@@ -72,54 +37,12 @@ interface MarkdownEditorProps {
72
37
  /** Mỗi lần nội dung đổi, nhận markdown đã chuẩn hoá. */
73
38
  onChange?: (markdown: string) => void;
74
39
  /**
75
- * ⌘/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ì
76
- * phím giữ nghĩa mặc định.
77
- */
78
- onSubmit?: () => void;
79
- onFocus?: () => void;
80
- onBlur?: () => void;
81
- /** Chữ mờ khi rỗng. Mặc định theo `ComitorLocaleProvider`. */
82
- placeholder?: string;
83
- /** `false` = chế độ ĐỌC: không con trỏ, không menu, liên kết bấm mở được. Đổi lúc chạy được. */
84
- editable?: boolean;
85
- autoFocus?: boolean;
86
- /**
87
- * `outline` (mặc định) — khung như `Textarea`, cho biểu mẫu và ô bình luận.
88
- * `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.
89
- */
90
- variant?: MarkdownEditorVariant;
91
- /** 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. */
92
- mentions?: MarkdownEditorMentions;
93
- /**
94
- * Bật ảnh: dán, kéo thả, và mục "Ảnh" của menu `/`. Nhận `File`, trả `src` để LƯU vào markdown —
95
- * khoá trong kho, hoặc một route ổn định của app; KHÔNG trả URL đã ký có hạn (xem `resolveImageSrc`).
96
- */
97
- onImageUpload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;
98
- /** Lỗi tải ảnh. Mặc định: toast `imageUploadFailed` (cần `Toaster` ở gốc app). */
99
- onImageUploadError?: (error: unknown, file: File) => void;
100
- /** Đổi `src` ĐÃ LƯU thành địa chỉ HIỂN THỊ (vd URL đã ký) lúc vẽ. Markdown vẫn giữ `src` gốc. */
101
- resolveImageSrc?: (src: string) => string;
102
- /**
103
- * Extension Tiptap thêm vào (vd node `KEY-n` của module). Chỉ đọc LÚC MOUNT — đổi danh sách là
104
- * 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.
105
- * Phải cùng phiên bản `@tiptap/*` với gói.
40
+ * `bubble` (mặc định) — kiểu Linear: định dạng hiện ở chỗ bôi chọn. `fixed` — thanh dính đầu khung,
41
+ * cho người soạn không thuộc phím tắt (mô tả dài, trang nội bộ). Xem `EditorToolbarMode`.
106
42
  */
107
- extensions?: readonly AnyExtension[];
108
- labels?: Partial<MarkdownEditorLabels>;
109
- /** Có `name` thì kèm một `<input type="hidden">` mang markdown — để trình soạn nằm trong `<form>` thật. */
110
- name?: string;
111
- /** Đặ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. */
112
- id?: string;
113
- "aria-label"?: string;
114
- "aria-labelledby"?: string;
115
- "aria-describedby"?: string;
116
- "aria-invalid"?: boolean | "true" | "false";
117
- "aria-required"?: boolean | "true" | "false";
118
- className?: string;
119
- /** Lớp thêm cho vùng soạn (phần tử ProseMirror) — vd `min-h-40`, `max-h-96 overflow-y-auto`. */
120
- contentClassName?: string;
43
+ toolbar?: EditorToolbarMode;
121
44
  ref?: React.Ref<MarkdownEditorHandle>;
122
45
  }
123
- declare function MarkdownEditor({ 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, ref }: MarkdownEditorProps): React.JSX.Element;
46
+ declare function MarkdownEditor({ toolbar, ref, ...props }: MarkdownEditorProps): React.JSX.Element;
124
47
 
125
- export { MarkdownEditor, type MarkdownEditorHandle, type MarkdownEditorMentionItem, type MarkdownEditorMentionSource, type MarkdownEditorMentions, type MarkdownEditorProps, type MarkdownEditorVariant };
48
+ export { MarkdownEditor, type MarkdownEditorHandle, type MarkdownEditorProps };