@comitor/ui 3.6.1 → 3.7.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 +5 -5
- package/dist/components/composite/combobox.d.ts +9 -0
- package/dist/components/composite/combobox.js +16 -3
- package/dist/components/composite/combobox.js.map +1 -1
- package/dist/components/composite/inline-edit.d.ts +7 -1
- package/dist/components/composite/inline-edit.js +10 -1
- package/dist/components/composite/inline-edit.js.map +1 -1
- package/dist/components/composite/key-sequence.d.ts +44 -0
- package/dist/components/composite/key-sequence.js +32 -0
- package/dist/components/composite/key-sequence.js.map +1 -0
- package/dist/components/composite/keyboard-shortcuts-dialog.d.ts +39 -0
- package/dist/components/composite/keyboard-shortcuts-dialog.js +78 -0
- package/dist/components/composite/keyboard-shortcuts-dialog.js.map +1 -0
- package/dist/components/composite/use-key-sequences.d.ts +28 -0
- package/dist/components/composite/use-key-sequences.js +44 -0
- package/dist/components/composite/use-key-sequences.js.map +1 -0
- package/dist/components/ui/badge.d.ts +19 -2
- package/dist/components/ui/badge.js +44 -4
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/components/ui/item.js +13 -2
- package/dist/components/ui/item.js.map +1 -1
- package/dist/editor/bubble-toolbar.d.ts +2 -1
- package/dist/editor/editor-core.d.ts +2 -1
- package/dist/editor/editor-core.js +6 -4
- package/dist/editor/editor-core.js.map +1 -1
- package/dist/editor/editor-types.d.ts +3 -1
- package/dist/editor/fixed-toolbar.d.ts +2 -1
- package/dist/editor/image-upload.d.ts +3 -1
- package/dist/editor/index.d.ts +2 -1
- package/dist/editor/markdown-editor-classes.js +3 -1
- package/dist/editor/markdown-editor-classes.js.map +1 -1
- package/dist/editor/markdown-editor.d.ts +2 -1
- package/dist/editor/markdown-extensions.d.ts +9 -3
- package/dist/editor/markdown-extensions.js +28 -5
- package/dist/editor/markdown-extensions.js.map +1 -1
- package/dist/editor/markdown-syntax.d.ts +3 -1
- package/dist/editor/markdown-syntax.js +10 -4
- package/dist/editor/markdown-syntax.js.map +1 -1
- package/dist/editor/rich-text-editor.d.ts +2 -1
- package/dist/editor/slash-commands.d.ts +3 -1
- package/dist/editor/toolbar-actions.d.ts +3 -1
- package/dist/{editor-types-BMST7-VR.d.ts → editor-types-CSrMeuSw.d.ts} +20 -3
- 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/keyboard-shortcuts.d.ts +13 -0
- package/dist/i18n/namespaces/keyboard-shortcuts.js +15 -0
- package/dist/i18n/namespaces/keyboard-shortcuts.js.map +1 -0
- package/dist/i18n/namespaces/shell.js +4 -2
- package/dist/i18n/namespaces/shell.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/shell/app-shell.d.ts +14 -1
- package/dist/shell/app-shell.js +24 -2
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/display-prefs-seed.d.ts +64 -0
- package/dist/shell/display-prefs-seed.js +58 -0
- package/dist/shell/display-prefs-seed.js.map +1 -0
- package/dist/shell/index.d.ts +1 -0
- package/dist/shell/index.js +1 -0
- package/dist/shell/index.js.map +1 -1
- package/dist/shell/types.d.ts +2 -0
- package/dist/shell/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { KeySequence } from './key-sequence.js';
|
|
2
|
+
|
|
3
|
+
interface UseKeySequencesOptions<Id extends string> {
|
|
4
|
+
/** Sổ phím tắt. Đổi được lúc chạy (vd lọc theo quyền) — listener đọc bản mới nhất, không gắn lại. */
|
|
5
|
+
sequences: readonly KeySequence<Id>[];
|
|
6
|
+
/** Một phím tắt khớp trọn. Phím đã được `preventDefault`. */
|
|
7
|
+
onTrigger: (id: Id) => void;
|
|
8
|
+
/** Tắt tạm (mặc định `true`). */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Khoảng tối đa giữa hai phím của một chuỗi (mặc định `KEY_SEQUENCE_TIMEOUT_MS`). App có luật khác cũng
|
|
12
|
+
* đọc khoảng này (vd phím danh sách nhường chuỗi đang dở) thì truyền CÙNG hằng của app vào đây — hai hằng
|
|
13
|
+
* cho một khái niệm là hai giá trị sớm muộn lệch nhau.
|
|
14
|
+
*/
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Bộ bắt phím tắt TOÀN CỤC dạng chuỗi (`c`, `/`, `g` rồi `m`, `?`) — gắn MỘT lần trong khung app.
|
|
19
|
+
*
|
|
20
|
+
* Bỏ qua: phím đã bị chỗ khác `preventDefault`, phím gõ vào ô nhập / lớp nổi / lưới (`isEditableTarget`),
|
|
21
|
+
* phím có ⌘/Ctrl/Alt (⌘K là của `AppShell`). Phím ĐẦU của một chuỗi cũng bị `preventDefault` — không thì
|
|
22
|
+
* `g` cuộn trang hay mở tìm-nhanh của trình duyệt trước khi `m` kịp tới.
|
|
23
|
+
*
|
|
24
|
+
* Vẽ bảng liệt kê: `KeyboardShortcutsDialog`. Vẽ một phím: `KeyboardHint`.
|
|
25
|
+
*/
|
|
26
|
+
declare function useKeySequences<Id extends string>({ sequences, onTrigger, enabled, timeoutMs }: UseKeySequencesOptions<Id>): void;
|
|
27
|
+
|
|
28
|
+
export { type UseKeySequencesOptions, useKeySequences };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import {
|
|
4
|
+
advanceKeySequence,
|
|
5
|
+
isEditableTarget,
|
|
6
|
+
KEY_SEQUENCE_TIMEOUT_MS,
|
|
7
|
+
normalizeSequenceKey
|
|
8
|
+
} from "./key-sequence.js";
|
|
9
|
+
function useKeySequences({
|
|
10
|
+
sequences,
|
|
11
|
+
onTrigger,
|
|
12
|
+
enabled = true,
|
|
13
|
+
timeoutMs = KEY_SEQUENCE_TIMEOUT_MS
|
|
14
|
+
}) {
|
|
15
|
+
const latest = useRef({ sequences, onTrigger, timeoutMs });
|
|
16
|
+
latest.current = { sequences, onTrigger, timeoutMs };
|
|
17
|
+
const pending = useRef({ keys: [], at: 0 });
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!enabled) return;
|
|
20
|
+
function onKeyDown(event) {
|
|
21
|
+
if (event.defaultPrevented || isEditableTarget(event.target)) return;
|
|
22
|
+
const key = normalizeSequenceKey(event);
|
|
23
|
+
if (key === null) return;
|
|
24
|
+
const { sequences: current, onTrigger: trigger, timeoutMs: timeout } = latest.current;
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const carried = now - pending.current.at <= timeout ? pending.current.keys : [];
|
|
27
|
+
const result = advanceKeySequence(current, carried, key);
|
|
28
|
+
pending.current = { keys: result.pending, at: now };
|
|
29
|
+
if (result.pending.length > 0) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (result.matched === null) return;
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
trigger(result.matched);
|
|
36
|
+
}
|
|
37
|
+
window.addEventListener("keydown", onKeyDown);
|
|
38
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
39
|
+
}, [enabled]);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
useKeySequences
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=use-key-sequences.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/use-key-sequences.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport {\n advanceKeySequence,\n isEditableTarget,\n KEY_SEQUENCE_TIMEOUT_MS,\n type KeySequence,\n normalizeSequenceKey\n} from \"./key-sequence.js\";\n\nexport interface UseKeySequencesOptions<Id extends string> {\n /** Sổ phím tắt. Đổi được lúc chạy (vd lọc theo quyền) — listener đọc bản mới nhất, không gắn lại. */\n sequences: readonly KeySequence<Id>[];\n /** Một phím tắt khớp trọn. Phím đã được `preventDefault`. */\n onTrigger: (id: Id) => void;\n /** Tắt tạm (mặc định `true`). */\n enabled?: boolean;\n /**\n * Khoảng tối đa giữa hai phím của một chuỗi (mặc định `KEY_SEQUENCE_TIMEOUT_MS`). App có luật khác cũng\n * đọc khoảng này (vd phím danh sách nhường chuỗi đang dở) thì truyền CÙNG hằng của app vào đây — hai hằng\n * cho một khái niệm là hai giá trị sớm muộn lệch nhau.\n */\n timeoutMs?: number;\n}\n\n/**\n * Bộ bắt phím tắt TOÀN CỤC dạng chuỗi (`c`, `/`, `g` rồi `m`, `?`) — gắn MỘT lần trong khung app.\n *\n * Bỏ qua: phím đã bị chỗ khác `preventDefault`, phím gõ vào ô nhập / lớp nổi / lưới (`isEditableTarget`),\n * phím có ⌘/Ctrl/Alt (⌘K là của `AppShell`). Phím ĐẦU của một chuỗi cũng bị `preventDefault` — không thì\n * `g` cuộn trang hay mở tìm-nhanh của trình duyệt trước khi `m` kịp tới.\n *\n * Vẽ bảng liệt kê: `KeyboardShortcutsDialog`. Vẽ một phím: `KeyboardHint`.\n */\nexport function useKeySequences<Id extends string>({\n sequences,\n onTrigger,\n enabled = true,\n timeoutMs = KEY_SEQUENCE_TIMEOUT_MS\n}: UseKeySequencesOptions<Id>): void {\n const latest = useRef({ sequences, onTrigger, timeoutMs });\n latest.current = { sequences, onTrigger, timeoutMs };\n const pending = useRef<{ keys: string[]; at: number }>({ keys: [], at: 0 });\n\n useEffect(() => {\n if (!enabled) return;\n function onKeyDown(event: KeyboardEvent) {\n if (event.defaultPrevented || isEditableTarget(event.target)) return;\n const key = normalizeSequenceKey(event);\n if (key === null) return;\n const { sequences: current, onTrigger: trigger, timeoutMs: timeout } = latest.current;\n const now = Date.now();\n const carried = now - pending.current.at <= timeout ? pending.current.keys : [];\n const result = advanceKeySequence(current, carried, key);\n pending.current = { keys: result.pending, at: now };\n if (result.pending.length > 0) {\n event.preventDefault();\n return;\n }\n if (result.matched === null) return;\n event.preventDefault();\n trigger(result.matched);\n }\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [enabled]);\n}\n"],"mappings":";AAEA,SAAS,WAAW,cAAc;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AA0BA,SAAS,gBAAmC;AAAA,EACjD;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,YAAY;AACd,GAAqC;AACnC,QAAM,SAAS,OAAO,EAAE,WAAW,WAAW,UAAU,CAAC;AACzD,SAAO,UAAU,EAAE,WAAW,WAAW,UAAU;AACnD,QAAM,UAAU,OAAuC,EAAE,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;AAE1E,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,aAAS,UAAU,OAAsB;AACvC,UAAI,MAAM,oBAAoB,iBAAiB,MAAM,MAAM,EAAG;AAC9D,YAAM,MAAM,qBAAqB,KAAK;AACtC,UAAI,QAAQ,KAAM;AAClB,YAAM,EAAE,WAAW,SAAS,WAAW,SAAS,WAAW,QAAQ,IAAI,OAAO;AAC9E,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,UAAU,MAAM,QAAQ,QAAQ,MAAM,UAAU,QAAQ,QAAQ,OAAO,CAAC;AAC9E,YAAM,SAAS,mBAAmB,SAAS,SAAS,GAAG;AACvD,cAAQ,UAAU,EAAE,MAAM,OAAO,SAAS,IAAI,IAAI;AAClD,UAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,cAAM,eAAe;AACrB;AAAA,MACF;AACA,UAAI,OAAO,YAAY,KAAM;AAC7B,YAAM,eAAe;AACrB,cAAQ,OAAO,OAAO;AAAA,IACxB;AACA,WAAO,iBAAiB,WAAW,SAAS;AAC5C,WAAO,MAAM,OAAO,oBAAoB,WAAW,SAAS;AAAA,EAC9D,GAAG,CAAC,OAAO,CAAC;AACd;","names":[]}
|
|
@@ -7,9 +7,26 @@ declare const badgeVariants: (props?: ({
|
|
|
7
7
|
size?: "sm" | "md" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
interface BadgeProps extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {
|
|
10
|
-
/** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. */
|
|
10
|
+
/** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. Bị bỏ qua khi có `onRemove`. */
|
|
11
11
|
asChild?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Chip GỠ ĐƯỢC (từ 3.7.0): vẽ một nút ✕ ở cuối badge (người theo dõi, nhãn đã gắn…). Đi cùng
|
|
14
|
+
* `removeLabel` — thiếu thì nút không có tên trợ năng và gói cảnh báo ở console lúc phát triển.
|
|
15
|
+
*/
|
|
16
|
+
onRemove?: () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Tên trợ năng của nút ✕ — gói KHÔNG có nhãn mặc định, cố ý: nút chỉ có icon thì tên là thứ DUY NHẤT nói
|
|
19
|
+
* nó gỡ CÁI GÌ ("Gỡ Lan khỏi người theo dõi"), mà câu đó là chữ nghiệp vụ của app. Một mặc định chung
|
|
20
|
+
* kiểu "Remove" lặp N lần trong một danh sách là N nút trùng tên. (Và nó giữ `badge.tsx` là file tĩnh —
|
|
21
|
+
* đọc nhãn từ `ComitorLocaleProvider` sẽ buộc nó thành client.)
|
|
22
|
+
*
|
|
23
|
+
* Vì sao không ép bằng kiểu (union "có `onRemove` thì bắt buộc `removeLabel`"): `BadgeProps` là interface
|
|
24
|
+
* đã publish, đổi sang union là gãy mọi `interface X extends BadgeProps` của app.
|
|
25
|
+
*/
|
|
26
|
+
removeLabel?: string;
|
|
27
|
+
/** Khoá riêng nút ✕ (vd đang lưu) mà badge vẫn hiện bình thường. */
|
|
28
|
+
removeDisabled?: boolean;
|
|
12
29
|
}
|
|
13
|
-
declare function Badge({ className, variant, size, asChild, ...props }: BadgeProps): React.JSX.Element;
|
|
30
|
+
declare function Badge({ className, variant, size, asChild, onRemove, removeLabel, removeDisabled, children, ...props }: BadgeProps): React.JSX.Element;
|
|
14
31
|
|
|
15
32
|
export { Badge, type BadgeProps, badgeVariants };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
|
+
import { X } from "lucide-react";
|
|
4
5
|
import { cn } from "../../lib/cn.js";
|
|
5
6
|
const badgeVariants = cva(
|
|
6
7
|
"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none",
|
|
@@ -26,9 +27,48 @@ const badgeVariants = cva(
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
);
|
|
29
|
-
function Badge({
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
function Badge({
|
|
31
|
+
className,
|
|
32
|
+
variant,
|
|
33
|
+
size,
|
|
34
|
+
asChild = false,
|
|
35
|
+
onRemove,
|
|
36
|
+
removeLabel,
|
|
37
|
+
removeDisabled,
|
|
38
|
+
children,
|
|
39
|
+
...props
|
|
40
|
+
}) {
|
|
41
|
+
const Comp = asChild && !onRemove ? Slot : "span";
|
|
42
|
+
if (onRemove && !removeLabel && typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
43
|
+
console.warn(
|
|
44
|
+
"[Badge] `onRemove` c\u1EA7n `removeLabel` \u2014 n\xFAt \u2715 ch\u1EC9 c\xF3 icon th\xEC t\xEAn tr\u1EE3 n\u0103ng l\xE0 th\u1EE9 duy nh\u1EA5t n\xF3i n\xF3 g\u1EE1 g\xEC."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return /* @__PURE__ */ jsxs(Comp, { "data-slot": "badge", className: cn(badgeVariants({ variant, size }), onRemove && "pr-1", className), ...props, children: [
|
|
48
|
+
children,
|
|
49
|
+
onRemove ? (
|
|
50
|
+
/*
|
|
51
|
+
* `opacity-70`, KHÔNG `text-x/70`: icon lucide là nhiều nét cắt nhau, alpha trong `color` tô chỗ
|
|
52
|
+
* giao hai lần (docs/hop-dong-mau.md §7b). Vùng bấm `size-4` nằm gọn trong chiều cao badge `md`.
|
|
53
|
+
*/
|
|
54
|
+
/* @__PURE__ */ jsx(
|
|
55
|
+
"button",
|
|
56
|
+
{
|
|
57
|
+
type: "button",
|
|
58
|
+
"data-slot": "badge-remove",
|
|
59
|
+
"aria-label": removeLabel,
|
|
60
|
+
disabled: removeDisabled,
|
|
61
|
+
onClick: onRemove,
|
|
62
|
+
className: cn(
|
|
63
|
+
"inline-flex size-4 shrink-0 items-center justify-center rounded-sm opacity-70 transition-opacity",
|
|
64
|
+
"hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
65
|
+
"disabled:pointer-events-none disabled:opacity-40"
|
|
66
|
+
),
|
|
67
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3" })
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
) : null
|
|
71
|
+
] });
|
|
32
72
|
}
|
|
33
73
|
export {
|
|
34
74
|
Badge,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/badge.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds (biến thể tonal + 2 cỡ sm/md).\n * API giữ tương thích shadcn (cva + asChild + data-slot).\n *\n * ─ BIẾN THỂ TONAL: NỀN LÀ `--x`, CHỮ LÀ `--x-ink` ─\n *\n * Badge in chữ 10–12px nên phải đạt AA 4,5:1 trên chính cái tint mà nó đứng lên. Hợp đồng\n * ba vai trò (docs/hop-dong-mau.md §1) đã có sẵn bậc chữ cho việc này: nền là `--success` /\n * `--warning` / `--destructive`, chữ là bậc `-ink` tương ứng. Đo trên tint hợp lên `--card`:\n *\n * success-ink #216A48 trên bg-success/15 #E0F2E9 = 5,60:1 ✓ (light)\n * #4ADE80 trên #1E362C = 7,44:1 ✓ (dark)\n * warning-ink #7A5800 trên bg-warning/20 #FCF4DB = 5,93:1 ✓ (light)\n * #FACC15 trên #443C1B = 7,19:1 ✓ (dark)\n * destructive-ink #9A3537 trên bg-destructive/15 #F7E2E2 = 5,76:1 ✓ (light)\n * #F87171 trên #311D22 = 5,71:1 ✓ (dark)\n *\n * Bản trước dẫn xuất bậc tối tại chỗ bằng `color-mix(--x, --ink N%)` cho light rồi lật về\n * `dark:text-success` / `dark:text-warning` / `dark:text-red` cho dark. Số thì đạt, nhưng\n * NHÁNH DARK vi phạm hợp đồng: `--success`/`--warning` là token TÔ NỀN, không phải màu chữ —\n * app nào đổi `--success` cho hợp brand mình là badge hỏng chữ mà không có gì báo. Bậc `-ink`\n * mới là biến mang ràng buộc tương phản, và nó tự lật theo theme (styles.css khai `-ink` riêng\n * cho `.dark`) nên không cần biến thể `dark:` nào ở đây nữa.\n *\n * ⚠ Trần tint: `/20` là mức đậm nhất an toàn cho MỌI tone ở cả hai theme. `warning` chịu được\n * `/20` thoải mái (trần của vàng là `/60` light · `/36` dark) nhưng đỏ ở dark chỉ tới `/22` —\n * muốn đậm hơn `/15` phải tra bảng trần tint ở docs/hop-dong-mau.md §2.\n *\n * ─ VIỀN CỦA BIẾN THỂ `outline` ─\n * `border-control-edge` chứ không `border-border`: nền badge trong suốt nên VIỀN là thứ duy\n * nhất khoanh vùng nó → chịu ngưỡng 3:1 (WCAG 1.4.11). `--border` là bậc TRANG TRÍ, chỉ\n * 1,16:1 (light) / 1,17:1 (dark) — badge gần như tàng hình. `--control-edge`: 3,83:1 / 4,94:1 ✓ ở bảng\n * TƯƠNG PHẢN CAO; ở bảng MẶC ĐỊNH nó cũng chỉ 1,16:1 (light) / 2,00:1 (dark) trên `--card` (đo trên 3.3.4) —\n * khoản dưới ngưỡng bảng mặc định đã chấp nhận cho viền control, người cần tương phản bật bảng tương phản cao.\n *\n * ─ BIẾN THỂ `default`/`primary` ─\n * Vẫn là thang gold thô (`gold-100`/`gold-600` ở light, `gold-500/20`+`gold-200` ở dark) vì\n * cặp này đã đo và dư biên: 7,77:1 (light) · 10,23:1 (dark).\n */\nconst badgeVariants = cva(\n \"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n secondary: \"bg-secondary text-secondary-foreground\",\n outline: \"bg-transparent border border-control-edge text-foreground\",\n success: \"bg-green/10 dark:bg-green/20 text-green-ink\",\n warning: \"bg-yellow/10 dark:bg-yellow/20 text-yellow-ink\",\n destructive: \"bg-red/10 dark:bg-red/15 text-destructive-ink\"\n },\n size: {\n sm: \"px-1.5 py-0.5 text-[10px]\",\n md: \"px-2 py-0.5 text-xs\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\"\n }\n }\n);\n\nexport interface BadgeProps extends React.ComponentProps<\"span\">, VariantProps<typeof badgeVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. */\n asChild?: boolean;\n}\n\nfunction Badge({
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/badge.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds (biến thể tonal + 2 cỡ sm/md).\n * API giữ tương thích shadcn (cva + asChild + data-slot).\n *\n * ─ BIẾN THỂ TONAL: NỀN LÀ `--x`, CHỮ LÀ `--x-ink` ─\n *\n * Badge in chữ 10–12px nên phải đạt AA 4,5:1 trên chính cái tint mà nó đứng lên. Hợp đồng\n * ba vai trò (docs/hop-dong-mau.md §1) đã có sẵn bậc chữ cho việc này: nền là `--success` /\n * `--warning` / `--destructive`, chữ là bậc `-ink` tương ứng. Đo trên tint hợp lên `--card`:\n *\n * success-ink #216A48 trên bg-success/15 #E0F2E9 = 5,60:1 ✓ (light)\n * #4ADE80 trên #1E362C = 7,44:1 ✓ (dark)\n * warning-ink #7A5800 trên bg-warning/20 #FCF4DB = 5,93:1 ✓ (light)\n * #FACC15 trên #443C1B = 7,19:1 ✓ (dark)\n * destructive-ink #9A3537 trên bg-destructive/15 #F7E2E2 = 5,76:1 ✓ (light)\n * #F87171 trên #311D22 = 5,71:1 ✓ (dark)\n *\n * Bản trước dẫn xuất bậc tối tại chỗ bằng `color-mix(--x, --ink N%)` cho light rồi lật về\n * `dark:text-success` / `dark:text-warning` / `dark:text-red` cho dark. Số thì đạt, nhưng\n * NHÁNH DARK vi phạm hợp đồng: `--success`/`--warning` là token TÔ NỀN, không phải màu chữ —\n * app nào đổi `--success` cho hợp brand mình là badge hỏng chữ mà không có gì báo. Bậc `-ink`\n * mới là biến mang ràng buộc tương phản, và nó tự lật theo theme (styles.css khai `-ink` riêng\n * cho `.dark`) nên không cần biến thể `dark:` nào ở đây nữa.\n *\n * ⚠ Trần tint: `/20` là mức đậm nhất an toàn cho MỌI tone ở cả hai theme. `warning` chịu được\n * `/20` thoải mái (trần của vàng là `/60` light · `/36` dark) nhưng đỏ ở dark chỉ tới `/22` —\n * muốn đậm hơn `/15` phải tra bảng trần tint ở docs/hop-dong-mau.md §2.\n *\n * ─ VIỀN CỦA BIẾN THỂ `outline` ─\n * `border-control-edge` chứ không `border-border`: nền badge trong suốt nên VIỀN là thứ duy\n * nhất khoanh vùng nó → chịu ngưỡng 3:1 (WCAG 1.4.11). `--border` là bậc TRANG TRÍ, chỉ\n * 1,16:1 (light) / 1,17:1 (dark) — badge gần như tàng hình. `--control-edge`: 3,83:1 / 4,94:1 ✓ ở bảng\n * TƯƠNG PHẢN CAO; ở bảng MẶC ĐỊNH nó cũng chỉ 1,16:1 (light) / 2,00:1 (dark) trên `--card` (đo trên 3.3.4) —\n * khoản dưới ngưỡng bảng mặc định đã chấp nhận cho viền control, người cần tương phản bật bảng tương phản cao.\n *\n * ─ BIẾN THỂ `default`/`primary` ─\n * Vẫn là thang gold thô (`gold-100`/`gold-600` ở light, `gold-500/20`+`gold-200` ở dark) vì\n * cặp này đã đo và dư biên: 7,77:1 (light) · 10,23:1 (dark).\n */\nconst badgeVariants = cva(\n \"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n secondary: \"bg-secondary text-secondary-foreground\",\n outline: \"bg-transparent border border-control-edge text-foreground\",\n success: \"bg-green/10 dark:bg-green/20 text-green-ink\",\n warning: \"bg-yellow/10 dark:bg-yellow/20 text-yellow-ink\",\n destructive: \"bg-red/10 dark:bg-red/15 text-destructive-ink\"\n },\n size: {\n sm: \"px-1.5 py-0.5 text-[10px]\",\n md: \"px-2 py-0.5 text-xs\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\"\n }\n }\n);\n\nexport interface BadgeProps extends React.ComponentProps<\"span\">, VariantProps<typeof badgeVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. Bị bỏ qua khi có `onRemove`. */\n asChild?: boolean;\n /**\n * Chip GỠ ĐƯỢC (từ 3.7.0): vẽ một nút ✕ ở cuối badge (người theo dõi, nhãn đã gắn…). Đi cùng\n * `removeLabel` — thiếu thì nút không có tên trợ năng và gói cảnh báo ở console lúc phát triển.\n */\n onRemove?: () => void;\n /**\n * Tên trợ năng của nút ✕ — gói KHÔNG có nhãn mặc định, cố ý: nút chỉ có icon thì tên là thứ DUY NHẤT nói\n * nó gỡ CÁI GÌ (\"Gỡ Lan khỏi người theo dõi\"), mà câu đó là chữ nghiệp vụ của app. Một mặc định chung\n * kiểu \"Remove\" lặp N lần trong một danh sách là N nút trùng tên. (Và nó giữ `badge.tsx` là file tĩnh —\n * đọc nhãn từ `ComitorLocaleProvider` sẽ buộc nó thành client.)\n *\n * Vì sao không ép bằng kiểu (union \"có `onRemove` thì bắt buộc `removeLabel`\"): `BadgeProps` là interface\n * đã publish, đổi sang union là gãy mọi `interface X extends BadgeProps` của app.\n */\n removeLabel?: string;\n /** Khoá riêng nút ✕ (vd đang lưu) mà badge vẫn hiện bình thường. */\n removeDisabled?: boolean;\n}\n\nfunction Badge({\n className,\n variant,\n size,\n asChild = false,\n onRemove,\n removeLabel,\n removeDisabled,\n children,\n ...props\n}: BadgeProps) {\n const Comp = asChild && !onRemove ? Slot : \"span\";\n // `typeof process` — cùng lý do với `circular-progress.tsx`: ESM thuần trên trình duyệt không có `process`.\n if (onRemove && !removeLabel && typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\") {\n console.warn(\n \"[Badge] `onRemove` cần `removeLabel` — nút ✕ chỉ có icon thì tên trợ năng là thứ duy nhất nói nó gỡ gì.\"\n );\n }\n\n return (\n <Comp data-slot=\"badge\" className={cn(badgeVariants({ variant, size }), onRemove && \"pr-1\", className)} {...props}>\n {children}\n {onRemove ? (\n /*\n * `opacity-70`, KHÔNG `text-x/70`: icon lucide là nhiều nét cắt nhau, alpha trong `color` tô chỗ\n * giao hai lần (docs/hop-dong-mau.md §7b). Vùng bấm `size-4` nằm gọn trong chiều cao badge `md`.\n */\n <button\n type=\"button\"\n data-slot=\"badge-remove\"\n aria-label={removeLabel}\n disabled={removeDisabled}\n onClick={onRemove}\n className={cn(\n \"inline-flex size-4 shrink-0 items-center justify-center rounded-sm opacity-70 transition-opacity\",\n \"hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50\",\n \"disabled:pointer-events-none disabled:opacity-40\"\n )}\n >\n <X aria-hidden=\"true\" className=\"size-3\" />\n </button>\n ) : null}\n </Comp>\n );\n}\n\nexport { Badge, badgeVariants };\n"],"mappings":"AAgHI,SAmBM,KAnBN;AAhHJ,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,SAAS;AAElB,SAAS,UAAU;AAyCnB,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WAAW;AAAA,QACX,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAwBA,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAe;AACb,QAAM,OAAO,WAAW,CAAC,WAAW,OAAO;AAE3C,MAAI,YAAY,CAAC,eAAe,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa,cAAc;AACvG,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,SACE,qBAAC,QAAK,aAAU,SAAQ,WAAW,GAAG,cAAc,EAAE,SAAS,KAAK,CAAC,GAAG,YAAY,QAAQ,SAAS,GAAI,GAAG,OACzG;AAAA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA,MAKC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,aAAU;AAAA,UACV,cAAY;AAAA,UACZ,UAAU;AAAA,UACV,SAAS;AAAA,UACT,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,UAEA,8BAAC,KAAE,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA,MAC3C;AAAA,QACE;AAAA,KACN;AAEJ;","names":[]}
|
|
@@ -6,7 +6,7 @@ import '@radix-ui/react-separator';
|
|
|
6
6
|
|
|
7
7
|
type ItemGroupProps = React.ComponentProps<"div">;
|
|
8
8
|
type ItemSeparatorProps = SeparatorProps;
|
|
9
|
-
declare function ItemGroup({ className, ...props }: ItemGroupProps): React.JSX.Element;
|
|
9
|
+
declare function ItemGroup({ className, children, ...props }: ItemGroupProps): React.JSX.Element;
|
|
10
10
|
declare function ItemSeparator({ className, ...props }: ItemSeparatorProps): React.JSX.Element;
|
|
11
11
|
declare const itemVariants: (props?: ({
|
|
12
12
|
variant?: "default" | "outline" | "muted" | null | undefined;
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
|
+
import { Children, cloneElement, Fragment, isValidElement } from "react";
|
|
4
5
|
import { cn } from "../../lib/cn.js";
|
|
5
6
|
import { Separator } from "./separator.js";
|
|
6
|
-
function
|
|
7
|
-
return
|
|
7
|
+
function withListItemRole(children) {
|
|
8
|
+
return Children.map(children, (child) => {
|
|
9
|
+
if (!isValidElement(child)) return child;
|
|
10
|
+
if (child.type === Fragment) return cloneElement(child, void 0, withListItemRole(child.props.children));
|
|
11
|
+
if (child.type === Item && !("role" in child.props) && !child.props.asChild) {
|
|
12
|
+
return cloneElement(child, { role: "listitem" });
|
|
13
|
+
}
|
|
14
|
+
return child;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function ItemGroup({ className, children, ...props }) {
|
|
18
|
+
return /* @__PURE__ */ jsx("div", { role: "list", "data-slot": "item-group", className: cn("group/item-group flex flex-col", className), ...props, children: withListItemRole(children) });
|
|
8
19
|
}
|
|
9
20
|
function ItemSeparator({ className, ...props }) {
|
|
10
21
|
return /* @__PURE__ */ jsx(Separator, { "data-slot": "item-separator", orientation: "horizontal", className: cn("my-0", className), ...props });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/item.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Separator, type SeparatorProps } from \"./separator.js\";\n\nexport type ItemGroupProps = React.ComponentProps<\"div\">;\nexport type ItemSeparatorProps = SeparatorProps;\n\nfunction ItemGroup({ className, ...props }: ItemGroupProps) {\n return (\n <div role=\"list\" data-slot=\"item-group\" className={cn(\"group/item-group flex flex-col\", className)} {...props}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/item.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { Children, cloneElement, Fragment, isValidElement, type ReactNode } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Separator, type SeparatorProps } from \"./separator.js\";\n\nexport type ItemGroupProps = React.ComponentProps<\"div\">;\nexport type ItemSeparatorProps = SeparatorProps;\n\n/**\n * Gắn `role=\"listitem\"` cho mọi `Item` là con của `ItemGroup` (đi xuyên `Fragment`), vì `ItemGroup` là\n * `role=\"list\"` — danh sách không có `listitem` là không hợp lệ (axe: aria-required-children).\n *\n * Vì sao `cloneElement` chứ không context: `item.tsx` là file TĨNH dùng được trong Server Component;\n * context buộc nó thành client. Cái giá: chỉ thấy `Item` viết TRỰC TIẾP trong cây con (qua mảng và\n * `Fragment`) — `Item` nằm trong một component khác của app thì app tự truyền `role=\"listitem\"`.\n *\n * Không gắn khi: app đã truyền `role` (kể cả `role={undefined}` thì không — chỉ kiểm có khoá), hoặc\n * `asChild` — `Item asChild` thường là một `<a>`, đè `listitem` lên đó là XOÁ vai trò liên kết.\n */\nfunction withListItemRole(children: ReactNode): ReactNode {\n return Children.map(children, (child) => {\n if (!isValidElement<{ children?: ReactNode; role?: string; asChild?: boolean }>(child)) return child;\n if (child.type === Fragment) return cloneElement(child, undefined, withListItemRole(child.props.children));\n if (child.type === Item && !(\"role\" in child.props) && !child.props.asChild) {\n return cloneElement(child, { role: \"listitem\" });\n }\n return child;\n });\n}\n\nfunction ItemGroup({ className, children, ...props }: ItemGroupProps) {\n return (\n <div role=\"list\" data-slot=\"item-group\" className={cn(\"group/item-group flex flex-col\", className)} {...props}>\n {withListItemRole(children)}\n </div>\n );\n}\n\nfunction ItemSeparator({ className, ...props }: ItemSeparatorProps) {\n return <Separator data-slot=\"item-separator\" orientation=\"horizontal\" className={cn(\"my-0\", className)} {...props} />;\n}\n\nconst itemVariants = cva(\n \"group/item flex items-center border border-transparent text-sm rounded-md transition-colors duration-150 ease-out [a&]:hover:bg-accent/50 [a&]:transition-colors flex-wrap outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline: \"border-border\",\n muted: \"bg-muted/50\"\n },\n size: {\n default: \"p-4 gap-4\",\n sm: \"py-(--list-item-py) px-4 gap-2.5\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\"\n }\n }\n);\n\nexport interface ItemProps extends React.ComponentProps<\"div\">, VariantProps<typeof itemVariants> {\n /** Render ra chính phần tử con (Radix Slot) — dùng khi cần <a> hoặc <button>. */\n asChild?: boolean;\n}\n\nfunction Item({ className, variant = \"default\", size = \"default\", asChild = false, ...props }: ItemProps) {\n const Comp = asChild ? Slot : \"div\";\n return (\n <Comp\n data-slot=\"item\"\n data-variant={variant}\n data-size={size}\n className={cn(itemVariants({ variant, size, className }))}\n {...props}\n />\n );\n}\n\nconst itemMediaVariants = cva(\n \"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n icon: \"size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4\",\n image: \"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover\"\n }\n },\n defaultVariants: {\n variant: \"default\"\n }\n }\n);\n\nexport interface ItemMediaProps extends React.ComponentProps<\"div\">, VariantProps<typeof itemMediaVariants> {}\nexport type ItemContentProps = React.ComponentProps<\"div\">;\nexport type ItemTitleProps = React.ComponentProps<\"div\">;\nexport type ItemDescriptionProps = React.ComponentProps<\"p\">;\nexport type ItemActionsProps = React.ComponentProps<\"div\">;\nexport type ItemHeaderProps = React.ComponentProps<\"div\">;\nexport type ItemFooterProps = React.ComponentProps<\"div\">;\n\nfunction ItemMedia({ className, variant = \"default\", ...props }: ItemMediaProps) {\n return (\n <div\n data-slot=\"item-media\"\n data-variant={variant}\n className={cn(itemMediaVariants({ variant, className }))}\n {...props}\n />\n );\n}\n\nfunction ItemContent({ className, ...props }: ItemContentProps) {\n return (\n <div\n data-slot=\"item-content\"\n className={cn(\"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none\", className)}\n {...props}\n />\n );\n}\n\nfunction ItemTitle({ className, ...props }: ItemTitleProps) {\n return (\n <div\n data-slot=\"item-title\"\n className={cn(\"flex w-fit items-center gap-2 text-sm leading-snug font-medium\", className)}\n {...props}\n />\n );\n}\n\nfunction ItemDescription({ className, ...props }: ItemDescriptionProps) {\n return (\n <p\n data-slot=\"item-description\"\n className={cn(\n \"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance\",\n \"[&>a:hover]:text-link-ink-hover [&>a]:underline [&>a]:underline-offset-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction ItemActions({ className, ...props }: ItemActionsProps) {\n return <div data-slot=\"item-actions\" className={cn(\"flex items-center gap-2\", className)} {...props} />;\n}\n\nfunction ItemHeader({ className, ...props }: ItemHeaderProps) {\n return (\n <div\n data-slot=\"item-header\"\n className={cn(\"flex basis-full items-center justify-between gap-2\", className)}\n {...props}\n />\n );\n}\n\nfunction ItemFooter({ className, ...props }: ItemFooterProps) {\n return (\n <div\n data-slot=\"item-footer\"\n className={cn(\"flex basis-full items-center justify-between gap-2\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Item,\n ItemActions,\n ItemContent,\n ItemDescription,\n ItemFooter,\n ItemGroup,\n ItemHeader,\n ItemMedia,\n ItemSeparator,\n ItemTitle,\n itemMediaVariants,\n itemVariants\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Editor } from '@tiptap/core';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { f as EditorFormat } from '../editor-types-
|
|
3
|
+
import { f as EditorFormat } from '../editor-types-CSrMeuSw.js';
|
|
4
4
|
import { MarkdownEditorLabels } from './markdown-editor-constants.js';
|
|
5
|
+
import '../components/composite/empty-state.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 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ỗ
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { g as EditorBaseProps, f as EditorFormat, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-
|
|
2
|
+
import { g as EditorBaseProps, f as EditorFormat, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-CSrMeuSw.js';
|
|
3
3
|
import '@tiptap/core';
|
|
4
|
+
import '../components/composite/empty-state.js';
|
|
4
5
|
import './markdown-editor-constants.js';
|
|
5
6
|
|
|
6
7
|
/** Handle chung, đọc/ghi theo định dạng của trình soạn — mỗi lớp ngoài đặt tên lại cho cặp này. */
|
|
@@ -81,7 +81,8 @@ function EditorCore({
|
|
|
81
81
|
placeholder: placeholder ?? labels.placeholder,
|
|
82
82
|
taskCheckbox: labels.taskCheckbox,
|
|
83
83
|
resolveImageSrc,
|
|
84
|
-
resolveMentionLabel: mentions?.resolveLabel
|
|
84
|
+
resolveMentionLabel: mentions?.resolveLabel,
|
|
85
|
+
resolveMentionHref: mentions?.getHref
|
|
85
86
|
});
|
|
86
87
|
const latest = useAsRef({ onChange, onFocus, onBlur, mentions, onImageUpload, labels, name });
|
|
87
88
|
const imageHandlers = useAsRef({
|
|
@@ -120,7 +121,8 @@ function EditorCore({
|
|
|
120
121
|
const mentionStore = useLazyRef(
|
|
121
122
|
() => new SuggestionStore(MENTION_PLUGIN_KEY, (item) => ({
|
|
122
123
|
id: item.id,
|
|
123
|
-
label: item.label
|
|
124
|
+
label: item.label,
|
|
125
|
+
kind: item.kind ?? null
|
|
124
126
|
}))
|
|
125
127
|
).current;
|
|
126
128
|
const [extensions] = React.useState(() => [
|
|
@@ -317,10 +319,10 @@ function EditorCore({
|
|
|
317
319
|
label: labels.mentionMenu,
|
|
318
320
|
emptyLabel: labels.mentionEmpty,
|
|
319
321
|
loadingLabel: labels.mentionLoading,
|
|
320
|
-
getKey: (item) => item.id
|
|
322
|
+
getKey: (item) => `${item.kind ?? ""}:${item.id}`,
|
|
321
323
|
exitWhenEmpty: (query) => /\s/.test(query),
|
|
322
324
|
renderItem: (item) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
323
|
-
/* @__PURE__ */ jsx(LetterAvatar, { name: item.label, src: item.avatarUrl, size: "xs", "aria-hidden": "true" }),
|
|
325
|
+
item.icon ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex size-6 shrink-0 items-center justify-center", children: /* @__PURE__ */ jsx(item.icon, { className: "size-4 text-muted-foreground" }) }) : /* @__PURE__ */ jsx(LetterAvatar, { name: item.label, src: item.avatarUrl, size: "xs", "aria-hidden": "true" }),
|
|
324
326
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
|
|
325
327
|
item.description && /* @__PURE__ */ jsx("span", { className: "max-w-[45%] truncate text-xs text-muted-foreground", children: item.description })
|
|
326
328
|
] })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/editor/editor-core.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Lõi CHUNG của hai trình soạn — `MarkdownEditor` (lưu markdown) và `RichTextEditor` (lưu HTML).\n *\n * Hai component công khai chỉ là hai lớp mỏng: chọn định dạng, thanh công cụ mặc định, và tên của cặp\n * đọc/ghi trên handle (`getMarkdown` / `getHTML`). Mọi thứ còn lại — menu `/` và `@`, ảnh, liên kết,\n * phím tắt, trợ năng, đồng bộ `value` — nằm ở đây, MỘT bản: hai bản là hai nơi để sửa một lỗi mà\n * quên nơi kia.\n *\n * Không xuất khỏi entry: app chọn định dạng bằng cách chọn COMPONENT, không bằng prop. Định dạng quyết\n * định cả lược đồ (`createSchemaExtensions`), mà lược đồ chỉ dựng một lần lúc mount — một prop đổi được\n * lúc chạy sẽ là một prop nói dối.\n */\n\nimport type { AnyExtension, Editor } from \"@tiptap/core\";\nimport { PluginKey } from \"@tiptap/pm/state\";\nimport { EditorContent, useEditor } from \"@tiptap/react\";\nimport * as React from \"react\";\nimport { LetterAvatar } from \"../components/composite/letter-avatar.js\";\nimport { toast } from \"../components/composite/toast.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { matchesSearch } from \"../lib/text-search.js\";\nimport { useAsRef } from \"../lib/use-as-ref.js\";\nimport { useLazyRef } from \"../lib/use-lazy-ref.js\";\nimport { BubbleToolbar } from \"./bubble-toolbar.js\";\nimport type {\n EditorBaseProps,\n EditorFormat,\n EditorHandleBase,\n EditorToolbarMode,\n MarkdownEditorMentionItem\n} from \"./editor-types.js\";\nimport { FixedToolbar } from \"./fixed-toolbar.js\";\nimport {\n createImageUploadExtension,\n type ImageUploadHandlers,\n insertImageFiles,\n type PendingUploads\n} from \"./image-upload.js\";\nimport { MARKDOWN_EDITOR_CONTENT_CLASS } from \"./markdown-editor-classes.js\";\nimport {\n createEditorKeymap,\n createSchemaExtensions,\n type EditorKeymapHandlers,\n type MarkdownEditorRuntime\n} from \"./markdown-extensions.js\";\nimport { trimMarkdown } from \"./markdown-syntax.js\";\nimport {\n createSlashCommandExtension,\n filterSlashCommands,\n SLASH_PLUGIN_KEY,\n type SlashMenuItem\n} from \"./slash-commands.js\";\nimport { SuggestionMenu } from \"./suggestion-menu.js\";\nimport { SuggestionStore } from \"./suggestion-store.js\";\n\n/** Handle chung, đọc/ghi theo định dạng của trình soạn — mỗi lớp ngoài đặt tên lại cho cặp này. */\nexport interface EditorCoreHandle extends EditorHandleBase {\n getValue: () => string;\n setValue: (value: string) => void;\n}\n\nexport interface EditorCoreProps<THandle> extends EditorBaseProps {\n format: EditorFormat;\n toolbar: EditorToolbarMode;\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n /** `data-slot` của khung ngoài — mỗi trình soạn một tên, để CSS/test của app nhắm đúng. */\n slot: string;\n handleRef?: React.Ref<THandle>;\n /**\n * Dựng handle công khai từ handle chung. Hàm ở cấp MODULE (định danh ổn định), và KHÔNG trải `core`\n * ra (`{ ...core }`): trải là đọc getter `editor` đúng một lần — lúc nó còn `null`.\n */\n createHandle: (core: EditorCoreHandle) => THandle;\n}\n\ntype MentionSelection = { id: string; label: string };\n\nconst MENTION_PLUGIN_KEY = new PluginKey(\"comitorMentionMenu\");\n\n/** Trần số người hiện khi nguồn là MẢNG mà chưa gõ gì — gõ thêm chữ là lọc; 500 dòng DOM thì không. */\nconst MENTION_LIST_LIMIT = 50;\n\nfunction filterMentionItems(items: readonly MarkdownEditorMentionItem[], query: string): MarkdownEditorMentionItem[] {\n const term = query.trim();\n const matches = term\n ? items.filter(\n (item) =>\n matchesSearch(item.label, term) ||\n // Đi QUA từ đầu của tên: \"@nguyenvana\" vẫn tới \"Nguyễn Văn An\" — cùng luật `Mention`.\n matchesSearch(item.label.replace(/\\s+/g, \"\"), term.replace(/\\s+/g, \"\")) ||\n (item.description ? matchesSearch(item.description, term) : false)\n )\n : items;\n return matches.slice(0, MENTION_LIST_LIMIT);\n}\n\n/**\n * Giá trị hiện tại theo định dạng. HTML của tài liệu rỗng là `<p></p>` — trả `\"\"` để \"rỗng\" là MỘT\n * giá trị ở cả hai trình soạn: biểu mẫu kiểm trường bắt buộc bằng `value === \"\"` mà không phải biết\n * trình soạn nào đứng sau.\n */\nfunction readValue(editor: Editor, format: EditorFormat): string {\n if (format === \"markdown\") return trimMarkdown(editor.getMarkdown());\n return editor.isEmpty ? \"\" : editor.getHTML();\n}\n\nconst toBoolean = (value: boolean | \"true\" | \"false\" | undefined) => value === true || value === \"true\";\n\nexport function EditorCore<THandle>({\n format,\n toolbar,\n value,\n defaultValue,\n onChange,\n onSubmit,\n onFocus,\n onBlur,\n placeholder,\n editable = true,\n autoFocus = false,\n variant = \"outline\",\n mentions,\n onImageUpload,\n onImageUploadError,\n resolveImageSrc,\n extensions: extraExtensions,\n labels: labelsProp,\n name,\n id,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n className,\n contentClassName,\n slot,\n handleRef,\n createHandle\n}: EditorCoreProps<THandle>) {\n const labels = useComitorLabels(\"markdownEditor\", labelsProp);\n\n /*\n * Mọi giá trị đổi được SAU KHI dựng đi qua ref: danh sách extension chỉ dựng MỘT lần (đổi nó là\n * dựng lại trình soạn — mất lịch sử hoàn tác, mất con trỏ), nên extension đọc giá trị mới nhất lúc\n * cần chứ không chép lúc dựng.\n */\n const runtime = useAsRef<MarkdownEditorRuntime>({\n placeholder: placeholder ?? labels.placeholder,\n taskCheckbox: labels.taskCheckbox,\n resolveImageSrc,\n resolveMentionLabel: mentions?.resolveLabel\n });\n const latest = useAsRef({ onChange, onFocus, onBlur, mentions, onImageUpload, labels, name });\n const imageHandlers = useAsRef<ImageUploadHandlers>({\n upload: onImageUpload,\n onError: (error, file) => {\n if (onImageUploadError) onImageUploadError(error, file);\n else toast(labels.imageUploadFailed, { variant: \"destructive\", description: file.name || undefined });\n },\n resolveSrc: resolveImageSrc,\n uploadingLabel: labels.imageUploading,\n format\n });\n\n const [linkRequest, setLinkRequest] = React.useState(0);\n const requestLink = React.useCallback(() => setLinkRequest((count) => count + 1), []);\n const keymapHandlers = useAsRef<EditorKeymapHandlers>({\n onLinkShortcut: () => {\n if (!editable) return false;\n requestLink();\n return true;\n },\n onSubmitShortcut: () => {\n if (!onSubmit) return false;\n onSubmit();\n return true;\n }\n });\n\n const initialValue = value ?? defaultValue ?? \"\";\n const lastValue = React.useRef(initialValue);\n const [hiddenValue, setHiddenValue] = React.useState(initialValue);\n const pendingUploads = useLazyRef<PendingUploads>(() => new Set()).current;\n const fileInputRef = React.useRef<HTMLInputElement>(null);\n const pickImage = React.useCallback(() => fileInputRef.current?.click(), []);\n\n const slashStore = useLazyRef(\n () => new SuggestionStore<SlashMenuItem, SlashMenuItem>(SLASH_PLUGIN_KEY, (item) => item)\n ).current;\n const mentionStore = useLazyRef(\n () =>\n new SuggestionStore<MarkdownEditorMentionItem, MentionSelection>(MENTION_PLUGIN_KEY, (item) => ({\n id: item.id,\n label: item.label\n }))\n ).current;\n\n const [extensions] = React.useState<AnyExtension[]>(() => [\n ...createSchemaExtensions({\n format,\n runtime,\n mentionSuggestion: {\n pluginKey: MENTION_PLUGIN_KEY,\n char: \"@\",\n // Tên người Việt có dấu cách. Khi chữ gõ có dấu cách mà không còn ai khớp, menu tự thoát\n // (`exitWhenEmpty`) — người dùng đang viết câu, không còn tìm người nữa.\n allowSpaces: true,\n debounce: typeof mentions?.items === \"function\" ? 150 : 0,\n items: async ({ query, signal }) => {\n const source = latest.current.mentions?.items;\n if (!source) return [];\n if (typeof source === \"function\") return [...(await source(query, { signal }))];\n return filterMentionItems(source, query);\n },\n allow: ({ editor: current }) =>\n Boolean(latest.current.mentions) &&\n current.isEditable &&\n !current.isActive(\"codeBlock\") &&\n !current.isActive(\"code\"),\n render: mentionStore.render\n }\n }),\n createSlashCommandExtension({\n items: ({ editor: current, query }) =>\n filterSlashCommands(current, query, latest.current.labels, {\n format,\n pickImage: latest.current.onImageUpload ? () => fileInputRef.current?.click() : undefined\n }),\n render: slashStore.render\n }),\n createImageUploadExtension(imageHandlers, pendingUploads),\n createEditorKeymap(keymapHandlers, format),\n ...(extraExtensions ?? [])\n ]);\n\n /*\n * `<label for={id}>` không trỏ được vào một `div contenteditable` (chỉ `input`/`textarea`/… là\n * \"labelable\") — nên `FormField` dựng nhãn đúng mà vùng soạn vẫn không có tên, và bấm nhãn không\n * đưa con trỏ vào. Effect bên dưới tìm nhãn theo `for` và ghi id của nó vào đây; tên trợ năng đi\n * qua `aria-labelledby` trong bộ thuộc tính — nhờ vậy tên dự phòng (`editorLabel`) được BỎ hẳn\n * chứ không nằm song song với nhãn thật.\n */\n const [labelElementId, setLabelElementId] = React.useState<string | null>(null);\n\n const contentPadding = variant === \"outline\" ? \"min-h-16 px-3 py-2\" : \"min-h-6\";\n\n /*\n * Thuộc tính của vùng soạn. Ổn định theo giá trị: `useEditor` so tuỳ chọn mỗi lượt render và gửi\n * lại `editorProps` mỗi khi định danh đổi — tức mỗi lần FormField bật/tắt `aria-invalid` là một\n * lần cập nhật đúng, chứ không phải mỗi phím gõ.\n *\n * Tên trợ năng: chỉ đặt khi SOẠN được (`role=\"textbox\"`). Ở chế độ đọc, vùng nội dung là văn bản\n * thường; `aria-label` trên một `div` không vai trò là thứ ARIA 1.2 cấm và trình đọc màn hình bỏ qua.\n * ⚠ Chế độ đọc phải GHI ĐÈ `role` chứ không bỏ trống: Tiptap luôn trộn `role=\"textbox\"` mặc định\n * vào TRƯỚC thuộc tính của app (`getEditorViewAttributes`), nên thiếu khoá là danh sách bình luận\n * đọc lên thành một dãy \"ô nhập chỉ đọc\" — đã gặp ở harness.\n */\n const editorProps = React.useMemo(() => {\n const attributes: Record<string, string> = {\n class: cn(MARKDOWN_EDITOR_CONTENT_CLASS, contentPadding, contentClassName)\n };\n if (id) attributes.id = id;\n if (!editable) attributes.role = \"none\";\n if (editable) {\n attributes.role = \"textbox\";\n attributes[\"aria-multiline\"] = \"true\";\n const labelledBy = ariaLabelledBy ?? labelElementId;\n if (ariaLabel || !labelledBy) attributes[\"aria-label\"] = ariaLabel ?? labels.editorLabel;\n if (labelledBy) attributes[\"aria-labelledby\"] = labelledBy;\n if (toBoolean(ariaRequired)) attributes[\"aria-required\"] = \"true\";\n }\n if (ariaDescribedBy) attributes[\"aria-describedby\"] = ariaDescribedBy;\n if (toBoolean(ariaInvalid)) attributes[\"aria-invalid\"] = \"true\";\n return { attributes };\n }, [\n contentPadding,\n contentClassName,\n id,\n editable,\n ariaLabel,\n ariaLabelledBy,\n labelElementId,\n ariaDescribedBy,\n ariaInvalid,\n ariaRequired,\n labels.editorLabel\n ]);\n\n const editor = useEditor({\n extensions,\n content: initialValue,\n contentType: format,\n editable,\n autofocus: autoFocus ? \"end\" : false,\n // Next App Router render ở máy chủ: dựng trình soạn ở client sau lần hydrate đầu, không thì HTML\n // hai phía lệch nhau. Khung rỗng giữ chỗ trong khoảng đó — xem phần render.\n immediatelyRender: false,\n editorProps,\n onUpdate: ({ editor: current }) => {\n const next = readValue(current, format);\n lastValue.current = next;\n latest.current.onChange?.(next);\n if (latest.current.name) setHiddenValue(next);\n },\n onFocus: () => latest.current.onFocus?.(),\n onBlur: () => latest.current.onBlur?.()\n });\n\n // Giá trị đổi từ NGOÀI (xoá sau khi gửi, nạp bản nháp…). Giá trị chính mình vừa phát thì bỏ qua —\n // không thì mỗi phím gõ là một lần nạp lại, con trỏ nhảy về đầu.\n React.useEffect(() => {\n if (!editor || value === undefined || value === lastValue.current) return;\n editor.commands.setContent(value, { contentType: format, emitUpdate: false });\n lastValue.current = value;\n setHiddenValue(value);\n }, [editor, value, format]);\n\n // `useEditor` cố ý giữ nguyên `editable` khi cập nhật tuỳ chọn, nên phải tự đặt. Không phát `update`:\n // đổi chế độ không phải đổi nội dung.\n React.useEffect(() => {\n if (editor && editor.isEditable !== editable) editor.setEditable(editable, false);\n }, [editor, editable]);\n\n // Nối `<label for={id}>` — lý do ở chỗ khai `labelElementId`. Bấm nhãn thì tự đưa con trỏ vào.\n React.useEffect(() => {\n if (!editor || !id || !editable || ariaLabel || ariaLabelledBy) return;\n const label = document.querySelector<HTMLLabelElement>(`label[for=\"${CSS.escape(id)}\"]`);\n if (!label) return;\n if (!label.id) label.id = `${id}-label`;\n setLabelElementId(label.id);\n const focusEditor = (event: MouseEvent) => {\n event.preventDefault();\n editor.commands.focus();\n };\n label.addEventListener(\"click\", focusEditor);\n return () => {\n label.removeEventListener(\"click\", focusEditor);\n setLabelElementId(null);\n };\n }, [editor, id, editable, ariaLabel, ariaLabelledBy]);\n\n /*\n * Handle ỔN ĐỊNH suốt đời component, đọc trình soạn MỚI NHẤT qua ref. Dựng lại theo `editor` thì ai\n * giữ đối tượng cũ — một closure, một biến ngoài React — thấy `editor: null` mãi, vì trình soạn chỉ\n * có SAU lượt render đầu (`immediatelyRender: false`). Đã gặp ở harness.\n */\n const editorRef = useAsRef(editor);\n const core = React.useMemo<EditorCoreHandle>(\n () => ({\n focus: (position = \"end\") => {\n editorRef.current?.commands.focus(position);\n },\n getValue: () => (editorRef.current ? readValue(editorRef.current, format) : lastValue.current),\n setValue: (next) => {\n editorRef.current?.commands.setContent(next, { contentType: format, emitUpdate: true });\n },\n clear: () => {\n editorRef.current?.commands.clearContent(true);\n },\n isEmpty: () => (editorRef.current ? editorRef.current.isEmpty : lastValue.current.trim() === \"\"),\n isUploading: () => pendingUploads.size > 0,\n get editor() {\n return editorRef.current;\n }\n }),\n [editorRef, pendingUploads, format]\n );\n React.useImperativeHandle(handleRef, () => createHandle(core), [createHandle, core]);\n\n return (\n <div\n data-slot={slot}\n data-variant={variant}\n data-toolbar={toolbar}\n data-readonly={!editable || undefined}\n className={cn(\n \"relative min-w-0\",\n variant === \"outline\" &&\n // Khuôn khung của `Textarea`: viền `border-input`, nền trong suốt (dark: `--input-fill`),\n // focus `--primary-ink` + vòng `ring/50`; lỗi đọc từ `aria-invalid` của CHÍNH vùng soạn.\n \"rounded-md border border-input bg-transparent shadow-xs transition-colors duration-150 ease-out focus-within:border-primary-ink focus-within:ring-2 focus-within:ring-ring/50 has-[[aria-invalid=true]]:border-destructive-ink dark:bg-input-fill\",\n className\n )}\n >\n {toolbar === \"fixed\" && editable && (\n <FixedToolbar\n editor={editor}\n labels={labels}\n format={format}\n onLink={requestLink}\n onPickImage={onImageUpload ? pickImage : undefined}\n // Khung `outline` bo góc mà KHÔNG `overflow-hidden` (nó sẽ giết `sticky`) — nền thanh tự bo\n // theo góc của khung. `ghost` không có khung: chừa khoảng cho chữ khỏi dính vào viền dưới.\n className={variant === \"outline\" ? \"rounded-t-[inherit]\" : \"mb-3\"}\n />\n )}\n\n {editor ? (\n <EditorContent editor={editor} />\n ) : (\n // Giữ chỗ tới khi trình soạn dựng xong ở client (`immediatelyRender: false`) — cùng chiều cao tối thiểu.\n <div aria-hidden=\"true\" className={cn(contentPadding, contentClassName)} />\n )}\n\n {editor && editable && (\n <BubbleToolbar\n editor={editor}\n labels={labels}\n format={format}\n formatting={toolbar === \"bubble\"}\n linkRequest={linkRequest}\n />\n )}\n\n {editor && (\n <SuggestionMenu\n editor={editor}\n store={slashStore}\n label={labels.slashMenu}\n emptyLabel={labels.slashEmpty}\n getKey={(item) => item.id}\n renderItem={(item) => (\n <>\n <item.icon aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.hint && (\n // Cú pháp gõ tắt — mờ, chữ đều, như phím tắt trong `CommandShortcut`.\n <span className=\"ml-auto font-mono text-xs text-muted-foreground\">{item.hint}</span>\n )}\n </>\n )}\n />\n )}\n\n {editor && mentions && (\n <SuggestionMenu\n editor={editor}\n store={mentionStore}\n label={labels.mentionMenu}\n emptyLabel={labels.mentionEmpty}\n loadingLabel={labels.mentionLoading}\n getKey={(item) => item.id}\n exitWhenEmpty={(query) => /\\s/.test(query)}\n renderItem={(item) => (\n <>\n <LetterAvatar name={item.label} src={item.avatarUrl} size=\"xs\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.description && (\n <span className=\"max-w-[45%] truncate text-xs text-muted-foreground\">{item.description}</span>\n )}\n </>\n )}\n />\n )}\n\n {onImageUpload && (\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n tabIndex={-1}\n onChange={(event) => {\n const files = Array.from(event.target.files ?? []).filter((file) => file.type.startsWith(\"image/\"));\n // Xoá giá trị để chọn LẠI đúng tệp đó lần sau vẫn phát `change`.\n event.target.value = \"\";\n if (editor && files.length > 0)\n insertImageFiles(editor, files, undefined, imageHandlers.current, pendingUploads);\n }}\n />\n )}\n\n {name && <input type=\"hidden\" name={name} value={hiddenValue} />}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/editor/editor-core.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Lõi CHUNG của hai trình soạn — `MarkdownEditor` (lưu markdown) và `RichTextEditor` (lưu HTML).\n *\n * Hai component công khai chỉ là hai lớp mỏng: chọn định dạng, thanh công cụ mặc định, và tên của cặp\n * đọc/ghi trên handle (`getMarkdown` / `getHTML`). Mọi thứ còn lại — menu `/` và `@`, ảnh, liên kết,\n * phím tắt, trợ năng, đồng bộ `value` — nằm ở đây, MỘT bản: hai bản là hai nơi để sửa một lỗi mà\n * quên nơi kia.\n *\n * Không xuất khỏi entry: app chọn định dạng bằng cách chọn COMPONENT, không bằng prop. Định dạng quyết\n * định cả lược đồ (`createSchemaExtensions`), mà lược đồ chỉ dựng một lần lúc mount — một prop đổi được\n * lúc chạy sẽ là một prop nói dối.\n */\n\nimport type { AnyExtension, Editor } from \"@tiptap/core\";\nimport { PluginKey } from \"@tiptap/pm/state\";\nimport { EditorContent, useEditor } from \"@tiptap/react\";\nimport * as React from \"react\";\nimport { LetterAvatar } from \"../components/composite/letter-avatar.js\";\nimport { toast } from \"../components/composite/toast.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { matchesSearch } from \"../lib/text-search.js\";\nimport { useAsRef } from \"../lib/use-as-ref.js\";\nimport { useLazyRef } from \"../lib/use-lazy-ref.js\";\nimport { BubbleToolbar } from \"./bubble-toolbar.js\";\nimport type {\n EditorBaseProps,\n EditorFormat,\n EditorHandleBase,\n EditorToolbarMode,\n MarkdownEditorMentionItem\n} from \"./editor-types.js\";\nimport { FixedToolbar } from \"./fixed-toolbar.js\";\nimport {\n createImageUploadExtension,\n type ImageUploadHandlers,\n insertImageFiles,\n type PendingUploads\n} from \"./image-upload.js\";\nimport { MARKDOWN_EDITOR_CONTENT_CLASS } from \"./markdown-editor-classes.js\";\nimport {\n createEditorKeymap,\n createSchemaExtensions,\n type EditorKeymapHandlers,\n type MarkdownEditorRuntime\n} from \"./markdown-extensions.js\";\nimport { trimMarkdown } from \"./markdown-syntax.js\";\nimport {\n createSlashCommandExtension,\n filterSlashCommands,\n SLASH_PLUGIN_KEY,\n type SlashMenuItem\n} from \"./slash-commands.js\";\nimport { SuggestionMenu } from \"./suggestion-menu.js\";\nimport { SuggestionStore } from \"./suggestion-store.js\";\n\n/** Handle chung, đọc/ghi theo định dạng của trình soạn — mỗi lớp ngoài đặt tên lại cho cặp này. */\nexport interface EditorCoreHandle extends EditorHandleBase {\n getValue: () => string;\n setValue: (value: string) => void;\n}\n\nexport interface EditorCoreProps<THandle> extends EditorBaseProps {\n format: EditorFormat;\n toolbar: EditorToolbarMode;\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n /** `data-slot` của khung ngoài — mỗi trình soạn một tên, để CSS/test của app nhắm đúng. */\n slot: string;\n handleRef?: React.Ref<THandle>;\n /**\n * Dựng handle công khai từ handle chung. Hàm ở cấp MODULE (định danh ổn định), và KHÔNG trải `core`\n * ra (`{ ...core }`): trải là đọc getter `editor` đúng một lần — lúc nó còn `null`.\n */\n createHandle: (core: EditorCoreHandle) => THandle;\n}\n\ntype MentionSelection = { id: string; label: string; kind: string | null };\n\nconst MENTION_PLUGIN_KEY = new PluginKey(\"comitorMentionMenu\");\n\n/** Trần số người hiện khi nguồn là MẢNG mà chưa gõ gì — gõ thêm chữ là lọc; 500 dòng DOM thì không. */\nconst MENTION_LIST_LIMIT = 50;\n\nfunction filterMentionItems(items: readonly MarkdownEditorMentionItem[], query: string): MarkdownEditorMentionItem[] {\n const term = query.trim();\n const matches = term\n ? items.filter(\n (item) =>\n matchesSearch(item.label, term) ||\n // Đi QUA từ đầu của tên: \"@nguyenvana\" vẫn tới \"Nguyễn Văn An\" — cùng luật `Mention`.\n matchesSearch(item.label.replace(/\\s+/g, \"\"), term.replace(/\\s+/g, \"\")) ||\n (item.description ? matchesSearch(item.description, term) : false)\n )\n : items;\n return matches.slice(0, MENTION_LIST_LIMIT);\n}\n\n/**\n * Giá trị hiện tại theo định dạng. HTML của tài liệu rỗng là `<p></p>` — trả `\"\"` để \"rỗng\" là MỘT\n * giá trị ở cả hai trình soạn: biểu mẫu kiểm trường bắt buộc bằng `value === \"\"` mà không phải biết\n * trình soạn nào đứng sau.\n */\nfunction readValue(editor: Editor, format: EditorFormat): string {\n if (format === \"markdown\") return trimMarkdown(editor.getMarkdown());\n return editor.isEmpty ? \"\" : editor.getHTML();\n}\n\nconst toBoolean = (value: boolean | \"true\" | \"false\" | undefined) => value === true || value === \"true\";\n\nexport function EditorCore<THandle>({\n format,\n toolbar,\n value,\n defaultValue,\n onChange,\n onSubmit,\n onFocus,\n onBlur,\n placeholder,\n editable = true,\n autoFocus = false,\n variant = \"outline\",\n mentions,\n onImageUpload,\n onImageUploadError,\n resolveImageSrc,\n extensions: extraExtensions,\n labels: labelsProp,\n name,\n id,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n className,\n contentClassName,\n slot,\n handleRef,\n createHandle\n}: EditorCoreProps<THandle>) {\n const labels = useComitorLabels(\"markdownEditor\", labelsProp);\n\n /*\n * Mọi giá trị đổi được SAU KHI dựng đi qua ref: danh sách extension chỉ dựng MỘT lần (đổi nó là\n * dựng lại trình soạn — mất lịch sử hoàn tác, mất con trỏ), nên extension đọc giá trị mới nhất lúc\n * cần chứ không chép lúc dựng.\n */\n const runtime = useAsRef<MarkdownEditorRuntime>({\n placeholder: placeholder ?? labels.placeholder,\n taskCheckbox: labels.taskCheckbox,\n resolveImageSrc,\n resolveMentionLabel: mentions?.resolveLabel,\n resolveMentionHref: mentions?.getHref\n });\n const latest = useAsRef({ onChange, onFocus, onBlur, mentions, onImageUpload, labels, name });\n const imageHandlers = useAsRef<ImageUploadHandlers>({\n upload: onImageUpload,\n onError: (error, file) => {\n if (onImageUploadError) onImageUploadError(error, file);\n else toast(labels.imageUploadFailed, { variant: \"destructive\", description: file.name || undefined });\n },\n resolveSrc: resolveImageSrc,\n uploadingLabel: labels.imageUploading,\n format\n });\n\n const [linkRequest, setLinkRequest] = React.useState(0);\n const requestLink = React.useCallback(() => setLinkRequest((count) => count + 1), []);\n const keymapHandlers = useAsRef<EditorKeymapHandlers>({\n onLinkShortcut: () => {\n if (!editable) return false;\n requestLink();\n return true;\n },\n onSubmitShortcut: () => {\n if (!onSubmit) return false;\n onSubmit();\n return true;\n }\n });\n\n const initialValue = value ?? defaultValue ?? \"\";\n const lastValue = React.useRef(initialValue);\n const [hiddenValue, setHiddenValue] = React.useState(initialValue);\n const pendingUploads = useLazyRef<PendingUploads>(() => new Set()).current;\n const fileInputRef = React.useRef<HTMLInputElement>(null);\n const pickImage = React.useCallback(() => fileInputRef.current?.click(), []);\n\n const slashStore = useLazyRef(\n () => new SuggestionStore<SlashMenuItem, SlashMenuItem>(SLASH_PLUGIN_KEY, (item) => item)\n ).current;\n const mentionStore = useLazyRef(\n () =>\n new SuggestionStore<MarkdownEditorMentionItem, MentionSelection>(MENTION_PLUGIN_KEY, (item) => ({\n id: item.id,\n label: item.label,\n kind: item.kind ?? null\n }))\n ).current;\n\n const [extensions] = React.useState<AnyExtension[]>(() => [\n ...createSchemaExtensions({\n format,\n runtime,\n mentionSuggestion: {\n pluginKey: MENTION_PLUGIN_KEY,\n char: \"@\",\n // Tên người Việt có dấu cách. Khi chữ gõ có dấu cách mà không còn ai khớp, menu tự thoát\n // (`exitWhenEmpty`) — người dùng đang viết câu, không còn tìm người nữa.\n allowSpaces: true,\n debounce: typeof mentions?.items === \"function\" ? 150 : 0,\n items: async ({ query, signal }) => {\n const source = latest.current.mentions?.items;\n if (!source) return [];\n if (typeof source === \"function\") return [...(await source(query, { signal }))];\n return filterMentionItems(source, query);\n },\n allow: ({ editor: current }) =>\n Boolean(latest.current.mentions) &&\n current.isEditable &&\n !current.isActive(\"codeBlock\") &&\n !current.isActive(\"code\"),\n render: mentionStore.render\n }\n }),\n createSlashCommandExtension({\n items: ({ editor: current, query }) =>\n filterSlashCommands(current, query, latest.current.labels, {\n format,\n pickImage: latest.current.onImageUpload ? () => fileInputRef.current?.click() : undefined\n }),\n render: slashStore.render\n }),\n createImageUploadExtension(imageHandlers, pendingUploads),\n createEditorKeymap(keymapHandlers, format),\n ...(extraExtensions ?? [])\n ]);\n\n /*\n * `<label for={id}>` không trỏ được vào một `div contenteditable` (chỉ `input`/`textarea`/… là\n * \"labelable\") — nên `FormField` dựng nhãn đúng mà vùng soạn vẫn không có tên, và bấm nhãn không\n * đưa con trỏ vào. Effect bên dưới tìm nhãn theo `for` và ghi id của nó vào đây; tên trợ năng đi\n * qua `aria-labelledby` trong bộ thuộc tính — nhờ vậy tên dự phòng (`editorLabel`) được BỎ hẳn\n * chứ không nằm song song với nhãn thật.\n */\n const [labelElementId, setLabelElementId] = React.useState<string | null>(null);\n\n const contentPadding = variant === \"outline\" ? \"min-h-16 px-3 py-2\" : \"min-h-6\";\n\n /*\n * Thuộc tính của vùng soạn. Ổn định theo giá trị: `useEditor` so tuỳ chọn mỗi lượt render và gửi\n * lại `editorProps` mỗi khi định danh đổi — tức mỗi lần FormField bật/tắt `aria-invalid` là một\n * lần cập nhật đúng, chứ không phải mỗi phím gõ.\n *\n * Tên trợ năng: chỉ đặt khi SOẠN được (`role=\"textbox\"`). Ở chế độ đọc, vùng nội dung là văn bản\n * thường; `aria-label` trên một `div` không vai trò là thứ ARIA 1.2 cấm và trình đọc màn hình bỏ qua.\n * ⚠ Chế độ đọc phải GHI ĐÈ `role` chứ không bỏ trống: Tiptap luôn trộn `role=\"textbox\"` mặc định\n * vào TRƯỚC thuộc tính của app (`getEditorViewAttributes`), nên thiếu khoá là danh sách bình luận\n * đọc lên thành một dãy \"ô nhập chỉ đọc\" — đã gặp ở harness.\n */\n const editorProps = React.useMemo(() => {\n const attributes: Record<string, string> = {\n class: cn(MARKDOWN_EDITOR_CONTENT_CLASS, contentPadding, contentClassName)\n };\n if (id) attributes.id = id;\n if (!editable) attributes.role = \"none\";\n if (editable) {\n attributes.role = \"textbox\";\n attributes[\"aria-multiline\"] = \"true\";\n const labelledBy = ariaLabelledBy ?? labelElementId;\n if (ariaLabel || !labelledBy) attributes[\"aria-label\"] = ariaLabel ?? labels.editorLabel;\n if (labelledBy) attributes[\"aria-labelledby\"] = labelledBy;\n if (toBoolean(ariaRequired)) attributes[\"aria-required\"] = \"true\";\n }\n if (ariaDescribedBy) attributes[\"aria-describedby\"] = ariaDescribedBy;\n if (toBoolean(ariaInvalid)) attributes[\"aria-invalid\"] = \"true\";\n return { attributes };\n }, [\n contentPadding,\n contentClassName,\n id,\n editable,\n ariaLabel,\n ariaLabelledBy,\n labelElementId,\n ariaDescribedBy,\n ariaInvalid,\n ariaRequired,\n labels.editorLabel\n ]);\n\n const editor = useEditor({\n extensions,\n content: initialValue,\n contentType: format,\n editable,\n autofocus: autoFocus ? \"end\" : false,\n // Next App Router render ở máy chủ: dựng trình soạn ở client sau lần hydrate đầu, không thì HTML\n // hai phía lệch nhau. Khung rỗng giữ chỗ trong khoảng đó — xem phần render.\n immediatelyRender: false,\n editorProps,\n onUpdate: ({ editor: current }) => {\n const next = readValue(current, format);\n lastValue.current = next;\n latest.current.onChange?.(next);\n if (latest.current.name) setHiddenValue(next);\n },\n onFocus: () => latest.current.onFocus?.(),\n onBlur: () => latest.current.onBlur?.()\n });\n\n // Giá trị đổi từ NGOÀI (xoá sau khi gửi, nạp bản nháp…). Giá trị chính mình vừa phát thì bỏ qua —\n // không thì mỗi phím gõ là một lần nạp lại, con trỏ nhảy về đầu.\n React.useEffect(() => {\n if (!editor || value === undefined || value === lastValue.current) return;\n editor.commands.setContent(value, { contentType: format, emitUpdate: false });\n lastValue.current = value;\n setHiddenValue(value);\n }, [editor, value, format]);\n\n // `useEditor` cố ý giữ nguyên `editable` khi cập nhật tuỳ chọn, nên phải tự đặt. Không phát `update`:\n // đổi chế độ không phải đổi nội dung.\n React.useEffect(() => {\n if (editor && editor.isEditable !== editable) editor.setEditable(editable, false);\n }, [editor, editable]);\n\n // Nối `<label for={id}>` — lý do ở chỗ khai `labelElementId`. Bấm nhãn thì tự đưa con trỏ vào.\n React.useEffect(() => {\n if (!editor || !id || !editable || ariaLabel || ariaLabelledBy) return;\n const label = document.querySelector<HTMLLabelElement>(`label[for=\"${CSS.escape(id)}\"]`);\n if (!label) return;\n if (!label.id) label.id = `${id}-label`;\n setLabelElementId(label.id);\n const focusEditor = (event: MouseEvent) => {\n event.preventDefault();\n editor.commands.focus();\n };\n label.addEventListener(\"click\", focusEditor);\n return () => {\n label.removeEventListener(\"click\", focusEditor);\n setLabelElementId(null);\n };\n }, [editor, id, editable, ariaLabel, ariaLabelledBy]);\n\n /*\n * Handle ỔN ĐỊNH suốt đời component, đọc trình soạn MỚI NHẤT qua ref. Dựng lại theo `editor` thì ai\n * giữ đối tượng cũ — một closure, một biến ngoài React — thấy `editor: null` mãi, vì trình soạn chỉ\n * có SAU lượt render đầu (`immediatelyRender: false`). Đã gặp ở harness.\n */\n const editorRef = useAsRef(editor);\n const core = React.useMemo<EditorCoreHandle>(\n () => ({\n focus: (position = \"end\") => {\n editorRef.current?.commands.focus(position);\n },\n getValue: () => (editorRef.current ? readValue(editorRef.current, format) : lastValue.current),\n setValue: (next) => {\n editorRef.current?.commands.setContent(next, { contentType: format, emitUpdate: true });\n },\n clear: () => {\n editorRef.current?.commands.clearContent(true);\n },\n isEmpty: () => (editorRef.current ? editorRef.current.isEmpty : lastValue.current.trim() === \"\"),\n isUploading: () => pendingUploads.size > 0,\n get editor() {\n return editorRef.current;\n }\n }),\n [editorRef, pendingUploads, format]\n );\n React.useImperativeHandle(handleRef, () => createHandle(core), [createHandle, core]);\n\n return (\n <div\n data-slot={slot}\n data-variant={variant}\n data-toolbar={toolbar}\n data-readonly={!editable || undefined}\n className={cn(\n \"relative min-w-0\",\n variant === \"outline\" &&\n // Khuôn khung của `Textarea`: viền `border-input`, nền trong suốt (dark: `--input-fill`),\n // focus `--primary-ink` + vòng `ring/50`; lỗi đọc từ `aria-invalid` của CHÍNH vùng soạn.\n \"rounded-md border border-input bg-transparent shadow-xs transition-colors duration-150 ease-out focus-within:border-primary-ink focus-within:ring-2 focus-within:ring-ring/50 has-[[aria-invalid=true]]:border-destructive-ink dark:bg-input-fill\",\n className\n )}\n >\n {toolbar === \"fixed\" && editable && (\n <FixedToolbar\n editor={editor}\n labels={labels}\n format={format}\n onLink={requestLink}\n onPickImage={onImageUpload ? pickImage : undefined}\n // Khung `outline` bo góc mà KHÔNG `overflow-hidden` (nó sẽ giết `sticky`) — nền thanh tự bo\n // theo góc của khung. `ghost` không có khung: chừa khoảng cho chữ khỏi dính vào viền dưới.\n className={variant === \"outline\" ? \"rounded-t-[inherit]\" : \"mb-3\"}\n />\n )}\n\n {editor ? (\n <EditorContent editor={editor} />\n ) : (\n // Giữ chỗ tới khi trình soạn dựng xong ở client (`immediatelyRender: false`) — cùng chiều cao tối thiểu.\n <div aria-hidden=\"true\" className={cn(contentPadding, contentClassName)} />\n )}\n\n {editor && editable && (\n <BubbleToolbar\n editor={editor}\n labels={labels}\n format={format}\n formatting={toolbar === \"bubble\"}\n linkRequest={linkRequest}\n />\n )}\n\n {editor && (\n <SuggestionMenu\n editor={editor}\n store={slashStore}\n label={labels.slashMenu}\n emptyLabel={labels.slashEmpty}\n getKey={(item) => item.id}\n renderItem={(item) => (\n <>\n <item.icon aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.hint && (\n // Cú pháp gõ tắt — mờ, chữ đều, như phím tắt trong `CommandShortcut`.\n <span className=\"ml-auto font-mono text-xs text-muted-foreground\">{item.hint}</span>\n )}\n </>\n )}\n />\n )}\n\n {editor && mentions && (\n <SuggestionMenu\n editor={editor}\n store={mentionStore}\n label={labels.mentionMenu}\n emptyLabel={labels.mentionEmpty}\n loadingLabel={labels.mentionLoading}\n // Id của hai loại có thể trùng nhau (người `u1` và việc `u1`) — khoá phải mang cả loại.\n getKey={(item) => `${item.kind ?? \"\"}:${item.id}`}\n exitWhenEmpty={(query) => /\\s/.test(query)}\n renderItem={(item) => (\n <>\n {item.icon ? (\n <span aria-hidden=\"true\" className=\"flex size-6 shrink-0 items-center justify-center\">\n <item.icon className=\"size-4 text-muted-foreground\" />\n </span>\n ) : (\n <LetterAvatar name={item.label} src={item.avatarUrl} size=\"xs\" aria-hidden=\"true\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.description && (\n <span className=\"max-w-[45%] truncate text-xs text-muted-foreground\">{item.description}</span>\n )}\n </>\n )}\n />\n )}\n\n {onImageUpload && (\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n tabIndex={-1}\n onChange={(event) => {\n const files = Array.from(event.target.files ?? []).filter((file) => file.type.startsWith(\"image/\"));\n // Xoá giá trị để chọn LẠI đúng tệp đó lần sau vẫn phát `change`.\n event.target.value = \"\";\n if (editor && files.length > 0)\n insertImageFiles(editor, files, undefined, imageHandlers.current, pendingUploads);\n }}\n />\n )}\n\n {name && <input type=\"hidden\" name={name} value={hiddenValue} />}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import '@tiptap/core';
|
|
2
|
-
|
|
2
|
+
import '../components/composite/empty-state.js';
|
|
3
|
+
export { g as EditorBaseProps, f as EditorFormat, E as EditorHandleBase, a as EditorToolbarMode, b as MarkdownEditorMentionItem, c as MarkdownEditorMentionSource, d as MarkdownEditorMentions, e as MarkdownEditorVariant } from '../editor-types-CSrMeuSw.js';
|
|
3
4
|
import './markdown-editor-constants.js';
|
|
5
|
+
import 'react';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Editor } from '@tiptap/core';
|
|
3
|
-
import { f as EditorFormat } from '../editor-types-
|
|
3
|
+
import { f as EditorFormat } from '../editor-types-CSrMeuSw.js';
|
|
4
4
|
import { MarkdownEditorLabels } from './markdown-editor-constants.js';
|
|
5
|
+
import '../components/composite/empty-state.js';
|
|
5
6
|
|
|
6
7
|
interface FixedToolbarProps {
|
|
7
8
|
/** `null` trước khi trình soạn dựng xong — thanh vẫn vẽ, mọi nút khoá. */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import '@tiptap/core';
|
|
2
|
-
export { I as ImageUploadHandlers, M as MarkdownEditorImageUploadResult, P as PendingUploads, h as createImageUploadExtension, i as imageFilesFrom, j as insertImageFiles } from '../editor-types-
|
|
2
|
+
export { I as ImageUploadHandlers, M as MarkdownEditorImageUploadResult, P as PendingUploads, h as createImageUploadExtension, i as imageFilesFrom, j as insertImageFiles } from '../editor-types-CSrMeuSw.js';
|
|
3
|
+
import '../components/composite/empty-state.js';
|
|
4
|
+
import 'react';
|
|
3
5
|
import './markdown-editor-constants.js';
|
package/dist/editor/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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-
|
|
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-CSrMeuSw.js';
|
|
2
2
|
export { MarkdownEditor, MarkdownEditorHandle, MarkdownEditorProps } from './markdown-editor.js';
|
|
3
3
|
export { DEFAULT_MARKDOWN_EDITOR_LABELS, MarkdownEditorBlockId, MarkdownEditorLabels } from './markdown-editor-constants.js';
|
|
4
4
|
export { RichTextEditor, RichTextEditorHandle, RichTextEditorProps } from './rich-text-editor.js';
|
|
5
5
|
import '@tiptap/core';
|
|
6
|
+
import '../components/composite/empty-state.js';
|
|
6
7
|
import 'react';
|
|
@@ -17,7 +17,9 @@ const INLINE = [
|
|
|
17
17
|
"[&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.875em]",
|
|
18
18
|
// 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.
|
|
19
19
|
"[&_[data-type=mention]]:rounded-sm [&_[data-type=mention]]:bg-info/15 [&_[data-type=mention]]:px-0.5",
|
|
20
|
-
"[&_[data-type=mention]]:py-px [&_[data-type=mention]]:font-medium [&_[data-type=mention]]:text-foreground"
|
|
20
|
+
"[&_[data-type=mention]]:py-px [&_[data-type=mention]]:font-medium [&_[data-type=mention]]:text-foreground",
|
|
21
|
+
// Nhắc có địa chỉ (`mentions.getHref`) là `<a>`: gạch chân khi trỏ vào để biết bấm được — màu giữ nguyên.
|
|
22
|
+
"[&_a[data-type=mention]]:underline-offset-4 [&_a[data-type=mention]:hover]:underline"
|
|
21
23
|
];
|
|
22
24
|
const LISTS = [
|
|
23
25
|
"[&_ul:not([data-type=taskList])]:list-disc [&_ol]:list-decimal",
|