@comitor/ui 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -5
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/data-grid/use-data-grid.d.ts +2 -2
- package/dist/editor/bubble-toolbar.d.ts +34 -0
- package/dist/editor/bubble-toolbar.js +391 -0
- package/dist/editor/bubble-toolbar.js.map +1 -0
- package/dist/editor/image-upload.d.ts +57 -0
- package/dist/editor/image-upload.js +136 -0
- package/dist/editor/image-upload.js.map +1 -0
- package/dist/editor/index.d.ts +5 -0
- package/dist/editor/index.js +11 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/markdown-editor-classes.d.ts +16 -0
- package/dist/editor/markdown-editor-classes.js +117 -0
- package/dist/editor/markdown-editor-classes.js.map +1 -0
- package/dist/editor/markdown-editor-constants.d.ts +92 -0
- package/dist/editor/markdown-editor-constants.js +6 -0
- package/dist/editor/markdown-editor-constants.js.map +1 -0
- package/dist/editor/markdown-editor.d.ts +125 -0
- package/dist/editor/markdown-editor.js +325 -0
- package/dist/editor/markdown-editor.js.map +1 -0
- package/dist/editor/markdown-extensions.d.ts +71 -0
- package/dist/editor/markdown-extensions.js +172 -0
- package/dist/editor/markdown-extensions.js.map +1 -0
- package/dist/editor/markdown-syntax.d.ts +87 -0
- package/dist/editor/markdown-syntax.js +58 -0
- package/dist/editor/markdown-syntax.js.map +1 -0
- package/dist/editor/slash-commands.d.ts +62 -0
- package/dist/editor/slash-commands.js +186 -0
- package/dist/editor/slash-commands.js.map +1 -0
- package/dist/editor/suggestion-menu.d.ts +38 -0
- package/dist/editor/suggestion-menu.js +144 -0
- package/dist/editor/suggestion-menu.js.map +1 -0
- package/dist/editor/suggestion-store.d.ts +47 -0
- package/dist/editor/suggestion-store.js +49 -0
- package/dist/editor/suggestion-store.js.map +1 -0
- package/dist/i18n/locale-provider.d.ts +1 -0
- package/dist/i18n/messages.d.ts +2 -0
- package/dist/i18n/messages.js +3 -0
- package/dist/i18n/messages.js.map +1 -1
- package/dist/i18n/namespaces/markdown-editor.d.ts +6 -0
- package/dist/i18n/namespaces/markdown-editor.js +127 -0
- package/dist/i18n/namespaces/markdown-editor.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/package.json +79 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ component ghép sẵn và khung ứng dụng (shell) kiểu workspace × app.
|
|
|
10
10
|
|
|
11
11
|
> Trạng thái: `3.0.0` — mặc định **tiếng Anh**, tiếng Việt dựng sẵn (§5) · 79 file primitive (Tier 1,
|
|
12
12
|
> gồm 18 component port từ [Dice UI](docs/diceui.md)) · 33 file composite (Tier 2) · 25 file shell
|
|
13
|
-
> (Tier 3) · 4 file chart · 32 file data-grid · 5 file uploader ·
|
|
13
|
+
> (Tier 3) · 4 file chart · 32 file data-grid · 5 file uploader · 11 file editor · 44 file ngôn ngữ (`src/i18n`). API
|
|
14
14
|
> chốt từ `1.0.0`. (Đếm bằng `ls src/<tầng>`; con số trôi theo mỗi bản — đếm lại, đừng chép.)
|
|
15
15
|
>
|
|
16
16
|
> Tài liệu đầy đủ: **https://design-system.comitor.ai** —
|
|
@@ -41,6 +41,11 @@ pnpm add recharts # cho @comitor/ui/chart
|
|
|
41
41
|
pnpm add react-hook-form # cho @comitor/ui/form
|
|
42
42
|
pnpm add @tanstack/react-table # cho @comitor/ui/data-grid
|
|
43
43
|
pnpm add @uppy/core @uppy/xhr-upload # cho @comitor/ui/uploader
|
|
44
|
+
# cho @comitor/ui/editor — MỌI gói @tiptap/* CÙNG một phiên bản (docs/editor.md §3)
|
|
45
|
+
pnpm add @tiptap/core @tiptap/pm @tiptap/react @tiptap/starter-kit @tiptap/markdown \
|
|
46
|
+
@tiptap/extensions @tiptap/extension-list @tiptap/extension-table @tiptap/extension-image \
|
|
47
|
+
@tiptap/extension-mention @tiptap/suggestion @tiptap/extension-paragraph \
|
|
48
|
+
@tiptap/extension-code-block-lowlight lowlight highlight.js
|
|
44
49
|
```
|
|
45
50
|
|
|
46
51
|
| Peer | Range | Bắt buộc | Cần khi |
|
|
@@ -55,6 +60,8 @@ pnpm add @uppy/core @uppy/xhr-upload # cho @comitor/ui/uploader
|
|
|
55
60
|
| `@tanstack/react-table` | `>=8.21.0 <9` | — | `/data-grid` (app và gói phải dùng **cùng một bản**, nếu không `ColumnDef` app tự khai sẽ lệch kiểu) |
|
|
56
61
|
| `@uppy/core` | `^6.0.0` | — | `/uploader` |
|
|
57
62
|
| `@uppy/xhr-upload` | `^6.0.0` | — | `/uploader` |
|
|
63
|
+
| `@tiptap/*` (13 gói, xem lệnh trên) | `^3.31.0` | — | `/editor` — mọi gói **cùng một phiên bản** (chúng ghim `@tiptap/core`/`@tiptap/pm` bằng số chính xác), và app tự viết extension phải dùng **chính** bản đó |
|
|
64
|
+
| `lowlight` | `^3.0.0` | — | `/editor` (tô màu khối mã; kéo theo `highlight.js` `^11`) |
|
|
58
65
|
|
|
59
66
|
> **Vì sao `recharts` và `lucide-react` là `>=` chứ không phải `^`.** `^2.15.0` loại thẳng recharts
|
|
60
67
|
> 3 — mà 3.x chạy tốt với gói này: kiểu của tầng `/chart` không dẫn xuất `payload`/`label` từ
|
|
@@ -113,7 +120,7 @@ buộc ở thẻ `<html>`: https://design-system.comitor.ai/shell
|
|
|
113
120
|
|
|
114
121
|
## 3. Entry point
|
|
115
122
|
|
|
116
|
-
Package có **
|
|
123
|
+
Package có **8 entry**, tách theo peer optional mà mỗi tầng kéo theo:
|
|
117
124
|
|
|
118
125
|
| Entry | Nội dung | Peer optional kéo theo |
|
|
119
126
|
|---|---|---|
|
|
@@ -122,6 +129,7 @@ Package có **7 entry**, tách theo peer optional mà mỗi tầng kéo theo:
|
|
|
122
129
|
| `@comitor/ui/chart` | Lớp shadcn bọc recharts + `Comitor*Chart` + `CHART_COLORS` | `recharts` (~100KB) |
|
|
123
130
|
| `@comitor/ui/data-grid` | `DataGrid` — lưới sửa trực tiếp kiểu bảng tính: ảo hoá dòng, chọn vùng bằng bàn phím, copy/paste với Excel, ghim cột, lọc/sắp xếp nhiều tiêu chí, hoàn tác/làm lại | `@tanstack/react-table` (peer); kèm `@tanstack/react-virtual` + `@dnd-kit/*` (dependency của gói) |
|
|
124
131
|
| `@comitor/ui/uploader` | `FileUpload` (tệp tài liệu) · `ImageUploadField` (ảnh đại diện, logo) · `useFileUpload` | `@uppy/core`, `@uppy/xhr-upload` (~13KB gzip) |
|
|
132
|
+
| `@comitor/ui/editor` | `MarkdownEditor` — trình soạn kiểu Linear, lưu bằng markdown: gõ tắt, menu `/`, `@` nhắc người, thanh định dạng nổi, ảnh, bảng, khối mã tô màu | `@tiptap/*` + `lowlight` (~234KB gzip — nạp lười nếu trang không cần ngay) |
|
|
125
133
|
| `@comitor/ui/form` | Binding shadcn ⇄ react-hook-form | `react-hook-form` |
|
|
126
134
|
| `@comitor/ui/tokens` | Token dạng object TS — cho nơi không có Tailwind: canvas, email HTML, PDF | không có |
|
|
127
135
|
|
|
@@ -168,7 +176,7 @@ https://design-system.comitor.ai/foundations/server-client
|
|
|
168
176
|
|
|
169
177
|
## 5. Bảng component
|
|
170
178
|
|
|
171
|
-
Số file nguồn hiện tại: **79 primitive · 33 composite · 25 shell · 4 chart · 32 data-grid · 5 uploader ·
|
|
179
|
+
Số file nguồn hiện tại: **79 primitive · 33 composite · 25 shell · 4 chart · 32 data-grid · 5 uploader · 11 editor · 44 i18n**. Mỗi component có trang
|
|
172
180
|
riêng kèm demo, bảng prop và cạm bẫy của nó:
|
|
173
181
|
|
|
174
182
|
| Tầng | Là gì | Trang |
|
|
@@ -179,6 +187,7 @@ riêng kèm demo, bảng prop và cạm bẫy của nó:
|
|
|
179
187
|
| **chart** · 4 file · `@comitor/ui/chart` | Lớp shadcn bọc recharts, cùng ba bản dựng sẵn theo brand. ⚠ Ba wrapper `Comitor*Chart` **mặc định TẮT** hiệu ứng vẽ dần (`isAnimationActive = false`) — bật lại là biểu đồ kẹt ở khung đầu: có trục, có lưới, KHÔNG có dữ liệu. Mặc định đó KHÔNG áp cho `<ChartContainer>` + recharts do app tự lắp | https://design-system.comitor.ai/components/chart |
|
|
180
188
|
| **data-grid** · 32 file · `@comitor/ui/data-grid` | `DataGrid` — lưới sửa trực tiếp kiểu bảng tính, cho màn nhập liệu hàng loạt. Port từ [tablecn](https://www.tablecn.com/data-grid) (MIT) sang primitive + token của gói. ⚠ Là tầng DUY NHẤT có peer **`@tanstack/react-table`**, có thêm prop `locale` riêng (xem dưới), và mọi thứ trong nó là client component | [docs/data-grid.md](docs/data-grid.md) |
|
|
181
189
|
| **uploader** · 5 file · `@comitor/ui/uploader` | `FileUpload` (tệp) · `ImageUploadField` (ảnh) · `useFileUpload` — kéo thả, xem trước, tiến độ. Peer optional: `@uppy/core` + `@uppy/xhr-upload` | https://design-system.comitor.ai/components |
|
|
190
|
+
| **editor** · 11 file · `@comitor/ui/editor` | `MarkdownEditor` — soạn trực tiếp trên bản đã định dạng, giá trị vào/ra là markdown (GFM). Dựng trên Tiptap 3 (ProseMirror). ⚠ Định dạng LƯU là hợp đồng: nhắc người là `[@Tên](mention:<id>)`, ảnh lưu `src` app trả về (không phải URL đã ký), gạch chân cố ý không có. Peer optional: bộ `@tiptap/*` + `lowlight` | [docs/editor.md](docs/editor.md) |
|
|
182
191
|
| **form** · `@comitor/ui/form` | Binding shadcn ⇄ react-hook-form | https://design-system.comitor.ai/components/form |
|
|
183
192
|
|
|
184
193
|
**Ngôn ngữ — mặc định TIẾNG ANH, tiếng Việt dựng sẵn (từ `3.0.0`).** Mọi chuỗi hiển thị của gói —
|
|
@@ -280,7 +289,7 @@ https://design-system.comitor.ai/foundations/colors
|
|
|
280
289
|
|
|
281
290
|
Mỗi bản publish đi qua cùng một chuỗi cổng (`prepublishOnly` nối chúng lại): typecheck → build →
|
|
282
291
|
`verify:directives` → `smoke:entries` → `check:exports` → `test:nav-active` → `test:grid-contract`
|
|
283
|
-
→ `test:grid-render` → `test:a11y-gate` → `lint:a11y`. Những cổng đáng để người dùng gói biết là chúng tồn tại:
|
|
292
|
+
→ `test:grid-render` → `test:editor-markdown` → `test:a11y-gate` → `lint:a11y`. Những cổng đáng để người dùng gói biết là chúng tồn tại:
|
|
284
293
|
|
|
285
294
|
- **`lint:a11y` — hợp đồng màu.** Quét **toàn bộ** `src/**/*.{ts,tsx}` theo QUY TẮC (không theo danh
|
|
286
295
|
sách phát hiện) và đối chiếu với `styles.css`: token TÔ NỀN đem làm màu chữ/icon/viền là sai
|
|
@@ -300,7 +309,10 @@ Mỗi bản publish đi qua cùng một chuỗi cổng (`prepublishOnly` nối c
|
|
|
300
309
|
- **`verify:directives` — ranh giới server/client.** File nguồn nào mở đầu bằng `"use client"` thì
|
|
301
310
|
file `dist/` tương ứng phải mở đầu y hệt (thiếu file build cũng tính là hỏng), và không hằng dữ
|
|
302
311
|
liệu thuần nào được kẹt trong file client (§4.2).
|
|
303
|
-
- **`smoke:entries`** nạp cả
|
|
312
|
+
- **`smoke:entries`** nạp cả 8 entry bằng Node ESM thuần, qua đúng `exports` map (§3).
|
|
313
|
+
- **`test:editor-markdown`** — markdown là định dạng LƯU của `MarkdownEditor`, nên phép đo là tài liệu →
|
|
314
|
+
markdown → tài liệu phải ra đúng tài liệu ban đầu, trên CHÍNH lược đồ trong `dist/`. Lỗi loại này không lộ
|
|
315
|
+
ra lúc gõ mà ở lần mở sau. Trên lược đồ gốc của Tiptap (chưa vá) cổng trượt 16/46 — [docs/editor.md](docs/editor.md) §5.
|
|
304
316
|
- **`test:a11y-gate`** tự kiểm chính cổng màu (cổng còn bắt đủ không); **`test:nav-active`** khoá
|
|
305
317
|
toàn bộ luật active matching của sidebar dưới dạng hàm thuần (§4.1).
|
|
306
318
|
- **`test:grid-render`** render THẬT `DataGrid` bằng `react-dom/server` rồi soi HTML ra được. Nó
|
|
@@ -18,7 +18,7 @@ interface ItemProps extends React.ComponentProps<"div">, VariantProps<typeof ite
|
|
|
18
18
|
}
|
|
19
19
|
declare function Item({ className, variant, size, asChild, ...props }: ItemProps): React.JSX.Element;
|
|
20
20
|
declare const itemMediaVariants: (props?: ({
|
|
21
|
-
variant?: "
|
|
21
|
+
variant?: "image" | "default" | "icon" | null | undefined;
|
|
22
22
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
23
23
|
interface ItemMediaProps extends React.ComponentProps<"div">, VariantProps<typeof itemMediaVariants> {
|
|
24
24
|
}
|
|
@@ -122,7 +122,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
|
|
|
122
122
|
pasteDialog: PasteDialogState;
|
|
123
123
|
onRowAdd: ((event?: React.MouseEvent<HTMLDivElement>) => Promise<void>) | undefined;
|
|
124
124
|
adjustLayout: boolean;
|
|
125
|
-
locale: {
|
|
125
|
+
locale: "en" | "vi" | {
|
|
126
126
|
grid: {
|
|
127
127
|
label: string;
|
|
128
128
|
addRow: string;
|
|
@@ -397,7 +397,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
|
|
|
397
397
|
shortcuts: string;
|
|
398
398
|
};
|
|
399
399
|
};
|
|
400
|
-
} |
|
|
400
|
+
} | undefined;
|
|
401
401
|
};
|
|
402
402
|
|
|
403
403
|
export { type UseDataGridProps, useDataGrid };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { MarkdownEditorLabels } from './markdown-editor-constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thanh định dạng NỔI — hiện khi bôi chọn chữ, như Linear: không có thanh công cụ cố định chiếm chỗ
|
|
7
|
+
* trên đầu tài liệu, định dạng ở ngay chỗ đang chọn.
|
|
8
|
+
*
|
|
9
|
+
* Ba trạng thái trong cùng một bong bóng:
|
|
10
|
+
* · đang chọn chữ → nút định dạng (dấu nội dòng + đổi khối);
|
|
11
|
+
* · con trỏ đứng TRONG một liên kết (không chọn gì) → địa chỉ + Mở / Sửa / Gỡ;
|
|
12
|
+
* · đang sửa liên kết (nút Liên kết, hoặc ⌘K) → ô nhập địa chỉ.
|
|
13
|
+
*
|
|
14
|
+
* Vị trí do `BubbleMenu` của Tiptap tính (Floating UI). Nó gắn vào CHA của vùng soạn chứ không vào
|
|
15
|
+
* `document.body`, nên ở trong `Dialog` vẫn nằm trong lớp của hộp thoại.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Chuẩn hoá địa chỉ người dùng gõ vào ô liên kết. Trả `null` khi KHÔNG nhận.
|
|
20
|
+
*
|
|
21
|
+
* Nhận: `http(s):`, `mailto:`, `tel:`, đường dẫn trong app (`/tasks/CV-12`, `#muc`), và hai dạng gõ
|
|
22
|
+
* tắt — tên miền trần (`comitor.ai/docs` → `https://…`), email trần (→ `mailto:`). Từ chối MỌI lược
|
|
23
|
+
* đồ khác: `javascript:`, `data:`, `vbscript:` là ba cách biến một liên kết thành mã chạy được.
|
|
24
|
+
*/
|
|
25
|
+
declare function normalizeLinkHref(input: string): string | null;
|
|
26
|
+
interface BubbleToolbarProps {
|
|
27
|
+
editor: Editor;
|
|
28
|
+
labels: MarkdownEditorLabels;
|
|
29
|
+
/** Tăng mỗi lần ⌘/Ctrl+K — mở ô liên kết. Số đếm thay vì cờ để hai lần ⌘K liền nhau vẫn là hai lần. */
|
|
30
|
+
linkRequest: number;
|
|
31
|
+
}
|
|
32
|
+
declare function BubbleToolbar({ editor, labels, linkRequest }: BubbleToolbarProps): React.JSX.Element | null;
|
|
33
|
+
|
|
34
|
+
export { BubbleToolbar, type BubbleToolbarProps, normalizeLinkHref };
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { NodeSelection } from "@tiptap/pm/state";
|
|
4
|
+
import { useEditorState } from "@tiptap/react";
|
|
5
|
+
import { BubbleMenu } from "@tiptap/react/menus";
|
|
6
|
+
import {
|
|
7
|
+
Bold,
|
|
8
|
+
Check,
|
|
9
|
+
Code,
|
|
10
|
+
ExternalLink,
|
|
11
|
+
Heading1,
|
|
12
|
+
Heading2,
|
|
13
|
+
Heading3,
|
|
14
|
+
Italic,
|
|
15
|
+
Link2,
|
|
16
|
+
Link2Off,
|
|
17
|
+
List,
|
|
18
|
+
ListChecks,
|
|
19
|
+
ListOrdered,
|
|
20
|
+
Pencil,
|
|
21
|
+
SquareCode,
|
|
22
|
+
Strikethrough,
|
|
23
|
+
TextQuote
|
|
24
|
+
} from "lucide-react";
|
|
25
|
+
import * as React from "react";
|
|
26
|
+
import { KeyboardHint } from "../components/composite/keyboard-hint.js";
|
|
27
|
+
import { Separator } from "../components/ui/separator.js";
|
|
28
|
+
import { Toggle } from "../components/ui/toggle.js";
|
|
29
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
|
|
30
|
+
import { cn } from "../lib/cn.js";
|
|
31
|
+
import { useAsRef } from "../lib/use-as-ref.js";
|
|
32
|
+
function normalizeLinkHref(input) {
|
|
33
|
+
const value = input.trim();
|
|
34
|
+
if (!value) return null;
|
|
35
|
+
if (/^(?:https?:\/\/|mailto:|tel:)/i.test(value)) return value;
|
|
36
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return null;
|
|
37
|
+
if (/^[/#]/.test(value)) return value;
|
|
38
|
+
if (/^[^\s@/]+@[^\s@/]+\.[^\s@/]+$/.test(value)) return `mailto:${value}`;
|
|
39
|
+
if (/^[^\s/]+\.[^\s/.]{2,}(?:[/?#]\S*)?$/.test(value)) return `https://${value}`;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const MARK_ACTIONS = [
|
|
43
|
+
{
|
|
44
|
+
id: "bold",
|
|
45
|
+
icon: Bold,
|
|
46
|
+
label: "bold",
|
|
47
|
+
keys: "mod+b",
|
|
48
|
+
isActive: (editor) => editor.isActive("bold"),
|
|
49
|
+
run: (editor) => editor.chain().focus().toggleBold().run()
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "italic",
|
|
53
|
+
icon: Italic,
|
|
54
|
+
label: "italic",
|
|
55
|
+
keys: "mod+i",
|
|
56
|
+
isActive: (editor) => editor.isActive("italic"),
|
|
57
|
+
run: (editor) => editor.chain().focus().toggleItalic().run()
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "strike",
|
|
61
|
+
icon: Strikethrough,
|
|
62
|
+
label: "strike",
|
|
63
|
+
keys: "mod+shift+s",
|
|
64
|
+
isActive: (editor) => editor.isActive("strike"),
|
|
65
|
+
run: (editor) => editor.chain().focus().toggleStrike().run()
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "code",
|
|
69
|
+
icon: Code,
|
|
70
|
+
label: "code",
|
|
71
|
+
keys: "mod+e",
|
|
72
|
+
isActive: (editor) => editor.isActive("code"),
|
|
73
|
+
run: (editor) => editor.chain().focus().toggleCode().run()
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
const BLOCK_ACTIONS = [
|
|
77
|
+
{
|
|
78
|
+
id: "heading1",
|
|
79
|
+
icon: Heading1,
|
|
80
|
+
label: "heading1",
|
|
81
|
+
keys: "mod+alt+1",
|
|
82
|
+
isActive: (editor) => editor.isActive("heading", { level: 1 }),
|
|
83
|
+
run: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run()
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "heading2",
|
|
87
|
+
icon: Heading2,
|
|
88
|
+
label: "heading2",
|
|
89
|
+
keys: "mod+alt+2",
|
|
90
|
+
isActive: (editor) => editor.isActive("heading", { level: 2 }),
|
|
91
|
+
run: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run()
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "heading3",
|
|
95
|
+
icon: Heading3,
|
|
96
|
+
label: "heading3",
|
|
97
|
+
keys: "mod+alt+3",
|
|
98
|
+
isActive: (editor) => editor.isActive("heading", { level: 3 }),
|
|
99
|
+
run: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run()
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "bulletList",
|
|
103
|
+
icon: List,
|
|
104
|
+
label: "bulletList",
|
|
105
|
+
keys: "mod+shift+8",
|
|
106
|
+
isActive: (editor) => editor.isActive("bulletList"),
|
|
107
|
+
run: (editor) => editor.chain().focus().toggleBulletList().run()
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "orderedList",
|
|
111
|
+
icon: ListOrdered,
|
|
112
|
+
label: "orderedList",
|
|
113
|
+
keys: "mod+shift+7",
|
|
114
|
+
isActive: (editor) => editor.isActive("orderedList"),
|
|
115
|
+
run: (editor) => editor.chain().focus().toggleOrderedList().run()
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "taskList",
|
|
119
|
+
icon: ListChecks,
|
|
120
|
+
label: "taskList",
|
|
121
|
+
keys: "mod+shift+9",
|
|
122
|
+
isActive: (editor) => editor.isActive("taskList"),
|
|
123
|
+
run: (editor) => editor.chain().focus().toggleTaskList().run()
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "blockquote",
|
|
127
|
+
icon: TextQuote,
|
|
128
|
+
label: "blockquote",
|
|
129
|
+
keys: "mod+shift+b",
|
|
130
|
+
isActive: (editor) => editor.isActive("blockquote"),
|
|
131
|
+
run: (editor) => editor.chain().focus().toggleBlockquote().run()
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "codeBlock",
|
|
135
|
+
icon: SquareCode,
|
|
136
|
+
label: "codeBlock",
|
|
137
|
+
keys: "mod+alt+c",
|
|
138
|
+
isActive: (editor) => editor.isActive("codeBlock"),
|
|
139
|
+
run: (editor) => editor.chain().focus().toggleCodeBlock().run()
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
function readToolbarState(editor) {
|
|
143
|
+
const active = {};
|
|
144
|
+
for (const action of [...MARK_ACTIONS, ...BLOCK_ACTIONS]) active[action.id] = action.isActive(editor);
|
|
145
|
+
const href = editor.getAttributes("link").href;
|
|
146
|
+
return {
|
|
147
|
+
active,
|
|
148
|
+
link: editor.isActive("link"),
|
|
149
|
+
href: typeof href === "string" ? href : "",
|
|
150
|
+
empty: editor.state.selection.empty,
|
|
151
|
+
inTable: editor.isActive("table")
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function sameToolbarState(a, b) {
|
|
155
|
+
if (!a || !b) return a === b;
|
|
156
|
+
if (a.link !== b.link || a.href !== b.href || a.empty !== b.empty || a.inTable !== b.inTable) return false;
|
|
157
|
+
return Object.keys(a.active).every((key) => a.active[key] === b.active[key]);
|
|
158
|
+
}
|
|
159
|
+
const BUTTON_CLASS = "h-7 min-w-7 px-1.5";
|
|
160
|
+
function ToolbarButton({
|
|
161
|
+
icon: Icon,
|
|
162
|
+
label,
|
|
163
|
+
keys,
|
|
164
|
+
pressed,
|
|
165
|
+
onClick,
|
|
166
|
+
className
|
|
167
|
+
}) {
|
|
168
|
+
const button = pressed === void 0 ? /* @__PURE__ */ jsx(
|
|
169
|
+
"button",
|
|
170
|
+
{
|
|
171
|
+
type: "button",
|
|
172
|
+
"aria-label": label,
|
|
173
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
174
|
+
onClick,
|
|
175
|
+
className: cn(
|
|
176
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors duration-150 ease-out hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0",
|
|
177
|
+
BUTTON_CLASS,
|
|
178
|
+
className
|
|
179
|
+
),
|
|
180
|
+
children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" })
|
|
181
|
+
}
|
|
182
|
+
) : /* @__PURE__ */ jsx(
|
|
183
|
+
Toggle,
|
|
184
|
+
{
|
|
185
|
+
"aria-label": label,
|
|
186
|
+
pressed,
|
|
187
|
+
onPressedChange: onClick,
|
|
188
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
189
|
+
className: cn(BUTTON_CLASS, className),
|
|
190
|
+
children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" })
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
return /* @__PURE__ */ jsxs(Tooltip, { delayDuration: 400, children: [
|
|
194
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: button }),
|
|
195
|
+
/* @__PURE__ */ jsxs(TooltipContent, { side: "top", sideOffset: 6, className: "flex items-center gap-2", children: [
|
|
196
|
+
label,
|
|
197
|
+
keys && /* @__PURE__ */ jsx(KeyboardHint, { keys, compact: true })
|
|
198
|
+
] })
|
|
199
|
+
] });
|
|
200
|
+
}
|
|
201
|
+
function BubbleToolbar({ editor, labels, linkRequest }) {
|
|
202
|
+
const state = useEditorState({
|
|
203
|
+
editor,
|
|
204
|
+
selector: ({ editor: current }) => readToolbarState(current),
|
|
205
|
+
equalityFn: sameToolbarState
|
|
206
|
+
});
|
|
207
|
+
const [editingLink, setEditingLink] = React.useState(false);
|
|
208
|
+
const editingLinkRef = useAsRef(editingLink);
|
|
209
|
+
const [draft, setDraft] = React.useState("");
|
|
210
|
+
const [invalid, setInvalid] = React.useState(false);
|
|
211
|
+
const inputRef = React.useRef(null);
|
|
212
|
+
const pluginKey = "comitorBubbleToolbar";
|
|
213
|
+
const showBubble = React.useCallback(() => {
|
|
214
|
+
editor.view.dispatch(editor.state.tr.setMeta(pluginKey, "show"));
|
|
215
|
+
editor.view.dispatch(editor.state.tr.setMeta(pluginKey, "updatePosition"));
|
|
216
|
+
}, [editor]);
|
|
217
|
+
const openLinkEditor = React.useCallback(() => {
|
|
218
|
+
const href = editor.getAttributes("link").href;
|
|
219
|
+
setDraft(typeof href === "string" ? href : "");
|
|
220
|
+
setInvalid(false);
|
|
221
|
+
setEditingLink(true);
|
|
222
|
+
}, [editor]);
|
|
223
|
+
React.useEffect(() => {
|
|
224
|
+
if (linkRequest === 0) return;
|
|
225
|
+
openLinkEditor();
|
|
226
|
+
}, [linkRequest, openLinkEditor]);
|
|
227
|
+
React.useEffect(() => {
|
|
228
|
+
if (!editingLink) return;
|
|
229
|
+
showBubble();
|
|
230
|
+
const frame = requestAnimationFrame(() => inputRef.current?.select());
|
|
231
|
+
return () => cancelAnimationFrame(frame);
|
|
232
|
+
}, [editingLink, showBubble]);
|
|
233
|
+
const closeLinkEditor = (refocus) => {
|
|
234
|
+
setEditingLink(false);
|
|
235
|
+
setInvalid(false);
|
|
236
|
+
if (refocus) editor.commands.focus();
|
|
237
|
+
};
|
|
238
|
+
const applyLink = () => {
|
|
239
|
+
const href = normalizeLinkHref(draft);
|
|
240
|
+
if (draft.trim() === "") {
|
|
241
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
242
|
+
closeLinkEditor(false);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!href) {
|
|
246
|
+
setInvalid(true);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const chain = editor.chain().focus().extendMarkRange("link");
|
|
250
|
+
if (editor.state.selection.empty && !editor.isActive("link")) {
|
|
251
|
+
chain.insertContent({ type: "text", text: draft.trim(), marks: [{ type: "link", attrs: { href } }] }).run();
|
|
252
|
+
} else {
|
|
253
|
+
chain.setLink({ href }).run();
|
|
254
|
+
}
|
|
255
|
+
closeLinkEditor(false);
|
|
256
|
+
};
|
|
257
|
+
const removeLink = () => {
|
|
258
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
259
|
+
closeLinkEditor(false);
|
|
260
|
+
};
|
|
261
|
+
const shouldShow = React.useCallback(
|
|
262
|
+
({ editor: current, element, view, state: editorState, from, to }) => {
|
|
263
|
+
if (!current.isEditable) return false;
|
|
264
|
+
if (!(view.hasFocus() || element.contains(document.activeElement))) return false;
|
|
265
|
+
if (editingLinkRef.current) return true;
|
|
266
|
+
const { selection } = editorState;
|
|
267
|
+
if (selection instanceof NodeSelection || current.isActive("codeBlock")) return false;
|
|
268
|
+
if (selection.empty) return current.isActive("link");
|
|
269
|
+
return editorState.doc.textBetween(from, to, " ").trim().length > 0;
|
|
270
|
+
},
|
|
271
|
+
[editingLinkRef]
|
|
272
|
+
);
|
|
273
|
+
const options = React.useMemo(
|
|
274
|
+
() => ({
|
|
275
|
+
// `fixed`: không bị cắt bởi khung cuộn chứa trình soạn (`DialogBody`, cột chi tiết cuộn riêng).
|
|
276
|
+
strategy: "fixed",
|
|
277
|
+
placement: "top",
|
|
278
|
+
offset: 8,
|
|
279
|
+
flip: true,
|
|
280
|
+
shift: { padding: 8 },
|
|
281
|
+
onHide: () => setEditingLink(false)
|
|
282
|
+
}),
|
|
283
|
+
[]
|
|
284
|
+
);
|
|
285
|
+
if (!state) return null;
|
|
286
|
+
return /* @__PURE__ */ jsx(
|
|
287
|
+
BubbleMenu,
|
|
288
|
+
{
|
|
289
|
+
editor,
|
|
290
|
+
pluginKey,
|
|
291
|
+
updateDelay: 100,
|
|
292
|
+
shouldShow,
|
|
293
|
+
options,
|
|
294
|
+
className: "z-50 rounded-lg border bg-popover p-1 text-popover-foreground shadow-overlay",
|
|
295
|
+
children: editingLink ? /* @__PURE__ */ jsxs(
|
|
296
|
+
"form",
|
|
297
|
+
{
|
|
298
|
+
className: "flex flex-col gap-1",
|
|
299
|
+
onSubmit: (event) => {
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
applyLink();
|
|
302
|
+
},
|
|
303
|
+
children: [
|
|
304
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
305
|
+
/* @__PURE__ */ jsx(
|
|
306
|
+
"input",
|
|
307
|
+
{
|
|
308
|
+
ref: inputRef,
|
|
309
|
+
type: "text",
|
|
310
|
+
inputMode: "url",
|
|
311
|
+
"aria-label": labels.linkInput,
|
|
312
|
+
"aria-invalid": invalid || void 0,
|
|
313
|
+
placeholder: labels.linkPlaceholder,
|
|
314
|
+
value: draft,
|
|
315
|
+
onChange: (event) => {
|
|
316
|
+
setDraft(event.target.value);
|
|
317
|
+
setInvalid(false);
|
|
318
|
+
},
|
|
319
|
+
onKeyDown: (event) => {
|
|
320
|
+
if (event.key === "Escape") {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
event.stopPropagation();
|
|
323
|
+
closeLinkEditor(true);
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
className: "h-7 w-60 rounded-md border border-input bg-transparent px-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50 aria-invalid:border-destructive-ink dark:bg-input-fill"
|
|
327
|
+
}
|
|
328
|
+
),
|
|
329
|
+
/* @__PURE__ */ jsx(ToolbarButton, { icon: Check, label: labels.linkApply, onClick: applyLink }),
|
|
330
|
+
state.link && /* @__PURE__ */ jsx(ToolbarButton, { icon: Link2Off, label: labels.linkRemove, onClick: removeLink })
|
|
331
|
+
] }),
|
|
332
|
+
invalid && /* @__PURE__ */ jsx("p", { role: "alert", className: "px-1 pb-0.5 text-xs text-destructive-ink", children: labels.linkInvalid })
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
) : state.empty && state.link ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
336
|
+
/* @__PURE__ */ jsxs(
|
|
337
|
+
"a",
|
|
338
|
+
{
|
|
339
|
+
href: state.href,
|
|
340
|
+
target: "_blank",
|
|
341
|
+
rel: "noopener noreferrer nofollow",
|
|
342
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
343
|
+
className: "flex max-w-60 items-center gap-1.5 truncate rounded-md px-1.5 py-1 text-sm text-link-ink underline-offset-4 hover:text-link-ink-hover hover:underline",
|
|
344
|
+
children: [
|
|
345
|
+
/* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": "true", className: "size-3.5 shrink-0" }),
|
|
346
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: state.href }),
|
|
347
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.linkOpen })
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
),
|
|
351
|
+
/* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" }),
|
|
352
|
+
/* @__PURE__ */ jsx(ToolbarButton, { icon: Pencil, label: labels.linkEdit, keys: "mod+k", onClick: openLinkEditor }),
|
|
353
|
+
/* @__PURE__ */ jsx(ToolbarButton, { icon: Link2Off, label: labels.linkRemove, onClick: removeLink })
|
|
354
|
+
] }) : /* @__PURE__ */ jsxs("div", { role: "toolbar", "aria-label": labels.toolbar, className: "flex items-center gap-0.5", children: [
|
|
355
|
+
MARK_ACTIONS.map((action) => /* @__PURE__ */ jsx(
|
|
356
|
+
ToolbarButton,
|
|
357
|
+
{
|
|
358
|
+
icon: action.icon,
|
|
359
|
+
label: labels[action.label],
|
|
360
|
+
keys: action.keys,
|
|
361
|
+
pressed: Boolean(state.active[action.id]),
|
|
362
|
+
onClick: () => action.run(editor)
|
|
363
|
+
},
|
|
364
|
+
action.id
|
|
365
|
+
)),
|
|
366
|
+
/* @__PURE__ */ jsx(ToolbarButton, { icon: Link2, label: labels.link, keys: "mod+k", pressed: state.link, onClick: openLinkEditor }),
|
|
367
|
+
!state.inTable && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 max-sm:hidden", children: [
|
|
368
|
+
/* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" }),
|
|
369
|
+
BLOCK_ACTIONS.map((action, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
370
|
+
(index === 3 || index === 6) && /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" }),
|
|
371
|
+
/* @__PURE__ */ jsx(
|
|
372
|
+
ToolbarButton,
|
|
373
|
+
{
|
|
374
|
+
icon: action.icon,
|
|
375
|
+
label: labels[action.label],
|
|
376
|
+
keys: action.keys,
|
|
377
|
+
pressed: Boolean(state.active[action.id]),
|
|
378
|
+
onClick: () => action.run(editor)
|
|
379
|
+
}
|
|
380
|
+
)
|
|
381
|
+
] }, action.id))
|
|
382
|
+
] })
|
|
383
|
+
] })
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
export {
|
|
388
|
+
BubbleToolbar,
|
|
389
|
+
normalizeLinkHref
|
|
390
|
+
};
|
|
391
|
+
//# sourceMappingURL=bubble-toolbar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/editor/bubble-toolbar.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Thanh định dạng NỔI — hiện khi bôi chọn chữ, như Linear: không có thanh công cụ cố định chiếm chỗ\n * trên đầu tài liệu, định dạng ở ngay chỗ đang chọn.\n *\n * Ba trạng thái trong cùng một bong bóng:\n * · đang chọn chữ → nút định dạng (dấu nội dòng + đổi khối);\n * · con trỏ đứng TRONG một liên kết (không chọn gì) → địa chỉ + Mở / Sửa / Gỡ;\n * · đang sửa liên kết (nút Liên kết, hoặc ⌘K) → ô nhập địa chỉ.\n *\n * Vị trí do `BubbleMenu` của Tiptap tính (Floating UI). Nó gắn vào CHA của vùng soạn chứ không vào\n * `document.body`, nên ở trong `Dialog` vẫn nằm trong lớp của hộp thoại.\n */\n\nimport type { Editor } from \"@tiptap/core\";\nimport { NodeSelection } from \"@tiptap/pm/state\";\nimport { useEditorState } from \"@tiptap/react\";\nimport { BubbleMenu, type BubbleMenuProps } from \"@tiptap/react/menus\";\nimport {\n Bold,\n Check,\n Code,\n ExternalLink,\n Heading1,\n Heading2,\n Heading3,\n Italic,\n Link2,\n Link2Off,\n List,\n ListChecks,\n ListOrdered,\n type LucideIcon,\n Pencil,\n SquareCode,\n Strikethrough,\n TextQuote\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { KeyboardHint } from \"../components/composite/keyboard-hint.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Toggle } from \"../components/ui/toggle.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useAsRef } from \"../lib/use-as-ref.js\";\nimport type { MarkdownEditorLabels } from \"./markdown-editor-constants.js\";\n\n/**\n * Chuẩn hoá địa chỉ người dùng gõ vào ô liên kết. Trả `null` khi KHÔNG nhận.\n *\n * Nhận: `http(s):`, `mailto:`, `tel:`, đường dẫn trong app (`/tasks/CV-12`, `#muc`), và hai dạng gõ\n * tắt — tên miền trần (`comitor.ai/docs` → `https://…`), email trần (→ `mailto:`). Từ chối MỌI lược\n * đồ khác: `javascript:`, `data:`, `vbscript:` là ba cách biến một liên kết thành mã chạy được.\n */\nexport function normalizeLinkHref(input: string): string | null {\n const value = input.trim();\n if (!value) return null;\n if (/^(?:https?:\\/\\/|mailto:|tel:)/i.test(value)) return value;\n if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return null;\n if (/^[/#]/.test(value)) return value;\n if (/^[^\\s@/]+@[^\\s@/]+\\.[^\\s@/]+$/.test(value)) return `mailto:${value}`;\n if (/^[^\\s/]+\\.[^\\s/.]{2,}(?:[/?#]\\S*)?$/.test(value)) return `https://${value}`;\n return null;\n}\n\n/** Khoá nhãn mang CHUỖI (loại `slashKeywords` và hàm `taskCheckbox`). */\ntype TextLabelKey = {\n [K in keyof MarkdownEditorLabels]: MarkdownEditorLabels[K] extends string ? K : never;\n}[keyof MarkdownEditorLabels];\n\ninterface ToolbarAction {\n id: string;\n icon: LucideIcon;\n label: TextLabelKey;\n /** Phím tắt cho tooltip — cú pháp của `KeyboardHint` (`mod+b`). Mặc định Tiptap, trừ ⌘K của gói. */\n keys?: string;\n isActive: (editor: Editor) => boolean;\n run: (editor: Editor) => void;\n}\n\nconst MARK_ACTIONS: readonly ToolbarAction[] = [\n {\n id: \"bold\",\n icon: Bold,\n label: \"bold\",\n keys: \"mod+b\",\n isActive: (editor) => editor.isActive(\"bold\"),\n run: (editor) => editor.chain().focus().toggleBold().run()\n },\n {\n id: \"italic\",\n icon: Italic,\n label: \"italic\",\n keys: \"mod+i\",\n isActive: (editor) => editor.isActive(\"italic\"),\n run: (editor) => editor.chain().focus().toggleItalic().run()\n },\n {\n id: \"strike\",\n icon: Strikethrough,\n label: \"strike\",\n keys: \"mod+shift+s\",\n isActive: (editor) => editor.isActive(\"strike\"),\n run: (editor) => editor.chain().focus().toggleStrike().run()\n },\n {\n id: \"code\",\n icon: Code,\n label: \"code\",\n keys: \"mod+e\",\n isActive: (editor) => editor.isActive(\"code\"),\n run: (editor) => editor.chain().focus().toggleCode().run()\n }\n];\n\n/*\n * Đổi khối. Ẩn ở màn hẹp (thanh sẽ rộng hơn 375px) và trong bảng (ô bảng chỉ chứa một đoạn văn —\n * xem `ComitorTableCell`): cả hai chỗ đó vẫn còn cú pháp gõ tắt và menu `/`.\n */\nconst BLOCK_ACTIONS: readonly ToolbarAction[] = [\n {\n id: \"heading1\",\n icon: Heading1,\n label: \"heading1\",\n keys: \"mod+alt+1\",\n isActive: (editor) => editor.isActive(\"heading\", { level: 1 }),\n run: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run()\n },\n {\n id: \"heading2\",\n icon: Heading2,\n label: \"heading2\",\n keys: \"mod+alt+2\",\n isActive: (editor) => editor.isActive(\"heading\", { level: 2 }),\n run: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run()\n },\n {\n id: \"heading3\",\n icon: Heading3,\n label: \"heading3\",\n keys: \"mod+alt+3\",\n isActive: (editor) => editor.isActive(\"heading\", { level: 3 }),\n run: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run()\n },\n {\n id: \"bulletList\",\n icon: List,\n label: \"bulletList\",\n keys: \"mod+shift+8\",\n isActive: (editor) => editor.isActive(\"bulletList\"),\n run: (editor) => editor.chain().focus().toggleBulletList().run()\n },\n {\n id: \"orderedList\",\n icon: ListOrdered,\n label: \"orderedList\",\n keys: \"mod+shift+7\",\n isActive: (editor) => editor.isActive(\"orderedList\"),\n run: (editor) => editor.chain().focus().toggleOrderedList().run()\n },\n {\n id: \"taskList\",\n icon: ListChecks,\n label: \"taskList\",\n keys: \"mod+shift+9\",\n isActive: (editor) => editor.isActive(\"taskList\"),\n run: (editor) => editor.chain().focus().toggleTaskList().run()\n },\n {\n id: \"blockquote\",\n icon: TextQuote,\n label: \"blockquote\",\n keys: \"mod+shift+b\",\n isActive: (editor) => editor.isActive(\"blockquote\"),\n run: (editor) => editor.chain().focus().toggleBlockquote().run()\n },\n {\n id: \"codeBlock\",\n icon: SquareCode,\n label: \"codeBlock\",\n keys: \"mod+alt+c\",\n isActive: (editor) => editor.isActive(\"codeBlock\"),\n run: (editor) => editor.chain().focus().toggleCodeBlock().run()\n }\n];\n\ninterface ToolbarState {\n active: Record<string, boolean>;\n link: boolean;\n href: string;\n empty: boolean;\n inTable: boolean;\n}\n\nfunction readToolbarState(editor: Editor): ToolbarState {\n const active: Record<string, boolean> = {};\n for (const action of [...MARK_ACTIONS, ...BLOCK_ACTIONS]) active[action.id] = action.isActive(editor);\n const href = editor.getAttributes(\"link\").href;\n return {\n active,\n link: editor.isActive(\"link\"),\n href: typeof href === \"string\" ? href : \"\",\n empty: editor.state.selection.empty,\n inTable: editor.isActive(\"table\")\n };\n}\n\n/** So nông — `useEditorState` chỉ render lại khi kết quả KHÁC, mà mỗi lần đọc là một object mới. */\nfunction sameToolbarState(a: ToolbarState | null, b: ToolbarState | null): boolean {\n if (!a || !b) return a === b;\n if (a.link !== b.link || a.href !== b.href || a.empty !== b.empty || a.inTable !== b.inTable) return false;\n return Object.keys(a.active).every((key) => a.active[key] === b.active[key]);\n}\n\n/*\n * Nút nhỏ hơn `Toggle size=\"sm\"` một bậc (28px): mười ba nút của Linear-style toolbar phải nằm gọn\n * trên một dòng ở màn laptop. Mousedown bị chặn để focus Ở LẠI vùng soạn — mất focus là mất vùng chọn\n * mà nút sắp áp định dạng lên.\n */\nconst BUTTON_CLASS = \"h-7 min-w-7 px-1.5\";\n\nfunction ToolbarButton({\n icon: Icon,\n label,\n keys,\n pressed,\n onClick,\n className\n}: {\n icon: LucideIcon;\n label: string;\n keys?: string;\n pressed?: boolean;\n onClick: () => void;\n className?: string;\n}) {\n const button =\n pressed === undefined ? (\n <button\n type=\"button\"\n aria-label={label}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClick}\n className={cn(\n \"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors duration-150 ease-out hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0\",\n BUTTON_CLASS,\n className\n )}\n >\n <Icon aria-hidden=\"true\" />\n </button>\n ) : (\n <Toggle\n aria-label={label}\n pressed={pressed}\n onPressedChange={onClick}\n onMouseDown={(event) => event.preventDefault()}\n className={cn(BUTTON_CLASS, className)}\n >\n <Icon aria-hidden=\"true\" />\n </Toggle>\n );\n\n return (\n <Tooltip delayDuration={400}>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent side=\"top\" sideOffset={6} className=\"flex items-center gap-2\">\n {label}\n {keys && <KeyboardHint keys={keys} compact />}\n </TooltipContent>\n </Tooltip>\n );\n}\n\nexport interface BubbleToolbarProps {\n editor: Editor;\n labels: MarkdownEditorLabels;\n /** Tăng mỗi lần ⌘/Ctrl+K — mở ô liên kết. Số đếm thay vì cờ để hai lần ⌘K liền nhau vẫn là hai lần. */\n linkRequest: number;\n}\n\nexport function BubbleToolbar({ editor, labels, linkRequest }: BubbleToolbarProps) {\n const state = useEditorState({\n editor,\n selector: ({ editor: current }) => readToolbarState(current),\n equalityFn: sameToolbarState\n });\n const [editingLink, setEditingLink] = React.useState(false);\n const editingLinkRef = useAsRef(editingLink);\n const [draft, setDraft] = React.useState(\"\");\n const [invalid, setInvalid] = React.useState(false);\n const inputRef = React.useRef<HTMLInputElement>(null);\n const pluginKey = \"comitorBubbleToolbar\";\n\n /*\n * Hai giao dịch, theo đúng thứ tự: meta `\"show\"` của plugin gọi `updatePosition()` TRƯỚC `show()`,\n * mà `updatePosition()` bỏ qua khi bong bóng đang ẩn — nên một mình `\"show\"` hiện bong bóng ở toạ\n * độ CŨ (lần hiện trước, hoặc góc trái trên). `\"updatePosition\"` sau đó mới tính đúng chỗ.\n */\n const showBubble = React.useCallback(() => {\n editor.view.dispatch(editor.state.tr.setMeta(pluginKey, \"show\"));\n editor.view.dispatch(editor.state.tr.setMeta(pluginKey, \"updatePosition\"));\n }, [editor]);\n\n const openLinkEditor = React.useCallback(() => {\n const href = editor.getAttributes(\"link\").href;\n setDraft(typeof href === \"string\" ? href : \"\");\n setInvalid(false);\n setEditingLink(true);\n }, [editor]);\n\n // ⌘K từ vùng soạn. Bỏ qua giá trị đầu (0) — đó là lúc mount, không phải một lần bấm.\n React.useEffect(() => {\n if (linkRequest === 0) return;\n openLinkEditor();\n }, [linkRequest, openLinkEditor]);\n\n // Vừa vào chế độ sửa: bắt plugin hiện bong bóng (kể cả khi không chọn gì) rồi đưa focus vào ô nhập.\n React.useEffect(() => {\n if (!editingLink) return;\n showBubble();\n const frame = requestAnimationFrame(() => inputRef.current?.select());\n return () => cancelAnimationFrame(frame);\n }, [editingLink, showBubble]);\n\n const closeLinkEditor = (refocus: boolean) => {\n setEditingLink(false);\n setInvalid(false);\n if (refocus) editor.commands.focus();\n };\n\n const applyLink = () => {\n const href = normalizeLinkHref(draft);\n if (draft.trim() === \"\") {\n editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n closeLinkEditor(false);\n return;\n }\n if (!href) {\n setInvalid(true);\n return;\n }\n const chain = editor.chain().focus().extendMarkRange(\"link\");\n if (editor.state.selection.empty && !editor.isActive(\"link\")) {\n // Không chọn chữ nào: chèn chính địa chỉ làm chữ của liên kết.\n chain.insertContent({ type: \"text\", text: draft.trim(), marks: [{ type: \"link\", attrs: { href } }] }).run();\n } else {\n chain.setLink({ href }).run();\n }\n closeLinkEditor(false);\n };\n\n const removeLink = () => {\n editor.chain().focus().extendMarkRange(\"link\").unsetLink().run();\n closeLinkEditor(false);\n };\n\n /*\n * Ổn định qua các lượt render: `BubbleMenu` gửi lại TOÀN BỘ tuỳ chọn vào plugin mỗi khi một prop\n * đổi định danh. `shouldShow` đọc cờ sửa-liên-kết qua ref vì cùng lý do.\n */\n const shouldShow = React.useCallback<NonNullable<BubbleMenuProps[\"shouldShow\"]>>(\n ({ editor: current, element, view, state: editorState, from, to }) => {\n if (!current.isEditable) return false;\n if (!(view.hasFocus() || element.contains(document.activeElement))) return false;\n if (editingLinkRef.current) return true;\n const { selection } = editorState;\n if (selection instanceof NodeSelection || current.isActive(\"codeBlock\")) return false;\n if (selection.empty) return current.isActive(\"link\");\n return editorState.doc.textBetween(from, to, \" \").trim().length > 0;\n },\n [editingLinkRef]\n );\n\n const options = React.useMemo<BubbleMenuProps[\"options\"]>(\n () => ({\n // `fixed`: không bị cắt bởi khung cuộn chứa trình soạn (`DialogBody`, cột chi tiết cuộn riêng).\n strategy: \"fixed\",\n placement: \"top\",\n offset: 8,\n flip: true,\n shift: { padding: 8 },\n onHide: () => setEditingLink(false)\n }),\n []\n );\n\n if (!state) return null;\n\n return (\n <BubbleMenu\n editor={editor}\n pluginKey={pluginKey}\n updateDelay={100}\n shouldShow={shouldShow}\n options={options}\n className=\"z-50 rounded-lg border bg-popover p-1 text-popover-foreground shadow-overlay\"\n >\n {editingLink ? (\n <form\n className=\"flex flex-col gap-1\"\n onSubmit={(event) => {\n event.preventDefault();\n applyLink();\n }}\n >\n <div className=\"flex items-center gap-1\">\n <input\n ref={inputRef}\n type=\"text\"\n inputMode=\"url\"\n aria-label={labels.linkInput}\n aria-invalid={invalid || undefined}\n placeholder={labels.linkPlaceholder}\n value={draft}\n onChange={(event) => {\n setDraft(event.target.value);\n setInvalid(false);\n }}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n // Escape ở đây chỉ đóng ô liên kết — không để nó lan tới `Dialog` bao ngoài.\n event.stopPropagation();\n closeLinkEditor(true);\n }\n }}\n className=\"h-7 w-60 rounded-md border border-input bg-transparent px-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50 aria-invalid:border-destructive-ink dark:bg-input-fill\"\n />\n <ToolbarButton icon={Check} label={labels.linkApply} onClick={applyLink} />\n {state.link && <ToolbarButton icon={Link2Off} label={labels.linkRemove} onClick={removeLink} />}\n </div>\n {invalid && (\n <p role=\"alert\" className=\"px-1 pb-0.5 text-xs text-destructive-ink\">\n {labels.linkInvalid}\n </p>\n )}\n </form>\n ) : state.empty && state.link ? (\n <div className=\"flex items-center gap-1\">\n <a\n href={state.href}\n target=\"_blank\"\n rel=\"noopener noreferrer nofollow\"\n onMouseDown={(event) => event.preventDefault()}\n className=\"flex max-w-60 items-center gap-1.5 truncate rounded-md px-1.5 py-1 text-sm text-link-ink underline-offset-4 hover:text-link-ink-hover hover:underline\"\n >\n <ExternalLink aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n <span className=\"truncate\">{state.href}</span>\n <span className=\"sr-only\">{labels.linkOpen}</span>\n </a>\n <Separator orientation=\"vertical\" className=\"mx-0.5 h-5\" />\n <ToolbarButton icon={Pencil} label={labels.linkEdit} keys=\"mod+k\" onClick={openLinkEditor} />\n <ToolbarButton icon={Link2Off} label={labels.linkRemove} onClick={removeLink} />\n </div>\n ) : (\n <div role=\"toolbar\" aria-label={labels.toolbar} className=\"flex items-center gap-0.5\">\n {MARK_ACTIONS.map((action) => (\n <ToolbarButton\n key={action.id}\n icon={action.icon}\n label={labels[action.label]}\n keys={action.keys}\n pressed={Boolean(state.active[action.id])}\n onClick={() => action.run(editor)}\n />\n ))}\n <ToolbarButton icon={Link2} label={labels.link} keys=\"mod+k\" pressed={state.link} onClick={openLinkEditor} />\n {!state.inTable && (\n <div className=\"flex items-center gap-0.5 max-sm:hidden\">\n <Separator orientation=\"vertical\" className=\"mx-0.5 h-5\" />\n {BLOCK_ACTIONS.map((action, index) => (\n <React.Fragment key={action.id}>\n {(index === 3 || index === 6) && <Separator orientation=\"vertical\" className=\"mx-0.5 h-5\" />}\n <ToolbarButton\n icon={action.icon}\n label={labels[action.label]}\n keys={action.keys}\n pressed={Boolean(state.active[action.id])}\n onClick={() => action.run(editor)}\n />\n </React.Fragment>\n ))}\n </div>\n )}\n </div>\n )}\n </BubbleMenu>\n );\n}\n"],"mappings":"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","names":[]}
|