@comitor/ui 3.12.0 → 3.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,7 +51,7 @@ function ChatThread({
51
51
  "aria-label": ariaLabel ?? t.log,
52
52
  "aria-busy": busy || void 0,
53
53
  tabIndex: 0,
54
- className: "min-h-0 flex-1 overflow-y-auto overscroll-contain outline-hidden focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-ring",
54
+ className: "relative min-h-0 flex-1 overflow-y-auto overscroll-contain outline-hidden focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-ring",
55
55
  onScroll,
56
56
  children: /* @__PURE__ */ jsx(
57
57
  "div",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/chat/chat-thread.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Bố cục khung hội thoại tham khảo prompt-kit (https://www.prompt-kit.com) — MIT License, Copyright\n * (c) 2025 Julien Thibeaut — và AI Elements của Vercel (Apache-2.0). Không chép mã: cả hai dựa vào\n * `use-stick-to-bottom` và `streamdown`; gói tự viết phần bám đáy (~40 dòng) và vẽ markdown bằng\n * chính `MarkdownEditor` chỉ-đọc. Lý do: docs/chat.md.\n */\nimport { ArrowDown } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/use-isomorphic-layout-effect.js\";\nimport type { ChatLabels } from \"./chat-constants.js\";\n\nexport interface ChatThreadProps extends React.ComponentProps<\"div\"> {\n /**\n * Tác tử đang trả lời. Đặt `aria-busy` lên vùng `log`: trình đọc màn hình CHỜ tới khi xong rồi mới\n * đọc tin mới — không thì nó đọc lại từng mẩu chữ đang về, hàng chục lần một câu.\n */\n busy?: boolean;\n /** Lớp cho cột tin bên trong (mặc định rộng tối đa `max-w-3xl`, căn giữa). */\n contentClassName?: string;\n labels?: Partial<ChatLabels>;\n}\n\n/** Khoảng cách tới đáy (px) mà vẫn coi là \"đang ở cuối\" — chữ mới về thì tự cuộn theo. */\nconst STICK_THRESHOLD = 48;\n\n/**\n * Vùng cuộn của cuộc trò chuyện: `role=\"log\"`, tự bám đáy khi có chữ mới VÀ người dùng đang ở cuối.\n * Người dùng cuộn lên đọc lại thì thôi bám (không giật xuống giữa lúc đọc), và hiện nút \"về tin mới\n * nhất\". Con là các `ChatMessage`.\n */\nexport function ChatThread({\n busy,\n className,\n contentClassName,\n labels,\n children,\n \"aria-label\": ariaLabel,\n ...props\n}: ChatThreadProps) {\n const t = useComitorLabels(\"chat\", labels);\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const contentRef = React.useRef<HTMLDivElement>(null);\n const atBottomRef = React.useRef(true);\n const [atBottom, setAtBottom] = React.useState(true);\n\n const scrollToBottom = React.useCallback((behavior: ScrollBehavior = \"auto\") => {\n const el = scrollRef.current;\n if (el) el.scrollTo({ top: el.scrollHeight, behavior });\n }, []);\n\n const onScroll = () => {\n const el = scrollRef.current;\n if (!el) return;\n const next = el.scrollHeight - el.scrollTop - el.clientHeight <= STICK_THRESHOLD;\n atBottomRef.current = next;\n setAtBottom(next);\n };\n\n // Mở ra là đứng ở tin cuối; nội dung lớn lên (tin mới, chữ đang về, ảnh tải xong) thì bám theo nếu\n // đang ở cuối. Theo dõi CỠ của cột tin chứ không theo `children`: chữ streaming đổi cỡ mà không\n // đổi số con, còn ảnh trong tin đổi cỡ mà không qua React.\n useIsomorphicLayoutEffect(() => {\n scrollToBottom();\n const content = contentRef.current;\n if (!content) return;\n const observer = new ResizeObserver(() => {\n if (atBottomRef.current) scrollToBottom();\n });\n observer.observe(content);\n return () => observer.disconnect();\n }, [scrollToBottom]);\n\n return (\n <div data-slot=\"chat-thread\" className={cn(\"relative flex min-h-0 flex-col\", className)} {...props}>\n <div\n ref={scrollRef}\n role=\"log\"\n aria-label={ariaLabel ?? t.log}\n aria-busy={busy || undefined}\n // Vùng cuộn phải tự nhận focus được — người chỉ dùng bàn phím mới cuộn được lịch sử (axe:\n // scrollable-region-focusable; Chrome tự làm việc này, Safari thì không).\n // biome-ignore lint/a11y/noNoninteractiveTabindex: vùng cuộn, không phải điều khiển — lý do ở trên\n tabIndex={0}\n className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain outline-hidden focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n onScroll={onScroll}\n >\n <div\n ref={contentRef}\n className={cn(\"mx-auto flex w-full max-w-3xl flex-col gap-6 px-4 py-6\", contentClassName)}\n >\n {children}\n </div>\n </div>\n {!atBottom && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label={t.scrollToLatest}\n className=\"absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full bg-card shadow-overlay\"\n onClick={() => {\n atBottomRef.current = true;\n setAtBottom(true);\n scrollToBottom(\"smooth\");\n }}\n >\n <ArrowDown aria-hidden=\"true\" className=\"size-4\" />\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":";AA8EI,SAaI,KAbJ;AAtEJ,SAAS,iBAAiB;AAC1B,YAAY,WAAW;AACvB,SAAS,cAAc;AACvB,SAAS,wBAAwB;AACjC,SAAS,UAAU;AACnB,SAAS,iCAAiC;AAe1C,MAAM,kBAAkB;AAOjB,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAoB;AAClB,QAAM,IAAI,iBAAiB,QAAQ,MAAM;AACzC,QAAM,YAAY,MAAM,OAAuB,IAAI;AACnD,QAAM,aAAa,MAAM,OAAuB,IAAI;AACpD,QAAM,cAAc,MAAM,OAAO,IAAI;AACrC,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,IAAI;AAEnD,QAAM,iBAAiB,MAAM,YAAY,CAAC,WAA2B,WAAW;AAC9E,UAAM,KAAK,UAAU;AACrB,QAAI,GAAI,IAAG,SAAS,EAAE,KAAK,GAAG,cAAc,SAAS,CAAC;AAAA,EACxD,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW,MAAM;AACrB,UAAM,KAAK,UAAU;AACrB,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,GAAG,eAAe,GAAG,YAAY,GAAG,gBAAgB;AACjE,gBAAY,UAAU;AACtB,gBAAY,IAAI;AAAA,EAClB;AAKA,4BAA0B,MAAM;AAC9B,mBAAe;AACf,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS;AACd,UAAM,WAAW,IAAI,eAAe,MAAM;AACxC,UAAI,YAAY,QAAS,gBAAe;AAAA,IAC1C,CAAC;AACD,aAAS,QAAQ,OAAO;AACxB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,cAAc,CAAC;AAEnB,SACE,qBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,kCAAkC,SAAS,GAAI,GAAG,OAC3F;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAY,aAAa,EAAE;AAAA,QAC3B,aAAW,QAAQ;AAAA,QAInB,UAAU;AAAA,QACV,WAAU;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,WAAW,GAAG,0DAA0D,gBAAgB;AAAA,YAEvF;AAAA;AAAA,QACH;AAAA;AAAA,IACF;AAAA,IACC,CAAC,YACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,cAAY,EAAE;AAAA,QACd,WAAU;AAAA,QACV,SAAS,MAAM;AACb,sBAAY,UAAU;AACtB,sBAAY,IAAI;AAChB,yBAAe,QAAQ;AAAA,QACzB;AAAA,QAEA,8BAAC,aAAU,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA,IACnD;AAAA,KAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/chat/chat-thread.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Bố cục khung hội thoại tham khảo prompt-kit (https://www.prompt-kit.com) — MIT License, Copyright\n * (c) 2025 Julien Thibeaut — và AI Elements của Vercel (Apache-2.0). Không chép mã: cả hai dựa vào\n * `use-stick-to-bottom` và `streamdown`; gói tự viết phần bám đáy (~40 dòng) và vẽ markdown bằng\n * chính `MarkdownEditor` chỉ-đọc. Lý do: docs/chat.md.\n */\nimport { ArrowDown } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/use-isomorphic-layout-effect.js\";\nimport type { ChatLabels } from \"./chat-constants.js\";\n\nexport interface ChatThreadProps extends React.ComponentProps<\"div\"> {\n /**\n * Tác tử đang trả lời. Đặt `aria-busy` lên vùng `log`: trình đọc màn hình CHỜ tới khi xong rồi mới\n * đọc tin mới — không thì nó đọc lại từng mẩu chữ đang về, hàng chục lần một câu.\n */\n busy?: boolean;\n /** Lớp cho cột tin bên trong (mặc định rộng tối đa `max-w-3xl`, căn giữa). */\n contentClassName?: string;\n labels?: Partial<ChatLabels>;\n}\n\n/** Khoảng cách tới đáy (px) mà vẫn coi là \"đang ở cuối\" — chữ mới về thì tự cuộn theo. */\nconst STICK_THRESHOLD = 48;\n\n/**\n * Vùng cuộn của cuộc trò chuyện: `role=\"log\"`, tự bám đáy khi có chữ mới VÀ người dùng đang ở cuối.\n * Người dùng cuộn lên đọc lại thì thôi bám (không giật xuống giữa lúc đọc), và hiện nút \"về tin mới\n * nhất\". Con là các `ChatMessage`.\n */\nexport function ChatThread({\n busy,\n className,\n contentClassName,\n labels,\n children,\n \"aria-label\": ariaLabel,\n ...props\n}: ChatThreadProps) {\n const t = useComitorLabels(\"chat\", labels);\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const contentRef = React.useRef<HTMLDivElement>(null);\n const atBottomRef = React.useRef(true);\n const [atBottom, setAtBottom] = React.useState(true);\n\n const scrollToBottom = React.useCallback((behavior: ScrollBehavior = \"auto\") => {\n const el = scrollRef.current;\n if (el) el.scrollTo({ top: el.scrollHeight, behavior });\n }, []);\n\n const onScroll = () => {\n const el = scrollRef.current;\n if (!el) return;\n const next = el.scrollHeight - el.scrollTop - el.clientHeight <= STICK_THRESHOLD;\n atBottomRef.current = next;\n setAtBottom(next);\n };\n\n // Mở ra là đứng ở tin cuối; nội dung lớn lên (tin mới, chữ đang về, ảnh tải xong) thì bám theo nếu\n // đang ở cuối. Theo dõi CỠ của cột tin chứ không theo `children`: chữ streaming đổi cỡ mà không\n // đổi số con, còn ảnh trong tin đổi cỡ mà không qua React.\n useIsomorphicLayoutEffect(() => {\n scrollToBottom();\n const content = contentRef.current;\n if (!content) return;\n const observer = new ResizeObserver(() => {\n if (atBottomRef.current) scrollToBottom();\n });\n observer.observe(content);\n return () => observer.disconnect();\n }, [scrollToBottom]);\n\n return (\n <div data-slot=\"chat-thread\" className={cn(\"relative flex min-h-0 flex-col\", className)} {...props}>\n <div\n ref={scrollRef}\n role=\"log\"\n aria-label={ariaLabel ?? t.log}\n aria-busy={busy || undefined}\n // Vùng cuộn phải tự nhận focus được — người chỉ dùng bàn phím mới cuộn được lịch sử (axe:\n // scrollable-region-focusable; Chrome tự làm việc này, Safari thì không).\n // biome-ignore lint/a11y/noNoninteractiveTabindex: vùng cuộn, không phải điều khiển — lý do ở trên\n tabIndex={0}\n // `relative`: vùng cuộn phải là khối chứa của mọi con `absolute` — nhất là chữ `sr-only` trong\n // tin (\"You\", câu giải thích nhãn AI, \"(opens in a new tab)\"). Không thì chúng neo vào gốc\n // `relative` ở NGOÀI vùng cuộn, thoát khỏi `overflow` của nó, và một khung `overflow-hidden`\n // chiều cao cố định bọc ngoài tự cuộn đi (~200px) khi focus đổi chỗ (3.12.1).\n className=\"relative min-h-0 flex-1 overflow-y-auto overscroll-contain outline-hidden focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n onScroll={onScroll}\n >\n <div\n ref={contentRef}\n className={cn(\"mx-auto flex w-full max-w-3xl flex-col gap-6 px-4 py-6\", contentClassName)}\n >\n {children}\n </div>\n </div>\n {!atBottom && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label={t.scrollToLatest}\n className=\"absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full bg-card shadow-overlay\"\n onClick={() => {\n atBottomRef.current = true;\n setAtBottom(true);\n scrollToBottom(\"smooth\");\n }}\n >\n <ArrowDown aria-hidden=\"true\" className=\"size-4\" />\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":";AA8EI,SAiBI,KAjBJ;AAtEJ,SAAS,iBAAiB;AAC1B,YAAY,WAAW;AACvB,SAAS,cAAc;AACvB,SAAS,wBAAwB;AACjC,SAAS,UAAU;AACnB,SAAS,iCAAiC;AAe1C,MAAM,kBAAkB;AAOjB,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAoB;AAClB,QAAM,IAAI,iBAAiB,QAAQ,MAAM;AACzC,QAAM,YAAY,MAAM,OAAuB,IAAI;AACnD,QAAM,aAAa,MAAM,OAAuB,IAAI;AACpD,QAAM,cAAc,MAAM,OAAO,IAAI;AACrC,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,IAAI;AAEnD,QAAM,iBAAiB,MAAM,YAAY,CAAC,WAA2B,WAAW;AAC9E,UAAM,KAAK,UAAU;AACrB,QAAI,GAAI,IAAG,SAAS,EAAE,KAAK,GAAG,cAAc,SAAS,CAAC;AAAA,EACxD,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW,MAAM;AACrB,UAAM,KAAK,UAAU;AACrB,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,GAAG,eAAe,GAAG,YAAY,GAAG,gBAAgB;AACjE,gBAAY,UAAU;AACtB,gBAAY,IAAI;AAAA,EAClB;AAKA,4BAA0B,MAAM;AAC9B,mBAAe;AACf,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS;AACd,UAAM,WAAW,IAAI,eAAe,MAAM;AACxC,UAAI,YAAY,QAAS,gBAAe;AAAA,IAC1C,CAAC;AACD,aAAS,QAAQ,OAAO;AACxB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,cAAc,CAAC;AAEnB,SACE,qBAAC,SAAI,aAAU,eAAc,WAAW,GAAG,kCAAkC,SAAS,GAAI,GAAG,OAC3F;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAY,aAAa,EAAE;AAAA,QAC3B,aAAW,QAAQ;AAAA,QAInB,UAAU;AAAA,QAKV,WAAU;AAAA,QACV;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,WAAW,GAAG,0DAA0D,gBAAgB;AAAA,YAEvF;AAAA;AAAA,QACH;AAAA;AAAA,IACF;AAAA,IACC,CAAC,YACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,cAAY,EAAE;AAAA,QACd,WAAU;AAAA,QACV,SAAS,MAAM;AACb,sBAAY,UAAU;AACtB,sBAAY,IAAI;AAChB,yBAAe,QAAQ;AAAA,QACzB;AAAA,QAEA,8BAAC,aAAU,eAAY,QAAO,WAAU,UAAS;AAAA;AAAA,IACnD;AAAA,KAEJ;AAEJ;","names":[]}
@@ -183,7 +183,10 @@ function TreeView({
183
183
  indent,
184
184
  canReorder: editable,
185
185
  canDrag: (dragged) => editable && dragged.every((item) => !item.getItemData()?.readOnly),
186
- canDrop: () => editable,
186
+ // Chỉ NHÁNH nhận thả vào trong (gốc là nhánh) — đúng mặc định `target.item.isFolder()` của Headless
187
+ // Tree, mà khai `canDrop` là đè mất. Lá chỉ nhận thả trước/sau; biến lá thành nhánh là `Tab` hoặc
188
+ // `isFolder` (docs/tree-view.md §6). 3.12.0 viết `() => editable`: thả giữa một lá là lồng vào nó.
189
+ canDrop: (_, target) => editable && target.item.isFolder(),
187
190
  onDrop,
188
191
  scrollToItem: (item) => virtualizerRef.current?.scrollToIndex(item.getItemMeta().index, { align: "auto" }),
189
192
  features: FEATURES
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/tree-view.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Lõi cây: Headless Tree (https://headless-tree.lukasbach.com) — MIT License, Copyright (c) 2023\n * Lukas Bach. Gói dùng nó làm THƯ VIỆN (dependency), không chép mã: Headless Tree lo trạng thái\n * focus/mở/chọn, ARIA `tree`/`treeitem`, phím điều hướng theo W3C và kéo thả có đổi cha; phần vẽ,\n * phím dàn ý, đánh số, cột cộng dồn và ảo hoá là của gói. Lý do chọn: docs/tree-view.md.\n */\nimport {\n type DragTarget,\n dragAndDropFeature,\n hotkeysCoreFeature,\n type ItemInstance,\n isOrderedDragTarget,\n selectionFeature,\n syncDataLoaderFeature,\n type TreeInstance\n} from \"@headless-tree/core\";\nimport { useTree } from \"@headless-tree/react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { ChevronRight } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useComitorLabels, useComitorLocale } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport {\n computeTreeNumbering,\n computeTreeRollup,\n groupTreeChildren,\n type TreeViewColumn,\n type TreeViewItem,\n type TreeViewLabels,\n type TreeViewMove\n} from \"./tree-view-constants.js\";\n\nexport interface TreeViewProps<T extends TreeViewItem>\n extends Omit<React.ComponentProps<\"div\">, \"children\" | \"onChange\" | \"onSelect\"> {\n /** Danh sách PHẲNG — `parentId` + thứ tự trong mảng (xem `TreeViewItem`). */\n items: T[];\n /** Nhánh đang mở (controlled). Bỏ trống thì cây tự giữ, bắt đầu từ `defaultExpandedIds`. */\n expandedIds?: string[];\n defaultExpandedIds?: string[];\n onExpandedIdsChange?: (ids: string[]) => void;\n /** Mục đang chọn (controlled). Bấm = chọn một; Ctrl/⌘ = thêm/bớt; Shift = chọn dải. */\n selectedIds?: string[];\n defaultSelectedIds?: string[];\n onSelectedIdsChange?: (ids: string[]) => void;\n /**\n * Người dùng đổi chỗ (kéo thả, Tab, Shift+Tab, Alt+↑/↓). Cây KHÔNG tự ghi: app áp bằng\n * `moveTreeItems` (hoặc gọi API) rồi truyền `items` mới. Không truyền = thứ tự chỉ đọc.\n */\n onMove?: (move: TreeViewMove) => void;\n /** Mở một mục: Enter, hoặc bấm đúp. */\n onItemOpen?: (item: T) => void;\n /** Cột phụ bên phải tên — số liệu có cộng dồn ở nhánh (giờ ước lượng, tiến độ…). */\n columns?: TreeViewColumn<T>[];\n /** Hiện số hiệu dàn ý `1.2.3` trước tên (WBS). */\n numbering?: boolean;\n /** Vẽ phần tên của một dòng — thay cho tên trần (icon, nhãn trạng thái…). */\n renderItem?: (\n item: T,\n state: { level: number; expanded: boolean; folder: boolean; number: string }\n ) => React.ReactNode;\n /** Chiều cao dòng, px (mặc định 32 — và 44 trên thiết bị chạm). */\n rowHeight?: number;\n /** Thụt lề mỗi bậc, px (mặc định 20). */\n indent?: number;\n /** Khoá mọi thao tác đổi chỗ dù có `onMove`. */\n readOnly?: boolean;\n labels?: Partial<TreeViewLabels>;\n}\n\nconst ROOT_ID = \"__comitor_tree_root__\";\n\nfunction useCoarsePointer(): boolean {\n const subscribe = React.useCallback((onChange: () => void) => {\n const mql = window.matchMedia(\"(pointer: coarse)\");\n mql.addEventListener(\"change\", onChange);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n return React.useSyncExternalStore(\n subscribe,\n () => window.matchMedia(\"(pointer: coarse)\").matches,\n () => false\n );\n}\n\n/** State nửa controlled: có `value` thì theo `value`, không thì tự giữ — cùng báo `onChange`. */\nfunction useSemiControlled(\n value: string[] | undefined,\n defaultValue: string[] | undefined,\n onChange: ((next: string[]) => void) | undefined\n) {\n const [inner, setInner] = React.useState<string[]>(defaultValue ?? []);\n const current = value ?? inner;\n const currentRef = React.useRef(current);\n currentRef.current = current;\n const onChangeRef = React.useRef(onChange);\n onChangeRef.current = onChange;\n const controlled = value !== undefined;\n const set = React.useCallback(\n (updater: string[] | ((old: string[]) => string[])) => {\n const next = typeof updater === \"function\" ? updater(currentRef.current) : updater;\n currentRef.current = next;\n if (!controlled) setInner(next);\n onChangeRef.current?.(next);\n },\n [controlled]\n );\n return [current, set] as const;\n}\n\nconst FEATURES = [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature, dragAndDropFeature];\n\n/**\n * Cây có thao tác DÀN Ý — dựng cho WBS (dự án → giai đoạn → hạng mục → việc), dùng được cho mọi\n * cây có thứ tự (danh mục, sơ đồ tổ chức, thư mục).\n *\n * ─ CONTROLLED ─\n * Dữ liệu và thứ tự là của app. Mọi lần đổi chỗ đi ra qua MỘT callback `onMove` (cha mới + vị trí\n * mới), kể cả khi nó đến từ kéo thả hay từ phím; `moveTreeItems` áp nó lên danh sách phẳng.\n *\n * ─ BÀN PHÍM ─ (một điểm dừng Tab — mục đang focus; mũi tên theo mẫu Treeview của W3C)\n * ↑ ↓ — chuyển mục · → — mở nhánh / xuống con đầu · ← — thu nhánh / lên cha · Home End\n * Tab — thụt vào (thành con cuối của mục đứng trên) · Shift+Tab — lùi ra (đứng sau cha cũ)\n * Alt+↑ / Alt+↓ — đổi chỗ với anh em · Enter — `onItemOpen` · Shift+↑/↓, Ctrl+A — chọn\n * Tab chỉ bị giữ lại khi thật sự thụt được; ở mục đầu tiên của một cấp nó đi tiếp ra ngoài như\n * thường — cây không thành cái bẫy bàn phím.\n */\nexport function TreeView<T extends TreeViewItem>({\n items,\n expandedIds,\n defaultExpandedIds,\n onExpandedIdsChange,\n selectedIds,\n defaultSelectedIds,\n onSelectedIdsChange,\n onMove,\n onItemOpen,\n columns,\n numbering = false,\n renderItem,\n rowHeight: rowHeightProp,\n indent = 20,\n readOnly = false,\n labels,\n className,\n \"aria-label\": ariaLabel,\n ...props\n}: TreeViewProps<T>) {\n const t = useComitorLabels(\"treeView\", labels);\n const { locale } = useComitorLocale();\n const coarse = useCoarsePointer();\n const rowHeight = rowHeightProp ?? (coarse ? 44 : 32);\n const editable = !readOnly && !!onMove;\n\n const [expanded, setExpanded] = useSemiControlled(expandedIds, defaultExpandedIds, onExpandedIdsChange);\n const [selected, setSelected] = useSemiControlled(selectedIds, defaultSelectedIds, onSelectedIdsChange);\n\n // ── Dữ liệu ──────────────────────────────────────────────────────────────────────────────────────\n const children = React.useMemo(() => groupTreeChildren(items), [items]);\n const byId = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);\n const numbers = React.useMemo(() => (numbering ? computeTreeNumbering(items) : null), [numbering, items]);\n const rollups = React.useMemo(\n () => (columns ?? []).map((column) => (column.value ? computeTreeRollup(items, column) : null)),\n [columns, items]\n );\n const numberFormat = React.useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }), [locale]);\n\n // Headless Tree đọc dữ liệu qua `dataLoader` — đi qua ref để loader ổn định giữa các lần render.\n const dataRef = React.useRef({ children, byId });\n dataRef.current = { children, byId };\n const dataLoader = React.useMemo(\n () => ({\n getItem: (id: string) => dataRef.current.byId.get(id) ?? ({ id, parentId: null, name: \"\" } as unknown as T),\n getChildren: (id: string) =>\n (dataRef.current.children.get(id === ROOT_ID ? null : id) ?? []).map((child) => child.id)\n }),\n []\n );\n\n const isFolder = (id: string) => (children.get(id)?.length ?? 0) > 0 || !!byId.get(id)?.isFolder;\n\n // ── Lời xướng + chuyển chỗ ───────────────────────────────────────────────────────────────────────\n const [announcement, setAnnouncement] = React.useState(\"\");\n const pendingFocusRef = React.useRef<string | null>(null);\n\n const emitMove = (move: TreeViewMove) => {\n if (!onMove) return;\n const first = byId.get(move.ids[0] ?? \"\");\n const remaining = (children.get(move.parentId) ?? []).filter((item) => !move.ids.includes(item.id));\n const total = remaining.length + move.ids.length;\n const position = Math.min(move.index, remaining.length) + 1;\n onMove(move);\n if (move.parentId) setExpanded((old) => (old.includes(move.parentId!) ? old : [...old, move.parentId!]));\n if (first) {\n const sameParent = move.ids.length === 1 && first.parentId === move.parentId;\n setAnnouncement(\n sameParent\n ? t.movedTo(first.name, position, total)\n : t.movedInto(first.name, move.parentId ? (byId.get(move.parentId)?.name ?? t.root) : t.root, position, total)\n );\n pendingFocusRef.current = first.id;\n }\n };\n\n const siblingsOf = (item: T) =>\n children.get(item.parentId != null && byId.has(item.parentId) ? item.parentId : null) ?? [];\n const canMoveItem = (id: string | undefined): id is string =>\n !!id && id !== ROOT_ID && editable && !byId.get(id)?.readOnly;\n\n const outline = {\n canIndent: (id: string) => {\n const item = byId.get(id);\n return !!item && canMoveItem(id) && siblingsOf(item).indexOf(item) > 0;\n },\n indent: (id: string) => {\n const item = byId.get(id);\n if (!item) return;\n const siblings = siblingsOf(item);\n const prev = siblings[siblings.indexOf(item) - 1];\n if (!prev) return;\n emitMove({ ids: [id], parentId: prev.id, index: children.get(prev.id)?.length ?? 0, kind: \"indent\" });\n },\n canOutdent: (id: string) => {\n const item = byId.get(id);\n return !!item && canMoveItem(id) && item.parentId != null && byId.has(item.parentId);\n },\n outdent: (id: string) => {\n const item = byId.get(id);\n const parent = item?.parentId != null ? byId.get(item.parentId) : undefined;\n if (!item || !parent) return;\n const grand = parent.parentId != null && byId.has(parent.parentId) ? parent.parentId : null;\n const index = (children.get(grand) ?? []).indexOf(parent) + 1;\n emitMove({ ids: [id], parentId: grand, index, kind: \"outdent\" });\n },\n canShift: (id: string, direction: -1 | 1) => {\n const item = byId.get(id);\n if (!item || !canMoveItem(id)) return false;\n const siblings = siblingsOf(item);\n const index = siblings.indexOf(item) + direction;\n return index >= 0 && index < siblings.length;\n },\n shift: (id: string, direction: -1 | 1) => {\n const item = byId.get(id);\n if (!item) return;\n const index = siblingsOf(item).indexOf(item) + direction;\n emitMove({\n ids: [id],\n parentId: siblingsOf(item) === children.get(null) ? null : item.parentId,\n index,\n kind: direction < 0 ? \"move-up\" : \"move-down\"\n });\n }\n };\n\n const onDrop = (dragged: ItemInstance<T>[], target: DragTarget<T>) => {\n const ids = dragged.map((item) => item.getId());\n const targetId = target.item.getId();\n const parentId = targetId === ROOT_ID ? null : targetId;\n const index = isOrderedDragTarget(target)\n ? target.insertionIndex\n : (children.get(parentId) ?? []).filter((item) => !ids.includes(item.id)).length;\n emitMove({ ids, parentId, index, kind: \"drag\" });\n };\n\n // ── Ảo hoá ───────────────────────────────────────────────────────────────────────────────────────\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const headerHeight = columns?.length ? 32 : 0;\n const virtualizerRef = React.useRef<ReturnType<typeof useVirtualizer<HTMLDivElement, Element>> | null>(null);\n\n const tree: TreeInstance<T> = useTree<T>({\n rootItemId: ROOT_ID,\n dataLoader,\n getItemName: (item) => item.getItemData()?.name ?? \"\",\n isItemFolder: (item) => isFolder(item.getId()),\n state: { expandedItems: expanded, selectedItems: selected },\n setExpandedItems: setExpanded,\n setSelectedItems: setSelected,\n indent,\n canReorder: editable,\n canDrag: (dragged) => editable && dragged.every((item) => !item.getItemData()?.readOnly),\n canDrop: () => editable,\n onDrop,\n scrollToItem: (item) => virtualizerRef.current?.scrollToIndex(item.getItemMeta().index, { align: \"auto\" }),\n features: FEATURES\n });\n\n /*\n * Phím dàn ý + Enter tự xử lý ở pha CAPTURE, KHÔNG đăng ký làm hotkey của Headless Tree: bộ\n * hotkey của nó khớp tổ hợp bằng cách tự đếm `keydown` của từng phím đang giữ, nên Shift/Alt nhấn\n * TRƯỚC khi focus vào cây (hoặc sự kiện không kèm `keydown` riêng của phím bổ trợ) là tổ hợp hụt —\n * đo ở harness: Shift+Tab rơi về Tab trần, Alt+↓ thành ↓ (chuyển mục thay vì dời mục). Đọc cờ\n * `shiftKey`/`altKey` của chính sự kiện thì không có trạng thái nào để lệch. React chạy capture ở\n * gốc, trước listener của Headless Tree trên khung cây, nên `stopPropagation` chặn được nó.\n */\n const onKeyDownCapture = (event: React.KeyboardEvent) => {\n if ((event.target as HTMLElement).getAttribute(\"role\") !== \"treeitem\") return;\n if (event.ctrlKey || event.metaKey) return;\n const id = tree.getFocusedItem().getId();\n const run = (action: () => void) => {\n event.preventDefault();\n event.stopPropagation();\n action();\n };\n if (event.key === \"Tab\" && !event.altKey) {\n if (event.shiftKey ? outline.canOutdent(id) : outline.canIndent(id)) {\n run(() => (event.shiftKey ? outline.outdent(id) : outline.indent(id)));\n }\n return;\n }\n if (editable && event.altKey && !event.shiftKey && (event.key === \"ArrowUp\" || event.key === \"ArrowDown\")) {\n const direction = event.key === \"ArrowUp\" ? -1 : 1;\n // Không dời được (đầu/cuối hàng anh em) vẫn nuốt phím: để rơi xuống thì Alt+↓ thành ↓ trần —\n // focus nhảy đi, người dùng tưởng mục đã dời.\n run(() => {\n if (outline.canShift(id, direction)) outline.shift(id, direction);\n });\n return;\n }\n if (event.key === \"Enter\" && !event.shiftKey && !event.altKey && onItemOpen) {\n const data = byId.get(id);\n if (data) run(() => onItemOpen(data));\n }\n };\n\n // Dữ liệu đổi (sau một lần chuyển chỗ, hoặc app nạp thêm) → dựng lại cây, rồi trả focus về mục\n // vừa chuyển: mục đổi cha là một phần tử mới trong DOM, focus cũ đã rơi khỏi cây.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chỉ chạy khi DỮ LIỆU đổi\n React.useEffect(() => {\n tree.rebuildTree();\n const id = pendingFocusRef.current;\n if (id) {\n pendingFocusRef.current = null;\n tree.getItemInstance(id)?.setFocused();\n tree.updateDomFocus();\n }\n }, [items]);\n\n const treeItems = tree.getItems();\n const virtualizer = useVirtualizer({\n count: treeItems.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 10,\n scrollMargin: headerHeight\n });\n virtualizerRef.current = virtualizer;\n // biome-ignore lint/correctness/useExhaustiveDependencies: đo lại khi cỡ dòng đổi (chạm ⇄ chuột)\n React.useEffect(() => {\n virtualizer.measure();\n }, [virtualizer, rowHeight]);\n\n const instructionsId = React.useId();\n\n const columnText = (column: TreeViewColumn<T>, index: number, item: T): string | null => {\n const value = rollups[index]?.get(item.id);\n if (value == null) return null;\n return column.formatText ? column.formatText(value, item) : numberFormat.format(value);\n };\n\n return (\n <div\n data-slot=\"tree-view\"\n className={cn(\"flex min-h-0 flex-col overflow-hidden rounded-lg border bg-card text-card-foreground\", className)}\n {...props}\n >\n <div ref={scrollRef} className=\"relative min-h-0 flex-1 overflow-auto overscroll-contain\">\n {columns && columns.length > 0 && (\n // Tiêu đề cột là của MẮT: cây ARIA không có hàng tiêu đề, nên giá trị cột đi vào tên của\n // từng mục (\"Thiết kế, Ước lượng: 12\").\n <div\n aria-hidden=\"true\"\n data-slot=\"tree-view-header\"\n className=\"sticky top-0 z-10 flex items-center gap-2 border-b bg-card pe-3 font-medium text-muted-foreground text-xs\"\n style={{ height: headerHeight, paddingInlineStart: 8 + 20 }}\n >\n <div className=\"min-w-0 flex-1 truncate\">{t.nameHeader}</div>\n {columns.map((column) => (\n <div\n key={column.id}\n className={cn(\"shrink-0 truncate\", column.align === \"start\" ? \"text-start\" : \"text-end\")}\n style={{ width: column.width ?? 96 }}\n >\n {column.header}\n </div>\n ))}\n </div>\n )}\n\n <div\n {...tree.getContainerProps(ariaLabel ?? t.label)}\n aria-describedby={editable ? instructionsId : undefined}\n className=\"relative outline-hidden\"\n onKeyDownCapture={onKeyDownCapture}\n style={{ height: virtualizer.getTotalSize() }}\n >\n {virtualizer.getVirtualItems().map((virtualItem) => {\n const item = treeItems[virtualItem.index];\n if (!item) return null;\n const data = item.getItemData();\n if (!data) return null;\n const meta = item.getItemMeta();\n const folder = item.isFolder();\n const isExpanded = folder && item.isExpanded();\n const number = numbers?.get(data.id) ?? \"\";\n const itemProps = item.getProps();\n const label = [\n number ? `${number} ${data.name}` : data.name,\n ...(columns ?? []).flatMap((column, index) => {\n const text = columnText(column, index, data);\n return text != null && typeof column.header === \"string\" ? [`${column.header}: ${text}`] : [];\n })\n ].join(\", \");\n\n return (\n <div\n {...itemProps}\n key={item.getKey()}\n // Lặp lại hai thuộc tính Headless Tree đã rải — để biome đọc được vai trò của thẻ.\n role=\"treeitem\"\n tabIndex={item.isFocused() ? 0 : -1}\n aria-label={label}\n data-slot=\"tree-view-item\"\n data-folder={folder || undefined}\n className={cn(\n \"absolute inset-x-0 flex cursor-default select-none items-center gap-2 pe-3 text-foreground text-sm outline-hidden\",\n \"hover:bg-muted focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-ring\",\n item.isSelected() && \"bg-accent text-accent-foreground hover:bg-accent\",\n item.isDragTarget() && \"bg-accent\"\n )}\n style={{\n top: virtualItem.start - headerHeight,\n height: rowHeight,\n paddingInlineStart: 8 + meta.level * indent\n }}\n onDoubleClick={() => onItemOpen?.(data)}\n >\n <span\n aria-hidden=\"true\"\n className=\"flex size-5 shrink-0 items-center justify-center text-muted-foreground\"\n >\n {folder && (\n <ChevronRight\n className={cn(\n \"size-3.5 transition-transform rtl:rotate-180\",\n isExpanded && \"rotate-90 rtl:rotate-90\"\n )}\n />\n )}\n </span>\n {number && (\n <span aria-hidden=\"true\" className=\"shrink-0 text-muted-foreground text-xs tabular-nums\">\n {number}\n </span>\n )}\n <span aria-hidden=\"true\" className={cn(\"min-w-0 flex-1 truncate\", folder && \"font-medium\")}>\n {renderItem\n ? renderItem(data, { level: meta.level, expanded: isExpanded, folder, number })\n : data.name}\n </span>\n {columns?.map((column, index) => {\n const value = rollups[index]?.get(data.id);\n const rolledUp = !!column.rollup && (children.get(data.id)?.length ?? 0) > 0;\n return (\n <span\n key={column.id}\n aria-hidden=\"true\"\n className={cn(\n \"shrink-0 truncate tabular-nums\",\n column.align === \"start\" ? \"text-start\" : \"text-end\",\n rolledUp ? \"font-medium\" : \"text-muted-foreground\"\n )}\n style={{ width: column.width ?? 96 }}\n >\n {column.render\n ? column.render(data)\n : value == null\n ? null\n : column.format\n ? column.format(value, data, { rolledUp })\n : numberFormat.format(value)}\n </span>\n );\n })}\n </div>\n );\n })}\n <div\n aria-hidden=\"true\"\n data-slot=\"tree-view-drag-line\"\n className=\"pointer-events-none -mt-px h-0.5 rounded-full bg-primary-ink\"\n style={tree.getDragLineStyle()}\n />\n </div>\n </div>\n\n {editable && (\n <p id={instructionsId} className=\"sr-only\">\n {t.instructions}\n </p>\n )}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </div>\n </div>\n );\n}\n"],"mappings":";AAkXU,SAME,KANF;AA6CI;AAvZd;AAAA,EAEE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,YAAY,WAAW;AACvB,SAAS,kBAAkB,wBAAwB;AACnD,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AAuCP,MAAM,UAAU;AAEhB,SAAS,mBAA4B;AACnC,QAAM,YAAY,MAAM,YAAY,CAAC,aAAyB;AAC5D,UAAM,MAAM,OAAO,WAAW,mBAAmB;AACjD,QAAI,iBAAiB,UAAU,QAAQ;AACvC,WAAO,MAAM,IAAI,oBAAoB,UAAU,QAAQ;AAAA,EACzD,GAAG,CAAC,CAAC;AACL,SAAO,MAAM;AAAA,IACX;AAAA,IACA,MAAM,OAAO,WAAW,mBAAmB,EAAE;AAAA,IAC7C,MAAM;AAAA,EACR;AACF;AAGA,SAAS,kBACP,OACA,cACA,UACA;AACA,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAmB,gBAAgB,CAAC,CAAC;AACrE,QAAM,UAAU,SAAS;AACzB,QAAM,aAAa,MAAM,OAAO,OAAO;AACvC,aAAW,UAAU;AACrB,QAAM,cAAc,MAAM,OAAO,QAAQ;AACzC,cAAY,UAAU;AACtB,QAAM,aAAa,UAAU;AAC7B,QAAM,MAAM,MAAM;AAAA,IAChB,CAAC,YAAsD;AACrD,YAAM,OAAO,OAAO,YAAY,aAAa,QAAQ,WAAW,OAAO,IAAI;AAC3E,iBAAW,UAAU;AACrB,UAAI,CAAC,WAAY,UAAS,IAAI;AAC9B,kBAAY,UAAU,IAAI;AAAA,IAC5B;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AACA,SAAO,CAAC,SAAS,GAAG;AACtB;AAEA,MAAM,WAAW,CAAC,uBAAuB,kBAAkB,oBAAoB,kBAAkB;AAiB1F,SAAS,SAAiC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,WAAW;AAAA,EACX,SAAS;AAAA,EACT,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAqB;AACnB,QAAM,IAAI,iBAAiB,YAAY,MAAM;AAC7C,QAAM,EAAE,OAAO,IAAI,iBAAiB;AACpC,QAAM,SAAS,iBAAiB;AAChC,QAAM,YAAY,kBAAkB,SAAS,KAAK;AAClD,QAAM,WAAW,CAAC,YAAY,CAAC,CAAC;AAEhC,QAAM,CAAC,UAAU,WAAW,IAAI,kBAAkB,aAAa,oBAAoB,mBAAmB;AACtG,QAAM,CAAC,UAAU,WAAW,IAAI,kBAAkB,aAAa,oBAAoB,mBAAmB;AAGtG,QAAM,WAAW,MAAM,QAAQ,MAAM,kBAAkB,KAAK,GAAG,CAAC,KAAK,CAAC;AACtE,QAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;AACvF,QAAM,UAAU,MAAM,QAAQ,MAAO,YAAY,qBAAqB,KAAK,IAAI,MAAO,CAAC,WAAW,KAAK,CAAC;AACxG,QAAM,UAAU,MAAM;AAAA,IACpB,OAAO,WAAW,CAAC,GAAG,IAAI,CAAC,WAAY,OAAO,QAAQ,kBAAkB,OAAO,MAAM,IAAI,IAAK;AAAA,IAC9F,CAAC,SAAS,KAAK;AAAA,EACjB;AACA,QAAM,eAAe,MAAM,QAAQ,MAAM,IAAI,KAAK,aAAa,QAAQ,EAAE,uBAAuB,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC;AAG9G,QAAM,UAAU,MAAM,OAAO,EAAE,UAAU,KAAK,CAAC;AAC/C,UAAQ,UAAU,EAAE,UAAU,KAAK;AACnC,QAAM,aAAa,MAAM;AAAA,IACvB,OAAO;AAAA,MACL,SAAS,CAAC,OAAe,QAAQ,QAAQ,KAAK,IAAI,EAAE,KAAM,EAAE,IAAI,UAAU,MAAM,MAAM,GAAG;AAAA,MACzF,aAAa,CAAC,QACX,QAAQ,QAAQ,SAAS,IAAI,OAAO,UAAU,OAAO,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,UAAU,MAAM,EAAE;AAAA,IAC5F;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,WAAW,CAAC,QAAgB,SAAS,IAAI,EAAE,GAAG,UAAU,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,GAAG;AAGxF,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,EAAE;AACzD,QAAM,kBAAkB,MAAM,OAAsB,IAAI;AAExD,QAAM,WAAW,CAAC,SAAuB;AACvC,QAAI,CAAC,OAAQ;AACb,UAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,CAAC,KAAK,EAAE;AACxC,UAAM,aAAa,SAAS,IAAI,KAAK,QAAQ,KAAK,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI,SAAS,KAAK,EAAE,CAAC;AAClG,UAAM,QAAQ,UAAU,SAAS,KAAK,IAAI;AAC1C,UAAM,WAAW,KAAK,IAAI,KAAK,OAAO,UAAU,MAAM,IAAI;AAC1D,WAAO,IAAI;AACX,QAAI,KAAK,SAAU,aAAY,CAAC,QAAS,IAAI,SAAS,KAAK,QAAS,IAAI,MAAM,CAAC,GAAG,KAAK,KAAK,QAAS,CAAE;AACvG,QAAI,OAAO;AACT,YAAM,aAAa,KAAK,IAAI,WAAW,KAAK,MAAM,aAAa,KAAK;AACpE;AAAA,QACE,aACI,EAAE,QAAQ,MAAM,MAAM,UAAU,KAAK,IACrC,EAAE,UAAU,MAAM,MAAM,KAAK,WAAY,KAAK,IAAI,KAAK,QAAQ,GAAG,QAAQ,EAAE,OAAQ,EAAE,MAAM,UAAU,KAAK;AAAA,MACjH;AACA,sBAAgB,UAAU,MAAM;AAAA,IAClC;AAAA,EACF;AAEA,QAAM,aAAa,CAAC,SAClB,SAAS,IAAI,KAAK,YAAY,QAAQ,KAAK,IAAI,KAAK,QAAQ,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC;AAC5F,QAAM,cAAc,CAAC,OACnB,CAAC,CAAC,MAAM,OAAO,WAAW,YAAY,CAAC,KAAK,IAAI,EAAE,GAAG;AAEvD,QAAM,UAAU;AAAA,IACd,WAAW,CAAC,OAAe;AACzB,YAAM,OAAO,KAAK,IAAI,EAAE;AACxB,aAAO,CAAC,CAAC,QAAQ,YAAY,EAAE,KAAK,WAAW,IAAI,EAAE,QAAQ,IAAI,IAAI;AAAA,IACvE;AAAA,IACA,QAAQ,CAAC,OAAe;AACtB,YAAM,OAAO,KAAK,IAAI,EAAE;AACxB,UAAI,CAAC,KAAM;AACX,YAAM,WAAW,WAAW,IAAI;AAChC,YAAM,OAAO,SAAS,SAAS,QAAQ,IAAI,IAAI,CAAC;AAChD,UAAI,CAAC,KAAM;AACX,eAAS,EAAE,KAAK,CAAC,EAAE,GAAG,UAAU,KAAK,IAAI,OAAO,SAAS,IAAI,KAAK,EAAE,GAAG,UAAU,GAAG,MAAM,SAAS,CAAC;AAAA,IACtG;AAAA,IACA,YAAY,CAAC,OAAe;AAC1B,YAAM,OAAO,KAAK,IAAI,EAAE;AACxB,aAAO,CAAC,CAAC,QAAQ,YAAY,EAAE,KAAK,KAAK,YAAY,QAAQ,KAAK,IAAI,KAAK,QAAQ;AAAA,IACrF;AAAA,IACA,SAAS,CAAC,OAAe;AACvB,YAAM,OAAO,KAAK,IAAI,EAAE;AACxB,YAAM,SAAS,MAAM,YAAY,OAAO,KAAK,IAAI,KAAK,QAAQ,IAAI;AAClE,UAAI,CAAC,QAAQ,CAAC,OAAQ;AACtB,YAAM,QAAQ,OAAO,YAAY,QAAQ,KAAK,IAAI,OAAO,QAAQ,IAAI,OAAO,WAAW;AACvF,YAAM,SAAS,SAAS,IAAI,KAAK,KAAK,CAAC,GAAG,QAAQ,MAAM,IAAI;AAC5D,eAAS,EAAE,KAAK,CAAC,EAAE,GAAG,UAAU,OAAO,OAAO,MAAM,UAAU,CAAC;AAAA,IACjE;AAAA,IACA,UAAU,CAAC,IAAY,cAAsB;AAC3C,YAAM,OAAO,KAAK,IAAI,EAAE;AACxB,UAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAG,QAAO;AACtC,YAAM,WAAW,WAAW,IAAI;AAChC,YAAM,QAAQ,SAAS,QAAQ,IAAI,IAAI;AACvC,aAAO,SAAS,KAAK,QAAQ,SAAS;AAAA,IACxC;AAAA,IACA,OAAO,CAAC,IAAY,cAAsB;AACxC,YAAM,OAAO,KAAK,IAAI,EAAE;AACxB,UAAI,CAAC,KAAM;AACX,YAAM,QAAQ,WAAW,IAAI,EAAE,QAAQ,IAAI,IAAI;AAC/C,eAAS;AAAA,QACP,KAAK,CAAC,EAAE;AAAA,QACR,UAAU,WAAW,IAAI,MAAM,SAAS,IAAI,IAAI,IAAI,OAAO,KAAK;AAAA,QAChE;AAAA,QACA,MAAM,YAAY,IAAI,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,SAA4B,WAA0B;AACpE,UAAM,MAAM,QAAQ,IAAI,CAAC,SAAS,KAAK,MAAM,CAAC;AAC9C,UAAM,WAAW,OAAO,KAAK,MAAM;AACnC,UAAM,WAAW,aAAa,UAAU,OAAO;AAC/C,UAAM,QAAQ,oBAAoB,MAAM,IACpC,OAAO,kBACN,SAAS,IAAI,QAAQ,KAAK,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,SAAS,KAAK,EAAE,CAAC,EAAE;AAC5E,aAAS,EAAE,KAAK,UAAU,OAAO,MAAM,OAAO,CAAC;AAAA,EACjD;AAGA,QAAM,YAAY,MAAM,OAAuB,IAAI;AACnD,QAAM,eAAe,SAAS,SAAS,KAAK;AAC5C,QAAM,iBAAiB,MAAM,OAA0E,IAAI;AAE3G,QAAM,OAAwB,QAAW;AAAA,IACvC,YAAY;AAAA,IACZ;AAAA,IACA,aAAa,CAAC,SAAS,KAAK,YAAY,GAAG,QAAQ;AAAA,IACnD,cAAc,CAAC,SAAS,SAAS,KAAK,MAAM,CAAC;AAAA,IAC7C,OAAO,EAAE,eAAe,UAAU,eAAe,SAAS;AAAA,IAC1D,kBAAkB;AAAA,IAClB,kBAAkB;AAAA,IAClB;AAAA,IACA,YAAY;AAAA,IACZ,SAAS,CAAC,YAAY,YAAY,QAAQ,MAAM,CAAC,SAAS,CAAC,KAAK,YAAY,GAAG,QAAQ;AAAA,IACvF,SAAS,MAAM;AAAA,IACf;AAAA,IACA,cAAc,CAAC,SAAS,eAAe,SAAS,cAAc,KAAK,YAAY,EAAE,OAAO,EAAE,OAAO,OAAO,CAAC;AAAA,IACzG,UAAU;AAAA,EACZ,CAAC;AAUD,QAAM,mBAAmB,CAAC,UAA+B;AACvD,QAAK,MAAM,OAAuB,aAAa,MAAM,MAAM,WAAY;AACvE,QAAI,MAAM,WAAW,MAAM,QAAS;AACpC,UAAM,KAAK,KAAK,eAAe,EAAE,MAAM;AACvC,UAAM,MAAM,CAAC,WAAuB;AAClC,YAAM,eAAe;AACrB,YAAM,gBAAgB;AACtB,aAAO;AAAA,IACT;AACA,QAAI,MAAM,QAAQ,SAAS,CAAC,MAAM,QAAQ;AACxC,UAAI,MAAM,WAAW,QAAQ,WAAW,EAAE,IAAI,QAAQ,UAAU,EAAE,GAAG;AACnE,YAAI,MAAO,MAAM,WAAW,QAAQ,QAAQ,EAAE,IAAI,QAAQ,OAAO,EAAE,CAAE;AAAA,MACvE;AACA;AAAA,IACF;AACA,QAAI,YAAY,MAAM,UAAU,CAAC,MAAM,aAAa,MAAM,QAAQ,aAAa,MAAM,QAAQ,cAAc;AACzG,YAAM,YAAY,MAAM,QAAQ,YAAY,KAAK;AAGjD,UAAI,MAAM;AACR,YAAI,QAAQ,SAAS,IAAI,SAAS,EAAG,SAAQ,MAAM,IAAI,SAAS;AAAA,MAClE,CAAC;AACD;AAAA,IACF;AACA,QAAI,MAAM,QAAQ,WAAW,CAAC,MAAM,YAAY,CAAC,MAAM,UAAU,YAAY;AAC3E,YAAM,OAAO,KAAK,IAAI,EAAE;AACxB,UAAI,KAAM,KAAI,MAAM,WAAW,IAAI,CAAC;AAAA,IACtC;AAAA,EACF;AAKA,QAAM,UAAU,MAAM;AACpB,SAAK,YAAY;AACjB,UAAM,KAAK,gBAAgB;AAC3B,QAAI,IAAI;AACN,sBAAgB,UAAU;AAC1B,WAAK,gBAAgB,EAAE,GAAG,WAAW;AACrC,WAAK,eAAe;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,YAAY,KAAK,SAAS;AAChC,QAAM,cAAc,eAAe;AAAA,IACjC,OAAO,UAAU;AAAA,IACjB,kBAAkB,MAAM,UAAU;AAAA,IAClC,cAAc,MAAM;AAAA,IACpB,UAAU;AAAA,IACV,cAAc;AAAA,EAChB,CAAC;AACD,iBAAe,UAAU;AAEzB,QAAM,UAAU,MAAM;AACpB,gBAAY,QAAQ;AAAA,EACtB,GAAG,CAAC,aAAa,SAAS,CAAC;AAE3B,QAAM,iBAAiB,MAAM,MAAM;AAEnC,QAAM,aAAa,CAAC,QAA2B,OAAe,SAA2B;AACvF,UAAM,QAAQ,QAAQ,KAAK,GAAG,IAAI,KAAK,EAAE;AACzC,QAAI,SAAS,KAAM,QAAO;AAC1B,WAAO,OAAO,aAAa,OAAO,WAAW,OAAO,IAAI,IAAI,aAAa,OAAO,KAAK;AAAA,EACvF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,wFAAwF,SAAS;AAAA,MAC9G,GAAG;AAAA,MAEJ;AAAA,6BAAC,SAAI,KAAK,WAAW,WAAU,4DAC5B;AAAA,qBAAW,QAAQ,SAAS;AAAA;AAAA,UAG3B;AAAA,YAAC;AAAA;AAAA,cACC,eAAY;AAAA,cACZ,aAAU;AAAA,cACV,WAAU;AAAA,cACV,OAAO,EAAE,QAAQ,cAAc,oBAAoB,IAAI,GAAG;AAAA,cAE1D;AAAA,oCAAC,SAAI,WAAU,2BAA2B,YAAE,YAAW;AAAA,gBACtD,QAAQ,IAAI,CAAC,WACZ;AAAA,kBAAC;AAAA;AAAA,oBAEC,WAAW,GAAG,qBAAqB,OAAO,UAAU,UAAU,eAAe,UAAU;AAAA,oBACvF,OAAO,EAAE,OAAO,OAAO,SAAS,GAAG;AAAA,oBAElC,iBAAO;AAAA;AAAA,kBAJH,OAAO;AAAA,gBAKd,CACD;AAAA;AAAA;AAAA,UACH;AAAA,UAGF;AAAA,YAAC;AAAA;AAAA,cACE,GAAG,KAAK,kBAAkB,aAAa,EAAE,KAAK;AAAA,cAC/C,oBAAkB,WAAW,iBAAiB;AAAA,cAC9C,WAAU;AAAA,cACV;AAAA,cACA,OAAO,EAAE,QAAQ,YAAY,aAAa,EAAE;AAAA,cAE3C;AAAA,4BAAY,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;AAClD,wBAAM,OAAO,UAAU,YAAY,KAAK;AACxC,sBAAI,CAAC,KAAM,QAAO;AAClB,wBAAM,OAAO,KAAK,YAAY;AAC9B,sBAAI,CAAC,KAAM,QAAO;AAClB,wBAAM,OAAO,KAAK,YAAY;AAC9B,wBAAM,SAAS,KAAK,SAAS;AAC7B,wBAAM,aAAa,UAAU,KAAK,WAAW;AAC7C,wBAAM,SAAS,SAAS,IAAI,KAAK,EAAE,KAAK;AACxC,wBAAM,YAAY,KAAK,SAAS;AAChC,wBAAM,QAAQ;AAAA,oBACZ,SAAS,GAAG,MAAM,IAAI,KAAK,IAAI,KAAK,KAAK;AAAA,oBACzC,IAAI,WAAW,CAAC,GAAG,QAAQ,CAAC,QAAQ,UAAU;AAC5C,4BAAM,OAAO,WAAW,QAAQ,OAAO,IAAI;AAC3C,6BAAO,QAAQ,QAAQ,OAAO,OAAO,WAAW,WAAW,CAAC,GAAG,OAAO,MAAM,KAAK,IAAI,EAAE,IAAI,CAAC;AAAA,oBAC9F,CAAC;AAAA,kBACH,EAAE,KAAK,IAAI;AAEX,yBACE;AAAA,oBAAC;AAAA;AAAA,sBACE,GAAG;AAAA,sBACJ,KAAK,KAAK,OAAO;AAAA,sBAEjB,MAAK;AAAA,sBACL,UAAU,KAAK,UAAU,IAAI,IAAI;AAAA,sBACjC,cAAY;AAAA,sBACZ,aAAU;AAAA,sBACV,eAAa,UAAU;AAAA,sBACvB,WAAW;AAAA,wBACT;AAAA,wBACA;AAAA,wBACA,KAAK,WAAW,KAAK;AAAA,wBACrB,KAAK,aAAa,KAAK;AAAA,sBACzB;AAAA,sBACA,OAAO;AAAA,wBACL,KAAK,YAAY,QAAQ;AAAA,wBACzB,QAAQ;AAAA,wBACR,oBAAoB,IAAI,KAAK,QAAQ;AAAA,sBACvC;AAAA,sBACA,eAAe,MAAM,aAAa,IAAI;AAAA;AAAA,oBAEtC;AAAA,sBAAC;AAAA;AAAA,wBACC,eAAY;AAAA,wBACZ,WAAU;AAAA,wBAET,oBACC;AAAA,0BAAC;AAAA;AAAA,4BACC,WAAW;AAAA,8BACT;AAAA,8BACA,cAAc;AAAA,4BAChB;AAAA;AAAA,wBACF;AAAA;AAAA,oBAEJ;AAAA,oBACC,UACC,oBAAC,UAAK,eAAY,QAAO,WAAU,uDAChC,kBACH;AAAA,oBAEF,oBAAC,UAAK,eAAY,QAAO,WAAW,GAAG,2BAA2B,UAAU,aAAa,GACtF,uBACG,WAAW,MAAM,EAAE,OAAO,KAAK,OAAO,UAAU,YAAY,QAAQ,OAAO,CAAC,IAC5E,KAAK,MACX;AAAA,oBACC,SAAS,IAAI,CAAC,QAAQ,UAAU;AAC/B,4BAAM,QAAQ,QAAQ,KAAK,GAAG,IAAI,KAAK,EAAE;AACzC,4BAAM,WAAW,CAAC,CAAC,OAAO,WAAW,SAAS,IAAI,KAAK,EAAE,GAAG,UAAU,KAAK;AAC3E,6BACE;AAAA,wBAAC;AAAA;AAAA,0BAEC,eAAY;AAAA,0BACZ,WAAW;AAAA,4BACT;AAAA,4BACA,OAAO,UAAU,UAAU,eAAe;AAAA,4BAC1C,WAAW,gBAAgB;AAAA,0BAC7B;AAAA,0BACA,OAAO,EAAE,OAAO,OAAO,SAAS,GAAG;AAAA,0BAElC,iBAAO,SACJ,OAAO,OAAO,IAAI,IAClB,SAAS,OACP,OACA,OAAO,SACL,OAAO,OAAO,OAAO,MAAM,EAAE,SAAS,CAAC,IACvC,aAAa,OAAO,KAAK;AAAA;AAAA,wBAf5B,OAAO;AAAA,sBAgBd;AAAA,oBAEJ,CAAC;AAAA,kBACH;AAAA,gBAEJ,CAAC;AAAA,gBACD;AAAA,kBAAC;AAAA;AAAA,oBACC,eAAY;AAAA,oBACZ,aAAU;AAAA,oBACV,WAAU;AAAA,oBACV,OAAO,KAAK,iBAAiB;AAAA;AAAA,gBAC/B;AAAA;AAAA;AAAA,UACF;AAAA,WACF;AAAA,QAEC,YACC,oBAAC,OAAE,IAAI,gBAAgB,WAAU,WAC9B,YAAE,cACL;AAAA,QAEF,oBAAC,SAAI,MAAK,UAAS,aAAU,UAAS,WAAU,WAC7C,wBACH;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/tree-view.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Lõi cây: Headless Tree (https://headless-tree.lukasbach.com) — MIT License, Copyright (c) 2023\n * Lukas Bach. Gói dùng nó làm THƯ VIỆN (dependency), không chép mã: Headless Tree lo trạng thái\n * focus/mở/chọn, ARIA `tree`/`treeitem`, phím điều hướng theo W3C và kéo thả có đổi cha; phần vẽ,\n * phím dàn ý, đánh số, cột cộng dồn và ảo hoá là của gói. Lý do chọn: docs/tree-view.md.\n */\nimport {\n type DragTarget,\n dragAndDropFeature,\n hotkeysCoreFeature,\n type ItemInstance,\n isOrderedDragTarget,\n selectionFeature,\n syncDataLoaderFeature,\n type TreeInstance\n} from \"@headless-tree/core\";\nimport { useTree } from \"@headless-tree/react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { ChevronRight } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useComitorLabels, useComitorLocale } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport {\n computeTreeNumbering,\n computeTreeRollup,\n groupTreeChildren,\n type TreeViewColumn,\n type TreeViewItem,\n type TreeViewLabels,\n type TreeViewMove\n} from \"./tree-view-constants.js\";\n\nexport interface TreeViewProps<T extends TreeViewItem>\n extends Omit<React.ComponentProps<\"div\">, \"children\" | \"onChange\" | \"onSelect\"> {\n /** Danh sách PHẲNG — `parentId` + thứ tự trong mảng (xem `TreeViewItem`). */\n items: T[];\n /** Nhánh đang mở (controlled). Bỏ trống thì cây tự giữ, bắt đầu từ `defaultExpandedIds`. */\n expandedIds?: string[];\n defaultExpandedIds?: string[];\n onExpandedIdsChange?: (ids: string[]) => void;\n /** Mục đang chọn (controlled). Bấm = chọn một; Ctrl/⌘ = thêm/bớt; Shift = chọn dải. */\n selectedIds?: string[];\n defaultSelectedIds?: string[];\n onSelectedIdsChange?: (ids: string[]) => void;\n /**\n * Người dùng đổi chỗ (kéo thả, Tab, Shift+Tab, Alt+↑/↓). Cây KHÔNG tự ghi: app áp bằng\n * `moveTreeItems` (hoặc gọi API) rồi truyền `items` mới. Không truyền = thứ tự chỉ đọc.\n */\n onMove?: (move: TreeViewMove) => void;\n /** Mở một mục: Enter, hoặc bấm đúp. */\n onItemOpen?: (item: T) => void;\n /** Cột phụ bên phải tên — số liệu có cộng dồn ở nhánh (giờ ước lượng, tiến độ…). */\n columns?: TreeViewColumn<T>[];\n /** Hiện số hiệu dàn ý `1.2.3` trước tên (WBS). */\n numbering?: boolean;\n /** Vẽ phần tên của một dòng — thay cho tên trần (icon, nhãn trạng thái…). */\n renderItem?: (\n item: T,\n state: { level: number; expanded: boolean; folder: boolean; number: string }\n ) => React.ReactNode;\n /** Chiều cao dòng, px (mặc định 32 — và 44 trên thiết bị chạm). */\n rowHeight?: number;\n /** Thụt lề mỗi bậc, px (mặc định 20). */\n indent?: number;\n /** Khoá mọi thao tác đổi chỗ dù có `onMove`. */\n readOnly?: boolean;\n labels?: Partial<TreeViewLabels>;\n}\n\nconst ROOT_ID = \"__comitor_tree_root__\";\n\nfunction useCoarsePointer(): boolean {\n const subscribe = React.useCallback((onChange: () => void) => {\n const mql = window.matchMedia(\"(pointer: coarse)\");\n mql.addEventListener(\"change\", onChange);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n return React.useSyncExternalStore(\n subscribe,\n () => window.matchMedia(\"(pointer: coarse)\").matches,\n () => false\n );\n}\n\n/** State nửa controlled: có `value` thì theo `value`, không thì tự giữ — cùng báo `onChange`. */\nfunction useSemiControlled(\n value: string[] | undefined,\n defaultValue: string[] | undefined,\n onChange: ((next: string[]) => void) | undefined\n) {\n const [inner, setInner] = React.useState<string[]>(defaultValue ?? []);\n const current = value ?? inner;\n const currentRef = React.useRef(current);\n currentRef.current = current;\n const onChangeRef = React.useRef(onChange);\n onChangeRef.current = onChange;\n const controlled = value !== undefined;\n const set = React.useCallback(\n (updater: string[] | ((old: string[]) => string[])) => {\n const next = typeof updater === \"function\" ? updater(currentRef.current) : updater;\n currentRef.current = next;\n if (!controlled) setInner(next);\n onChangeRef.current?.(next);\n },\n [controlled]\n );\n return [current, set] as const;\n}\n\nconst FEATURES = [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature, dragAndDropFeature];\n\n/**\n * Cây có thao tác DÀN Ý — dựng cho WBS (dự án → giai đoạn → hạng mục → việc), dùng được cho mọi\n * cây có thứ tự (danh mục, sơ đồ tổ chức, thư mục).\n *\n * ─ CONTROLLED ─\n * Dữ liệu và thứ tự là của app. Mọi lần đổi chỗ đi ra qua MỘT callback `onMove` (cha mới + vị trí\n * mới), kể cả khi nó đến từ kéo thả hay từ phím; `moveTreeItems` áp nó lên danh sách phẳng.\n *\n * ─ BÀN PHÍM ─ (một điểm dừng Tab — mục đang focus; mũi tên theo mẫu Treeview của W3C)\n * ↑ ↓ — chuyển mục · → — mở nhánh / xuống con đầu · ← — thu nhánh / lên cha · Home End\n * Tab — thụt vào (thành con cuối của mục đứng trên) · Shift+Tab — lùi ra (đứng sau cha cũ)\n * Alt+↑ / Alt+↓ — đổi chỗ với anh em · Enter — `onItemOpen` · Shift+↑/↓, Ctrl+A — chọn\n * Tab chỉ bị giữ lại khi thật sự thụt được; ở mục đầu tiên của một cấp nó đi tiếp ra ngoài như\n * thường — cây không thành cái bẫy bàn phím.\n */\nexport function TreeView<T extends TreeViewItem>({\n items,\n expandedIds,\n defaultExpandedIds,\n onExpandedIdsChange,\n selectedIds,\n defaultSelectedIds,\n onSelectedIdsChange,\n onMove,\n onItemOpen,\n columns,\n numbering = false,\n renderItem,\n rowHeight: rowHeightProp,\n indent = 20,\n readOnly = false,\n labels,\n className,\n \"aria-label\": ariaLabel,\n ...props\n}: TreeViewProps<T>) {\n const t = useComitorLabels(\"treeView\", labels);\n const { locale } = useComitorLocale();\n const coarse = useCoarsePointer();\n const rowHeight = rowHeightProp ?? (coarse ? 44 : 32);\n const editable = !readOnly && !!onMove;\n\n const [expanded, setExpanded] = useSemiControlled(expandedIds, defaultExpandedIds, onExpandedIdsChange);\n const [selected, setSelected] = useSemiControlled(selectedIds, defaultSelectedIds, onSelectedIdsChange);\n\n // ── Dữ liệu ──────────────────────────────────────────────────────────────────────────────────────\n const children = React.useMemo(() => groupTreeChildren(items), [items]);\n const byId = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);\n const numbers = React.useMemo(() => (numbering ? computeTreeNumbering(items) : null), [numbering, items]);\n const rollups = React.useMemo(\n () => (columns ?? []).map((column) => (column.value ? computeTreeRollup(items, column) : null)),\n [columns, items]\n );\n const numberFormat = React.useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }), [locale]);\n\n // Headless Tree đọc dữ liệu qua `dataLoader` — đi qua ref để loader ổn định giữa các lần render.\n const dataRef = React.useRef({ children, byId });\n dataRef.current = { children, byId };\n const dataLoader = React.useMemo(\n () => ({\n getItem: (id: string) => dataRef.current.byId.get(id) ?? ({ id, parentId: null, name: \"\" } as unknown as T),\n getChildren: (id: string) =>\n (dataRef.current.children.get(id === ROOT_ID ? null : id) ?? []).map((child) => child.id)\n }),\n []\n );\n\n const isFolder = (id: string) => (children.get(id)?.length ?? 0) > 0 || !!byId.get(id)?.isFolder;\n\n // ── Lời xướng + chuyển chỗ ───────────────────────────────────────────────────────────────────────\n const [announcement, setAnnouncement] = React.useState(\"\");\n const pendingFocusRef = React.useRef<string | null>(null);\n\n const emitMove = (move: TreeViewMove) => {\n if (!onMove) return;\n const first = byId.get(move.ids[0] ?? \"\");\n const remaining = (children.get(move.parentId) ?? []).filter((item) => !move.ids.includes(item.id));\n const total = remaining.length + move.ids.length;\n const position = Math.min(move.index, remaining.length) + 1;\n onMove(move);\n if (move.parentId) setExpanded((old) => (old.includes(move.parentId!) ? old : [...old, move.parentId!]));\n if (first) {\n const sameParent = move.ids.length === 1 && first.parentId === move.parentId;\n setAnnouncement(\n sameParent\n ? t.movedTo(first.name, position, total)\n : t.movedInto(first.name, move.parentId ? (byId.get(move.parentId)?.name ?? t.root) : t.root, position, total)\n );\n pendingFocusRef.current = first.id;\n }\n };\n\n const siblingsOf = (item: T) =>\n children.get(item.parentId != null && byId.has(item.parentId) ? item.parentId : null) ?? [];\n const canMoveItem = (id: string | undefined): id is string =>\n !!id && id !== ROOT_ID && editable && !byId.get(id)?.readOnly;\n\n const outline = {\n canIndent: (id: string) => {\n const item = byId.get(id);\n return !!item && canMoveItem(id) && siblingsOf(item).indexOf(item) > 0;\n },\n indent: (id: string) => {\n const item = byId.get(id);\n if (!item) return;\n const siblings = siblingsOf(item);\n const prev = siblings[siblings.indexOf(item) - 1];\n if (!prev) return;\n emitMove({ ids: [id], parentId: prev.id, index: children.get(prev.id)?.length ?? 0, kind: \"indent\" });\n },\n canOutdent: (id: string) => {\n const item = byId.get(id);\n return !!item && canMoveItem(id) && item.parentId != null && byId.has(item.parentId);\n },\n outdent: (id: string) => {\n const item = byId.get(id);\n const parent = item?.parentId != null ? byId.get(item.parentId) : undefined;\n if (!item || !parent) return;\n const grand = parent.parentId != null && byId.has(parent.parentId) ? parent.parentId : null;\n const index = (children.get(grand) ?? []).indexOf(parent) + 1;\n emitMove({ ids: [id], parentId: grand, index, kind: \"outdent\" });\n },\n canShift: (id: string, direction: -1 | 1) => {\n const item = byId.get(id);\n if (!item || !canMoveItem(id)) return false;\n const siblings = siblingsOf(item);\n const index = siblings.indexOf(item) + direction;\n return index >= 0 && index < siblings.length;\n },\n shift: (id: string, direction: -1 | 1) => {\n const item = byId.get(id);\n if (!item) return;\n const index = siblingsOf(item).indexOf(item) + direction;\n emitMove({\n ids: [id],\n parentId: siblingsOf(item) === children.get(null) ? null : item.parentId,\n index,\n kind: direction < 0 ? \"move-up\" : \"move-down\"\n });\n }\n };\n\n const onDrop = (dragged: ItemInstance<T>[], target: DragTarget<T>) => {\n const ids = dragged.map((item) => item.getId());\n const targetId = target.item.getId();\n const parentId = targetId === ROOT_ID ? null : targetId;\n const index = isOrderedDragTarget(target)\n ? target.insertionIndex\n : (children.get(parentId) ?? []).filter((item) => !ids.includes(item.id)).length;\n emitMove({ ids, parentId, index, kind: \"drag\" });\n };\n\n // ── Ảo hoá ───────────────────────────────────────────────────────────────────────────────────────\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const headerHeight = columns?.length ? 32 : 0;\n const virtualizerRef = React.useRef<ReturnType<typeof useVirtualizer<HTMLDivElement, Element>> | null>(null);\n\n const tree: TreeInstance<T> = useTree<T>({\n rootItemId: ROOT_ID,\n dataLoader,\n getItemName: (item) => item.getItemData()?.name ?? \"\",\n isItemFolder: (item) => isFolder(item.getId()),\n state: { expandedItems: expanded, selectedItems: selected },\n setExpandedItems: setExpanded,\n setSelectedItems: setSelected,\n indent,\n canReorder: editable,\n canDrag: (dragged) => editable && dragged.every((item) => !item.getItemData()?.readOnly),\n // Chỉ NHÁNH nhận thả vào trong (gốc là nhánh) — đúng mặc định `target.item.isFolder()` của Headless\n // Tree, mà khai `canDrop` là đè mất. Lá chỉ nhận thả trước/sau; biến lá thành nhánh là `Tab` hoặc\n // `isFolder` (docs/tree-view.md §6). 3.12.0 viết `() => editable`: thả giữa một lá là lồng vào nó.\n canDrop: (_, target) => editable && target.item.isFolder(),\n onDrop,\n scrollToItem: (item) => virtualizerRef.current?.scrollToIndex(item.getItemMeta().index, { align: \"auto\" }),\n features: FEATURES\n });\n\n /*\n * Phím dàn ý + Enter tự xử lý ở pha CAPTURE, KHÔNG đăng ký làm hotkey của Headless Tree: bộ\n * hotkey của nó khớp tổ hợp bằng cách tự đếm `keydown` của từng phím đang giữ, nên Shift/Alt nhấn\n * TRƯỚC khi focus vào cây (hoặc sự kiện không kèm `keydown` riêng của phím bổ trợ) là tổ hợp hụt —\n * đo ở harness: Shift+Tab rơi về Tab trần, Alt+↓ thành ↓ (chuyển mục thay vì dời mục). Đọc cờ\n * `shiftKey`/`altKey` của chính sự kiện thì không có trạng thái nào để lệch. React chạy capture ở\n * gốc, trước listener của Headless Tree trên khung cây, nên `stopPropagation` chặn được nó.\n */\n const onKeyDownCapture = (event: React.KeyboardEvent) => {\n if ((event.target as HTMLElement).getAttribute(\"role\") !== \"treeitem\") return;\n if (event.ctrlKey || event.metaKey) return;\n const id = tree.getFocusedItem().getId();\n const run = (action: () => void) => {\n event.preventDefault();\n event.stopPropagation();\n action();\n };\n if (event.key === \"Tab\" && !event.altKey) {\n if (event.shiftKey ? outline.canOutdent(id) : outline.canIndent(id)) {\n run(() => (event.shiftKey ? outline.outdent(id) : outline.indent(id)));\n }\n return;\n }\n if (editable && event.altKey && !event.shiftKey && (event.key === \"ArrowUp\" || event.key === \"ArrowDown\")) {\n const direction = event.key === \"ArrowUp\" ? -1 : 1;\n // Không dời được (đầu/cuối hàng anh em) vẫn nuốt phím: để rơi xuống thì Alt+↓ thành ↓ trần —\n // focus nhảy đi, người dùng tưởng mục đã dời.\n run(() => {\n if (outline.canShift(id, direction)) outline.shift(id, direction);\n });\n return;\n }\n if (event.key === \"Enter\" && !event.shiftKey && !event.altKey && onItemOpen) {\n const data = byId.get(id);\n if (data) run(() => onItemOpen(data));\n }\n };\n\n // Dữ liệu đổi (sau một lần chuyển chỗ, hoặc app nạp thêm) → dựng lại cây, rồi trả focus về mục\n // vừa chuyển: mục đổi cha là một phần tử mới trong DOM, focus cũ đã rơi khỏi cây.\n // biome-ignore lint/correctness/useExhaustiveDependencies: chỉ chạy khi DỮ LIỆU đổi\n React.useEffect(() => {\n tree.rebuildTree();\n const id = pendingFocusRef.current;\n if (id) {\n pendingFocusRef.current = null;\n tree.getItemInstance(id)?.setFocused();\n tree.updateDomFocus();\n }\n }, [items]);\n\n const treeItems = tree.getItems();\n const virtualizer = useVirtualizer({\n count: treeItems.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 10,\n scrollMargin: headerHeight\n });\n virtualizerRef.current = virtualizer;\n // biome-ignore lint/correctness/useExhaustiveDependencies: đo lại khi cỡ dòng đổi (chạm ⇄ chuột)\n React.useEffect(() => {\n virtualizer.measure();\n }, [virtualizer, rowHeight]);\n\n const instructionsId = React.useId();\n\n const columnText = (column: TreeViewColumn<T>, index: number, item: T): string | null => {\n const value = rollups[index]?.get(item.id);\n if (value == null) return null;\n return column.formatText ? column.formatText(value, item) : numberFormat.format(value);\n };\n\n return (\n <div\n data-slot=\"tree-view\"\n className={cn(\"flex min-h-0 flex-col overflow-hidden rounded-lg border bg-card text-card-foreground\", className)}\n {...props}\n >\n <div ref={scrollRef} className=\"relative min-h-0 flex-1 overflow-auto overscroll-contain\">\n {columns && columns.length > 0 && (\n // Tiêu đề cột là của MẮT: cây ARIA không có hàng tiêu đề, nên giá trị cột đi vào tên của\n // từng mục (\"Thiết kế, Ước lượng: 12\").\n <div\n aria-hidden=\"true\"\n data-slot=\"tree-view-header\"\n className=\"sticky top-0 z-10 flex items-center gap-2 border-b bg-card pe-3 font-medium text-muted-foreground text-xs\"\n style={{ height: headerHeight, paddingInlineStart: 8 + 20 }}\n >\n <div className=\"min-w-0 flex-1 truncate\">{t.nameHeader}</div>\n {columns.map((column) => (\n <div\n key={column.id}\n className={cn(\"shrink-0 truncate\", column.align === \"start\" ? \"text-start\" : \"text-end\")}\n style={{ width: column.width ?? 96 }}\n >\n {column.header}\n </div>\n ))}\n </div>\n )}\n\n <div\n {...tree.getContainerProps(ariaLabel ?? t.label)}\n aria-describedby={editable ? instructionsId : undefined}\n className=\"relative outline-hidden\"\n onKeyDownCapture={onKeyDownCapture}\n style={{ height: virtualizer.getTotalSize() }}\n >\n {virtualizer.getVirtualItems().map((virtualItem) => {\n const item = treeItems[virtualItem.index];\n if (!item) return null;\n const data = item.getItemData();\n if (!data) return null;\n const meta = item.getItemMeta();\n const folder = item.isFolder();\n const isExpanded = folder && item.isExpanded();\n const number = numbers?.get(data.id) ?? \"\";\n const itemProps = item.getProps();\n const label = [\n number ? `${number} ${data.name}` : data.name,\n ...(columns ?? []).flatMap((column, index) => {\n const text = columnText(column, index, data);\n return text != null && typeof column.header === \"string\" ? [`${column.header}: ${text}`] : [];\n })\n ].join(\", \");\n\n return (\n <div\n {...itemProps}\n key={item.getKey()}\n // Lặp lại hai thuộc tính Headless Tree đã rải — để biome đọc được vai trò của thẻ.\n role=\"treeitem\"\n tabIndex={item.isFocused() ? 0 : -1}\n aria-label={label}\n data-slot=\"tree-view-item\"\n data-folder={folder || undefined}\n className={cn(\n \"absolute inset-x-0 flex cursor-default select-none items-center gap-2 pe-3 text-foreground text-sm outline-hidden\",\n \"hover:bg-muted focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-ring\",\n item.isSelected() && \"bg-accent text-accent-foreground hover:bg-accent\",\n item.isDragTarget() && \"bg-accent\"\n )}\n style={{\n top: virtualItem.start - headerHeight,\n height: rowHeight,\n paddingInlineStart: 8 + meta.level * indent\n }}\n onDoubleClick={() => onItemOpen?.(data)}\n >\n <span\n aria-hidden=\"true\"\n className=\"flex size-5 shrink-0 items-center justify-center text-muted-foreground\"\n >\n {folder && (\n <ChevronRight\n className={cn(\n \"size-3.5 transition-transform rtl:rotate-180\",\n isExpanded && \"rotate-90 rtl:rotate-90\"\n )}\n />\n )}\n </span>\n {number && (\n <span aria-hidden=\"true\" className=\"shrink-0 text-muted-foreground text-xs tabular-nums\">\n {number}\n </span>\n )}\n <span aria-hidden=\"true\" className={cn(\"min-w-0 flex-1 truncate\", folder && \"font-medium\")}>\n {renderItem\n ? renderItem(data, { level: meta.level, expanded: isExpanded, folder, number })\n : data.name}\n </span>\n {columns?.map((column, index) => {\n const value = rollups[index]?.get(data.id);\n const rolledUp = !!column.rollup && (children.get(data.id)?.length ?? 0) > 0;\n return (\n <span\n key={column.id}\n aria-hidden=\"true\"\n className={cn(\n \"shrink-0 truncate tabular-nums\",\n column.align === \"start\" ? \"text-start\" : \"text-end\",\n rolledUp ? \"font-medium\" : \"text-muted-foreground\"\n )}\n style={{ width: column.width ?? 96 }}\n >\n {column.render\n ? column.render(data)\n : value == null\n ? null\n : column.format\n ? column.format(value, data, { rolledUp })\n : numberFormat.format(value)}\n </span>\n );\n })}\n </div>\n );\n })}\n <div\n aria-hidden=\"true\"\n data-slot=\"tree-view-drag-line\"\n className=\"pointer-events-none -mt-px h-0.5 rounded-full bg-primary-ink\"\n style={tree.getDragLineStyle()}\n />\n </div>\n </div>\n\n {editable && (\n <p id={instructionsId} className=\"sr-only\">\n {t.instructions}\n </p>\n )}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -117,7 +117,7 @@ export { DEFAULT_TOUR_LABELS, TourLabels } from './components/ui/tour-constants.
117
117
  export { ComitorLanguage, DEFAULT_COMITOR_LOCALE, getComitorLanguage } from './i18n/locale.js';
118
118
  export { ComitorLocaleProvider, ComitorLocaleProviderProps, useComitorLocale, useComitorMessages } from './i18n/locale-provider.js';
119
119
  export { COMITOR_MESSAGES, ComitorMessages, ComitorMessagesOverride, comitorMessagesEn, comitorMessagesVi, getComitorMessages, mergeComitorMessages } from './i18n/messages.js';
120
- export { AVATAR_TONE_NAMES, AvatarToneColors, AvatarToneName, getAvatarToneIndex } from './lib/avatar-tone.js';
120
+ export { AVATAR_TONE_NAMES, AvatarToneColors, AvatarToneName, LEGACY_AVATAR_TONE_ALIASES, getAvatarToneIndex } from './lib/avatar-tone.js';
121
121
  export { cn } from './lib/cn.js';
122
122
  export { ThemeMode, accent, accentForeground, accentInk, ash, brandNeutral, chart, getAvatarToneColors, gold, highContrast, layout, semantic, status, typography } from './tokens.js';
123
123
  export { DateRange } from 'react-day-picker';
package/dist/index.js CHANGED
@@ -141,7 +141,7 @@ import {
141
141
  getComitorMessages,
142
142
  mergeComitorMessages
143
143
  } from "./i18n/messages.js";
144
- import { AVATAR_TONE_NAMES } from "./lib/avatar-tone.js";
144
+ import { AVATAR_TONE_NAMES, LEGACY_AVATAR_TONE_ALIASES } from "./lib/avatar-tone.js";
145
145
  export * from "./lib/cn.js";
146
146
  export * from "./tokens.js";
147
147
  export {
@@ -150,6 +150,7 @@ export {
150
150
  ComitorLocaleProvider,
151
151
  DEFAULT_COMITOR_LOCALE,
152
152
  IconAvatar,
153
+ LEGACY_AVATAR_TONE_ALIASES,
153
154
  LetterAvatar,
154
155
  MASK_PATTERNS,
155
156
  applyCurrencyMask,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Entry chính \"@comitor/ui\" — Tier 1 (primitive) + Tier 2 (composite) + lib + token.\n *\n * Quy tắc của entry này: **chỉ được phụ thuộc peer BẮT BUỘC** (react, react-dom,\n * lucide-react, tailwindcss). Không `next/*`, không `next-themes`, không `recharts`,\n * không `react-hook-form` — đó đều là peer OPTIONAL. Vì `tsup` chạy `bundle: false`\n * (ESM per-file), chỉ cần barrel này re-export một file có import optional peer là\n * mọi app import \"@comitor/ui\" đều buộc phải cài gói đó, nếu không bundler gãy ở\n * bước resolve. Vì vậy 3 nhánh dưới đây nằm ở entry riêng:\n *\n * - Tier 3 shell (next/*, next-themes) → \"@comitor/ui/shell\"\n * - chart (recharts) → \"@comitor/ui/chart\"\n * - binding react-hook-form → \"@comitor/ui/form\"\n *\n * ⚠ Hai file trong `components/ui/` CỐ Ý không có mặt ở đây:\n * - `form.tsx` → xuất qua \"@comitor/ui/form\" (react-hook-form).\n * - `sonner.tsx` → xuất qua \"@comitor/ui/shell\" dưới tên `SonnerToaster`\n * (nó gọi `useTheme()` của next-themes). Bản dùng hằng ngày là composite\n * `Toaster`/`toast` ở Tier 2 — không phụ thuộc next-themes, có sẵn ở entry này.\n *\n * ⚠ Mọi import nội bộ phải giữ đuôi `.js`: output là ESM per-file, Node ESM thuần\n * không tự suy ra phần mở rộng (bundler thì tha, Node thì không).\n */\n\n/*\n * ⚠ Các file `*-constants` bên dưới — ở CẢ Tier 2 lẫn Tier 1 (từ `2.1.0`: `action-bar`, `cropper`,\n * `kanban`, `key-value`, `mask-input`, `phone-input`, `stepper`, `time-picker`, `tour`) — KHÔNG phải\n * chỗ để gom code cho gọn — chúng là RANH GIỚI SERVER/CLIENT. Component chủ (`command-palette.tsx`,\n * `data-table.tsx`, `kanban.tsx`…) đều mở đầu bằng `\"use client\"`,\n * mà mọi export của một module client đến tay Server Component dưới dạng client-reference proxy:\n * đọc ra `undefined`, KHÔNG throw, KHÔNG warning, trong khi TypeScript vẫn khai kiểu thật. Nhãn mặc\n * định / preset / sentinel là hằng dữ liệu thuần nên phải nằm ngoài ranh giới đó, và barrel này\n * phải xuất chúng THẲNG — re-export vòng qua file client là vô hiệu.\n * Cùng lý do với `chart/chart-colors.ts`; chi tiết ghi ở đầu file đó.\n * `scripts/verify-directives.mjs` canh không cho hằng thuần lọt ngược vào file client.\n */\n\n// ─── Tier 2 — composite (pattern lặp lại ở mọi app Comitor) ───\nexport * from \"./components/composite/avatar-group.js\";\nexport * from \"./components/composite/combobox.js\";\nexport * from \"./components/composite/command-palette.js\";\nexport * from \"./components/composite/command-palette-constants.js\";\nexport * from \"./components/composite/confirm-dialog.js\";\nexport * from \"./components/composite/copy-button.js\";\nexport * from \"./components/composite/data-table.js\";\nexport * from \"./components/composite/data-table-constants.js\";\nexport * from \"./components/composite/date-picker.js\";\nexport * from \"./components/composite/date-picker-constants.js\";\nexport * from \"./components/composite/empty-state.js\";\nexport * from \"./components/composite/filter-chips.js\";\nexport * from \"./components/composite/form-field.js\";\nexport * from \"./components/composite/form-field-constants.js\";\nexport * from \"./components/composite/form-section.js\";\nexport * from \"./components/composite/gantt.js\";\nexport * from \"./components/composite/gantt-constants.js\";\nexport { IconAvatar, type IconAvatarIcon, type IconAvatarProps } from \"./components/composite/icon-avatar.js\";\nexport * from \"./components/composite/inline-edit.js\";\nexport * from \"./components/composite/key-sequence.js\";\nexport * from \"./components/composite/keyboard-hint.js\";\nexport * from \"./components/composite/keyboard-shortcuts-dialog.js\";\nexport type { LetterAvatarProps, LetterAvatarSize } from \"./components/composite/letter-avatar.js\";\n// Xuất CHỌN LỌC (không `export *`): file này còn `AVATAR_TONES` nhập từ `lib/avatar-tone.js`\n// và các helper nội bộ khác. Liệt kê tường minh thì thêm helper riêng tư vào file KHÔNG tự động\n// biến nó thành API công khai — hàng rào mà `export *` không dựng được.\nexport { getAvatarToneClasses, LetterAvatar } from \"./components/composite/letter-avatar.js\";\nexport * from \"./components/composite/page-container.js\";\nexport * from \"./components/composite/page-header.js\";\nexport * from \"./components/composite/relative-time.js\";\nexport * from \"./components/composite/route-tabs.js\";\nexport * from \"./components/composite/search-filter-bar.js\";\nexport * from \"./components/composite/search-filter-bar-constants.js\";\nexport * from \"./components/composite/stat-card.js\";\nexport * from \"./components/composite/status-config.js\";\nexport * from \"./components/composite/status-pill.js\";\nexport * from \"./components/composite/table-pagination.js\";\nexport * from \"./components/composite/table-pagination-constants.js\";\n// Xuất CHỌN LỌC vì cùng lý do với `letter-avatar` ở trên: `text-utils.ts` là chỗ dễ bị thêm\n// helper nội bộ nhất của Tier 2 (0.1.0 từng rò `hashString`/`pickByString` ra API công khai\n// đúng theo đường này). Năm hàm dưới đây là hợp đồng, phần còn lại của file thì không.\nexport {\n getVietnameseInitials,\n matchesSearch,\n normalizeVi,\n removeVietnameseTones,\n truncate\n} from \"./components/composite/text-utils.js\";\nexport * from \"./components/composite/toast.js\";\nexport * from \"./components/composite/tree-view.js\";\nexport * from \"./components/composite/tree-view-constants.js\";\nexport * from \"./components/composite/use-key-sequences.js\";\n// ─── Tier 1 — primitive (shadcn/ui \"new-york\", đã custom theo brand Comitor) ───\nexport * from \"./components/ui/accordion.js\";\nexport * from \"./components/ui/action-bar.js\";\nexport * from \"./components/ui/action-bar-constants.js\";\nexport * from \"./components/ui/alert.js\";\nexport * from \"./components/ui/alert-dialog.js\";\nexport * from \"./components/ui/aspect-ratio.js\";\nexport * from \"./components/ui/avatar.js\";\nexport * from \"./components/ui/badge.js\";\nexport * from \"./components/ui/badge-overflow.js\";\nexport * from \"./components/ui/banner.js\";\nexport * from \"./components/ui/breadcrumb.js\";\nexport * from \"./components/ui/button.js\";\nexport * from \"./components/ui/button-group.js\";\nexport * from \"./components/ui/calendar.js\";\nexport * from \"./components/ui/card.js\";\nexport * from \"./components/ui/carousel.js\";\nexport * from \"./components/ui/checkbox.js\";\nexport * from \"./components/ui/circular-progress.js\";\nexport * from \"./components/ui/collapsible.js\";\nexport * from \"./components/ui/command.js\";\nexport * from \"./components/ui/context-menu.js\";\nexport * from \"./components/ui/cropper.js\";\nexport * from \"./components/ui/cropper-constants.js\";\nexport * from \"./components/ui/dialog.js\";\nexport * from \"./components/ui/drawer.js\";\nexport * from \"./components/ui/dropdown-menu.js\";\nexport * from \"./components/ui/empty.js\";\nexport * from \"./components/ui/field.js\";\nexport * from \"./components/ui/hover-card.js\";\nexport * from \"./components/ui/input.js\";\nexport * from \"./components/ui/input-group.js\";\nexport * from \"./components/ui/input-otp.js\";\nexport * from \"./components/ui/item.js\";\nexport * from \"./components/ui/kanban.js\";\nexport * from \"./components/ui/kanban-constants.js\";\nexport * from \"./components/ui/kbd.js\";\nexport * from \"./components/ui/key-value.js\";\nexport * from \"./components/ui/key-value-constants.js\";\nexport * from \"./components/ui/label.js\";\nexport * from \"./components/ui/mask-input.js\";\n/*\n * Xuất CHỌN LỌC: `mask-input-constants.ts` giữ cả bộ máy thuần của MaskInput (tách khỏi file\n * \"use client\" để hằng `MASK_PATTERNS` không thành proxy ở Server Component), gồm những helper nội\n * bộ như `DEFAULT_CURRENCY` / `REGEX_CACHE`. Chỉ đúng các tên thượng nguồn Dice UI xuất là API.\n */\nexport {\n applyCurrencyMask,\n applyMask,\n applyPercentageMask,\n fromUnmaskedIndex,\n getUnmaskedValue,\n MASK_PATTERNS,\n type MaskPattern,\n toUnmaskedIndex\n} from \"./components/ui/mask-input-constants.js\";\nexport * from \"./components/ui/mention.js\";\nexport * from \"./components/ui/menubar.js\";\nexport * from \"./components/ui/navigation-menu.js\";\nexport * from \"./components/ui/pagination.js\";\nexport * from \"./components/ui/phone-input.js\";\nexport * from \"./components/ui/phone-input-constants.js\";\nexport * from \"./components/ui/popover.js\";\nexport * from \"./components/ui/progress.js\";\nexport * from \"./components/ui/radio-group.js\";\nexport * from \"./components/ui/rating.js\";\nexport * from \"./components/ui/resizable.js\";\nexport * from \"./components/ui/responsive-dialog.js\";\nexport * from \"./components/ui/scroll-area.js\";\nexport * from \"./components/ui/scroll-spy.js\";\nexport * from \"./components/ui/select.js\";\nexport * from \"./components/ui/separator.js\";\nexport * from \"./components/ui/sheet.js\";\nexport * from \"./components/ui/skeleton.js\";\nexport * from \"./components/ui/slider.js\";\nexport * from \"./components/ui/spinner.js\";\nexport * from \"./components/ui/stepper.js\";\nexport * from \"./components/ui/stepper-constants.js\";\nexport * from \"./components/ui/switch.js\";\nexport * from \"./components/ui/table.js\";\nexport * from \"./components/ui/tabs.js\";\n/* Bảng lớp của Tabs xuất THẲNG từ module thuần — `RouteTabs` và `Tabs` dùng chung nó, và hằng\n dữ liệu đi vòng qua file \"use client\" sẽ thành proxy ở Server Component (CHANGELOG 0.2.2 §2). */\nexport * from \"./components/ui/tabs-variants.js\";\nexport * from \"./components/ui/tags-input.js\";\nexport * from \"./components/ui/textarea.js\";\nexport * from \"./components/ui/time-picker.js\";\nexport * from \"./components/ui/time-picker-constants.js\";\nexport * from \"./components/ui/timeline.js\";\nexport * from \"./components/ui/toggle.js\";\nexport * from \"./components/ui/toggle-group.js\";\nexport * from \"./components/ui/tooltip.js\";\nexport * from \"./components/ui/tour.js\";\nexport * from \"./components/ui/tour-constants.js\";\n// ─── Ngôn ngữ (từ 3.0.0) — một provider cho CẢ gói, mặc định tiếng Anh ───\n/*\n * Xuất CHỌN LỌC: `locale-provider.tsx` còn `useComitorLabels` — hook nội bộ các component dùng để\n * trộn `labels` lên bảng chữ, không phải API. Bảng chữ và hàm thuần xuất THẲNG từ module thuần\n * (`messages.ts`, `locale.ts`) chứ không vòng qua file \"use client\" — cùng lý do với các file\n * `*-constants` bên dưới.\n */\nexport { type ComitorLanguage, DEFAULT_COMITOR_LOCALE, getComitorLanguage } from \"./i18n/locale.js\";\nexport {\n ComitorLocaleProvider,\n type ComitorLocaleProviderProps,\n useComitorLocale,\n useComitorMessages\n} from \"./i18n/locale-provider.js\";\nexport {\n COMITOR_MESSAGES,\n type ComitorMessages,\n type ComitorMessagesOverride,\n comitorMessagesEn,\n comitorMessagesVi,\n getComitorMessages,\n mergeComitorMessages\n} from \"./i18n/messages.js\";\n/*\n * `IconAvatar` — em ruột của `LetterAvatar`, dùng chung bảng tone. Xuất kèm `AVATAR_TONE_NAMES`\n * và kiểu `AvatarToneName` vì prop `tone` chỉ có nghĩa khi chỗ gọi liệt kê được tập chọn (màn\n * quản trị cần dựng một `<Select>` từ đúng tập đó).\n *\n * ⚠ KHÔNG xuất `AVATAR_TONES` lẫn `getAvatarToneIndex` ra entry này: bảng thô và hàm băm vẫn là\n * nội bộ, đúng như `lib/avatar-tone.ts` dặn — xuất chúng là mời app dựng lại phép chọn tone của\n * riêng nó, tức cùng một tên ra hai màu.\n */\nexport { AVATAR_TONE_NAMES, type AvatarToneName } from \"./lib/avatar-tone.js\";\n\n// ─── lib + token ───\nexport * from \"./lib/cn.js\";\nexport * from \"./tokens.js\";\n"],"mappings":"AAsCA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,kBAA6D;AACtE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,sBAAsB,oBAAoB;AACnD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAMd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAQd,SAA+B,wBAAwB,0BAA0B;AACjF;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAUP,SAAS,yBAA8C;AAGvD,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Entry chính \"@comitor/ui\" — Tier 1 (primitive) + Tier 2 (composite) + lib + token.\n *\n * Quy tắc của entry này: **chỉ được phụ thuộc peer BẮT BUỘC** (react, react-dom,\n * lucide-react, tailwindcss). Không `next/*`, không `next-themes`, không `recharts`,\n * không `react-hook-form` — đó đều là peer OPTIONAL. Vì `tsup` chạy `bundle: false`\n * (ESM per-file), chỉ cần barrel này re-export một file có import optional peer là\n * mọi app import \"@comitor/ui\" đều buộc phải cài gói đó, nếu không bundler gãy ở\n * bước resolve. Vì vậy 3 nhánh dưới đây nằm ở entry riêng:\n *\n * - Tier 3 shell (next/*, next-themes) → \"@comitor/ui/shell\"\n * - chart (recharts) → \"@comitor/ui/chart\"\n * - binding react-hook-form → \"@comitor/ui/form\"\n *\n * ⚠ Hai file trong `components/ui/` CỐ Ý không có mặt ở đây:\n * - `form.tsx` → xuất qua \"@comitor/ui/form\" (react-hook-form).\n * - `sonner.tsx` → xuất qua \"@comitor/ui/shell\" dưới tên `SonnerToaster`\n * (nó gọi `useTheme()` của next-themes). Bản dùng hằng ngày là composite\n * `Toaster`/`toast` ở Tier 2 — không phụ thuộc next-themes, có sẵn ở entry này.\n *\n * ⚠ Mọi import nội bộ phải giữ đuôi `.js`: output là ESM per-file, Node ESM thuần\n * không tự suy ra phần mở rộng (bundler thì tha, Node thì không).\n */\n\n/*\n * ⚠ Các file `*-constants` bên dưới — ở CẢ Tier 2 lẫn Tier 1 (từ `2.1.0`: `action-bar`, `cropper`,\n * `kanban`, `key-value`, `mask-input`, `phone-input`, `stepper`, `time-picker`, `tour`) — KHÔNG phải\n * chỗ để gom code cho gọn — chúng là RANH GIỚI SERVER/CLIENT. Component chủ (`command-palette.tsx`,\n * `data-table.tsx`, `kanban.tsx`…) đều mở đầu bằng `\"use client\"`,\n * mà mọi export của một module client đến tay Server Component dưới dạng client-reference proxy:\n * đọc ra `undefined`, KHÔNG throw, KHÔNG warning, trong khi TypeScript vẫn khai kiểu thật. Nhãn mặc\n * định / preset / sentinel là hằng dữ liệu thuần nên phải nằm ngoài ranh giới đó, và barrel này\n * phải xuất chúng THẲNG — re-export vòng qua file client là vô hiệu.\n * Cùng lý do với `chart/chart-colors.ts`; chi tiết ghi ở đầu file đó.\n * `scripts/verify-directives.mjs` canh không cho hằng thuần lọt ngược vào file client.\n */\n\n// ─── Tier 2 — composite (pattern lặp lại ở mọi app Comitor) ───\nexport * from \"./components/composite/avatar-group.js\";\nexport * from \"./components/composite/combobox.js\";\nexport * from \"./components/composite/command-palette.js\";\nexport * from \"./components/composite/command-palette-constants.js\";\nexport * from \"./components/composite/confirm-dialog.js\";\nexport * from \"./components/composite/copy-button.js\";\nexport * from \"./components/composite/data-table.js\";\nexport * from \"./components/composite/data-table-constants.js\";\nexport * from \"./components/composite/date-picker.js\";\nexport * from \"./components/composite/date-picker-constants.js\";\nexport * from \"./components/composite/empty-state.js\";\nexport * from \"./components/composite/filter-chips.js\";\nexport * from \"./components/composite/form-field.js\";\nexport * from \"./components/composite/form-field-constants.js\";\nexport * from \"./components/composite/form-section.js\";\nexport * from \"./components/composite/gantt.js\";\nexport * from \"./components/composite/gantt-constants.js\";\nexport { IconAvatar, type IconAvatarIcon, type IconAvatarProps } from \"./components/composite/icon-avatar.js\";\nexport * from \"./components/composite/inline-edit.js\";\nexport * from \"./components/composite/key-sequence.js\";\nexport * from \"./components/composite/keyboard-hint.js\";\nexport * from \"./components/composite/keyboard-shortcuts-dialog.js\";\nexport type { LetterAvatarProps, LetterAvatarSize } from \"./components/composite/letter-avatar.js\";\n// Xuất CHỌN LỌC (không `export *`): file này còn `AVATAR_TONES` nhập từ `lib/avatar-tone.js`\n// và các helper nội bộ khác. Liệt kê tường minh thì thêm helper riêng tư vào file KHÔNG tự động\n// biến nó thành API công khai — hàng rào mà `export *` không dựng được.\nexport { getAvatarToneClasses, LetterAvatar } from \"./components/composite/letter-avatar.js\";\nexport * from \"./components/composite/page-container.js\";\nexport * from \"./components/composite/page-header.js\";\nexport * from \"./components/composite/relative-time.js\";\nexport * from \"./components/composite/route-tabs.js\";\nexport * from \"./components/composite/search-filter-bar.js\";\nexport * from \"./components/composite/search-filter-bar-constants.js\";\nexport * from \"./components/composite/stat-card.js\";\nexport * from \"./components/composite/status-config.js\";\nexport * from \"./components/composite/status-pill.js\";\nexport * from \"./components/composite/table-pagination.js\";\nexport * from \"./components/composite/table-pagination-constants.js\";\n// Xuất CHỌN LỌC vì cùng lý do với `letter-avatar` ở trên: `text-utils.ts` là chỗ dễ bị thêm\n// helper nội bộ nhất của Tier 2 (0.1.0 từng rò `hashString`/`pickByString` ra API công khai\n// đúng theo đường này). Năm hàm dưới đây là hợp đồng, phần còn lại của file thì không.\nexport {\n getVietnameseInitials,\n matchesSearch,\n normalizeVi,\n removeVietnameseTones,\n truncate\n} from \"./components/composite/text-utils.js\";\nexport * from \"./components/composite/toast.js\";\nexport * from \"./components/composite/tree-view.js\";\nexport * from \"./components/composite/tree-view-constants.js\";\nexport * from \"./components/composite/use-key-sequences.js\";\n// ─── Tier 1 — primitive (shadcn/ui \"new-york\", đã custom theo brand Comitor) ───\nexport * from \"./components/ui/accordion.js\";\nexport * from \"./components/ui/action-bar.js\";\nexport * from \"./components/ui/action-bar-constants.js\";\nexport * from \"./components/ui/alert.js\";\nexport * from \"./components/ui/alert-dialog.js\";\nexport * from \"./components/ui/aspect-ratio.js\";\nexport * from \"./components/ui/avatar.js\";\nexport * from \"./components/ui/badge.js\";\nexport * from \"./components/ui/badge-overflow.js\";\nexport * from \"./components/ui/banner.js\";\nexport * from \"./components/ui/breadcrumb.js\";\nexport * from \"./components/ui/button.js\";\nexport * from \"./components/ui/button-group.js\";\nexport * from \"./components/ui/calendar.js\";\nexport * from \"./components/ui/card.js\";\nexport * from \"./components/ui/carousel.js\";\nexport * from \"./components/ui/checkbox.js\";\nexport * from \"./components/ui/circular-progress.js\";\nexport * from \"./components/ui/collapsible.js\";\nexport * from \"./components/ui/command.js\";\nexport * from \"./components/ui/context-menu.js\";\nexport * from \"./components/ui/cropper.js\";\nexport * from \"./components/ui/cropper-constants.js\";\nexport * from \"./components/ui/dialog.js\";\nexport * from \"./components/ui/drawer.js\";\nexport * from \"./components/ui/dropdown-menu.js\";\nexport * from \"./components/ui/empty.js\";\nexport * from \"./components/ui/field.js\";\nexport * from \"./components/ui/hover-card.js\";\nexport * from \"./components/ui/input.js\";\nexport * from \"./components/ui/input-group.js\";\nexport * from \"./components/ui/input-otp.js\";\nexport * from \"./components/ui/item.js\";\nexport * from \"./components/ui/kanban.js\";\nexport * from \"./components/ui/kanban-constants.js\";\nexport * from \"./components/ui/kbd.js\";\nexport * from \"./components/ui/key-value.js\";\nexport * from \"./components/ui/key-value-constants.js\";\nexport * from \"./components/ui/label.js\";\nexport * from \"./components/ui/mask-input.js\";\n/*\n * Xuất CHỌN LỌC: `mask-input-constants.ts` giữ cả bộ máy thuần của MaskInput (tách khỏi file\n * \"use client\" để hằng `MASK_PATTERNS` không thành proxy ở Server Component), gồm những helper nội\n * bộ như `DEFAULT_CURRENCY` / `REGEX_CACHE`. Chỉ đúng các tên thượng nguồn Dice UI xuất là API.\n */\nexport {\n applyCurrencyMask,\n applyMask,\n applyPercentageMask,\n fromUnmaskedIndex,\n getUnmaskedValue,\n MASK_PATTERNS,\n type MaskPattern,\n toUnmaskedIndex\n} from \"./components/ui/mask-input-constants.js\";\nexport * from \"./components/ui/mention.js\";\nexport * from \"./components/ui/menubar.js\";\nexport * from \"./components/ui/navigation-menu.js\";\nexport * from \"./components/ui/pagination.js\";\nexport * from \"./components/ui/phone-input.js\";\nexport * from \"./components/ui/phone-input-constants.js\";\nexport * from \"./components/ui/popover.js\";\nexport * from \"./components/ui/progress.js\";\nexport * from \"./components/ui/radio-group.js\";\nexport * from \"./components/ui/rating.js\";\nexport * from \"./components/ui/resizable.js\";\nexport * from \"./components/ui/responsive-dialog.js\";\nexport * from \"./components/ui/scroll-area.js\";\nexport * from \"./components/ui/scroll-spy.js\";\nexport * from \"./components/ui/select.js\";\nexport * from \"./components/ui/separator.js\";\nexport * from \"./components/ui/sheet.js\";\nexport * from \"./components/ui/skeleton.js\";\nexport * from \"./components/ui/slider.js\";\nexport * from \"./components/ui/spinner.js\";\nexport * from \"./components/ui/stepper.js\";\nexport * from \"./components/ui/stepper-constants.js\";\nexport * from \"./components/ui/switch.js\";\nexport * from \"./components/ui/table.js\";\nexport * from \"./components/ui/tabs.js\";\n/* Bảng lớp của Tabs xuất THẲNG từ module thuần — `RouteTabs` và `Tabs` dùng chung nó, và hằng\n dữ liệu đi vòng qua file \"use client\" sẽ thành proxy ở Server Component (CHANGELOG 0.2.2 §2). */\nexport * from \"./components/ui/tabs-variants.js\";\nexport * from \"./components/ui/tags-input.js\";\nexport * from \"./components/ui/textarea.js\";\nexport * from \"./components/ui/time-picker.js\";\nexport * from \"./components/ui/time-picker-constants.js\";\nexport * from \"./components/ui/timeline.js\";\nexport * from \"./components/ui/toggle.js\";\nexport * from \"./components/ui/toggle-group.js\";\nexport * from \"./components/ui/tooltip.js\";\nexport * from \"./components/ui/tour.js\";\nexport * from \"./components/ui/tour-constants.js\";\n// ─── Ngôn ngữ (từ 3.0.0) — một provider cho CẢ gói, mặc định tiếng Anh ───\n/*\n * Xuất CHỌN LỌC: `locale-provider.tsx` còn `useComitorLabels` — hook nội bộ các component dùng để\n * trộn `labels` lên bảng chữ, không phải API. Bảng chữ và hàm thuần xuất THẲNG từ module thuần\n * (`messages.ts`, `locale.ts`) chứ không vòng qua file \"use client\" — cùng lý do với các file\n * `*-constants` bên dưới.\n */\nexport { type ComitorLanguage, DEFAULT_COMITOR_LOCALE, getComitorLanguage } from \"./i18n/locale.js\";\nexport {\n ComitorLocaleProvider,\n type ComitorLocaleProviderProps,\n useComitorLocale,\n useComitorMessages\n} from \"./i18n/locale-provider.js\";\nexport {\n COMITOR_MESSAGES,\n type ComitorMessages,\n type ComitorMessagesOverride,\n comitorMessagesEn,\n comitorMessagesVi,\n getComitorMessages,\n mergeComitorMessages\n} from \"./i18n/messages.js\";\n/*\n * `IconAvatar` — em ruột của `LetterAvatar`, dùng chung bảng tone. Xuất kèm `AVATAR_TONE_NAMES`\n * và kiểu `AvatarToneName` vì prop `tone` chỉ có nghĩa khi chỗ gọi liệt kê được tập chọn (màn\n * quản trị cần dựng một `<Select>` từ đúng tập đó). `LEGACY_AVATAR_TONE_ALIASES` (tên cũ → tên\n * mới, 3.10.0) đi cùng vì cùng lý do: màn ấy đọc `apps.icon_color` cũ phải ra đúng mục của\n * `<Select>` — CHANGELOG 3.10.0 đã ghi nó là tên mới nhưng tới 3.12.1 mới thật sự xuất ra.\n *\n * ⚠ KHÔNG xuất `AVATAR_TONES` lẫn `getAvatarToneIndex` ra entry này: bảng thô và hàm băm vẫn là\n * nội bộ, đúng như `lib/avatar-tone.ts` dặn — xuất chúng là mời app dựng lại phép chọn tone của\n * riêng nó, tức cùng một tên ra hai màu.\n */\nexport { AVATAR_TONE_NAMES, type AvatarToneName, LEGACY_AVATAR_TONE_ALIASES } from \"./lib/avatar-tone.js\";\n\n// ─── lib + token ───\nexport * from \"./lib/cn.js\";\nexport * from \"./tokens.js\";\n"],"mappings":"AAsCA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,kBAA6D;AACtE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,sBAAsB,oBAAoB;AACnD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAMd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAQd,SAA+B,wBAAwB,0BAA0B;AACjF;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAYP,SAAS,mBAAwC,kCAAkC;AAGnF,cAAc;AACd,cAAc;","names":[]}
@@ -124,6 +124,10 @@ type AvatarToneName = (typeof AVATAR_TONE_NAMES)[number];
124
124
  * Bí danh thay vì giữ tên cũ cho màu mới: tên là hợp đồng (cột `apps.icon_color` của Account), và
125
125
  * một tên nói dối về màu ("ash" mà tím) còn tệ hơn một lần đổi tên có lối lùi. Bí danh đi theo
126
126
  * cùng VỊ TRÍ, nên hàng database còn ghi tên cũ ra đúng màu mà phép băm cũng sẽ cho ở chỗ đó.
127
+ *
128
+ * Xuất ở `@comitor/ui` từ 3.12.1 — app ánh xạ tên cũ đọc từ database bằng chính bảng này thay vì
129
+ * chép một bản. ⚠ `Record` trần: khoá đến từ dữ liệu ngoài thì tra qua `Object.hasOwn` —
130
+ * `LEGACY_AVATAR_TONE_ALIASES["constructor"]` ra một hàm, không ra `undefined`.
127
131
  */
128
132
  declare const LEGACY_AVATAR_TONE_ALIASES: Readonly<Record<string, AvatarToneName>>;
129
133
  declare const AVATAR_TONES: AvatarToneTable<AvatarTone>;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/avatar-tone.ts"],"sourcesContent":["/**\n * NGUỒN SỰ THẬT DUY NHẤT của màu avatar chữ cái — cả gói chỉ có bảng này.\n *\n * File nội bộ (không nằm trong barrel nào): app không import trực tiếp được, mà đi qua đúng\n * hai cửa tuỳ theo chỗ vẽ:\n * • có Tailwind (DOM) → `getAvatarToneClasses(name)` ở `composite/letter-avatar.tsx`\n * • không có Tailwind (canvas, → `getAvatarToneColors(name, mode)` ở `tokens.ts`\n * email HTML, PDF)\n *\n * Vì sao MỘT bảng chứ không phải hai (một bảng class + một bảng hex \"ảnh chiếu\"): hai bảng viết\n * tay song song thì chỉ cần một lần thêm/xoá/đảo tone là cùng một người ra hai màu khác nhau giữa\n * web và email — mà không có gì báo. Ở đây mỗi tone là MỘT phần tử mang cả chuỗi class lẫn công\n * thức hex tương ứng, nằm sát nhau trên vài dòng liền: thêm tone là thêm cả ba, không quên được.\n * Thứ tự phần tử = chỉ số băm, nên thứ tự cũng chỉ tồn tại ở một chỗ.\n *\n * Vì sao công thức hex là HÀM nhận `palette` chứ không đọc thẳng token: `tokens.ts` là entry\n * `@comitor/ui/tokens`; nếu file này `import { accent } from \"../tokens.js\"` thì thành vòng import\n * (tokens.ts phải import ngược lại bảng này) — vòng có khởi tạo hằng ở giữa là lỗi TDZ lúc chạy.\n * Nhận bảng màu qua tham số thì phụ thuộc chỉ đi MỘT chiều: tokens.ts → file này.\n */\n\n/** Bảy sắc màu phụ mà bảng tone đọc (gold đi riêng qua thang của nó). */\ninterface AvatarToneHues {\n readonly teal: string;\n readonly green: string;\n readonly red: string;\n readonly yellow: string;\n readonly blue: string;\n readonly purple: string;\n readonly pink: string;\n}\n\n/** Đúng 8 tone — chốt ở tầng kiểu: `getAvatarToneIndex` băm theo độ dài này. */\ntype AvatarToneTable<T> = readonly [T, T, T, T, T, T, T, T];\n\n/** Cặp nền + màu chữ (hex đục) của một tone avatar. */\nexport interface AvatarToneColors {\n background: string;\n foreground: string;\n}\n\n/**\n * Các bậc màu mà công thức hex bên dưới đọc vào — `tokens.ts` truyền xuống một bộ cho light,\n * một bộ cho dark. Khai tường minh bằng `string` (không `typeof gold`) vì bản light và bản dark\n * là hai bộ giá trị khác nhau, không cùng một kiểu literal.\n */\nexport interface AvatarTonePalette {\n /** Thang gold — cần bậc 100 · 500 · 600. */\n gold: { readonly 100: string; readonly 500: string; readonly 600: string };\n /** Màu phụ ở vai trò TÔ NỀN (`accent.light` hoặc `accent.dark`). */\n accent: AvatarToneHues;\n /** Cùng màu phụ đó ở vai trò CHỮ trên nền trang (`accentInk.light` hoặc `accentInk.dark`). */\n accentInk: AvatarToneHues;\n /** Nền mà tint hợp thành lên — `--card` của theme. */\n card: string;\n}\n\ninterface AvatarTone {\n /**\n * Tên gọi của tone — để chỗ dùng CHỌN TAY một màu thay vì để hàm băm quyết.\n *\n * ⚠ Tên là một phần của HỢP ĐỒNG: nó đi vào cột database của app (`apps.icon_color` bên\n * `comitor-account`) và vào prop `tone` của `IconAvatar`. Đổi tên một tone là làm mọi hàng đã\n * lưu trỏ vào hư không — và hỏng đó IM LẶNG, vì giá trị lạ chỉ rơi về màu suy theo tên.\n * Thêm tone thì thêm tên mới; đừng đổi tên cũ.\n */\n readonly name: AvatarToneName;\n /** Vai trò DOM: chuỗi class token tĩnh (kèm biến thể `dark:`). */\n readonly classes: string;\n /** Vai trò canvas/email/PDF: đúng những gì `classes` nói, tính ra hex đục ở light. */\n readonly light: (p: AvatarTonePalette) => AvatarToneColors;\n /** Nhánh `dark:` của `classes`. */\n readonly dark: (p: AvatarTonePalette) => AvatarToneColors;\n}\n\n/**\n * Trộn `color` phủ `alpha` lên nền `over`, trả về hex đục.\n *\n * Tái hiện đúng phép mà trình duyệt làm với `bg-teal/15`: Tailwind sinh ra màu CÓ alpha, còn nền\n * phía dưới (`--card`) mới quyết định hex cuối cùng. Nơi không có CSS thì không có gì để hợp\n * thành, nên hex phải mang sẵn kết quả đã trộn.\n */\nfunction tintOver(color: string, over: string, alpha: number): string {\n const channel = (hex: string, i: number) => Number.parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16);\n const mixed = [0, 1, 2].map((i) => Math.round(channel(color, i) * alpha + channel(over, i) * (1 - alpha)));\n return `#${mixed.map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`.toUpperCase();\n}\n\n/**\n * 8 tone deterministic. Phía class TẤT CẢ là token tĩnh — không ghép chuỗi lúc chạy (`bg-${c}-100`)\n * vì Tailwind quét tĩnh, class ghép động sẽ không có mặt trong CSS của app; cũng không dùng hex\n * inline vì như vậy là mất dark mode.\n *\n * ─ VÌ SAO LÀ MÀU PHỤ + CHART CHỨ KHÔNG PHẢI TOKEN SEMANTIC ─\n * Đây là màu ĐỊNH DANH (phân biệt người), không phải màu TRẠNG THÁI — gán `--success` cho một con\n * người là sai nghĩa. Bộ màu phụ + bảng chart là nhóm token DUY NHẤT có đủ 8 sắc khác nhau và có\n * bản riêng cho dark, nên vẫn đổi theo theme và app vẫn ghi đè được.\n *\n * ─ TƯƠNG PHẢN: VÌ SAO BA TONE PHẢI HẠ XUỐNG BẬC INK Ở LIGHT ─\n * Chữ initials tuy `aria-hidden` nhưng người sáng mắt vẫn đọc, nên vẫn phải đạt AA 4,5:1. Bản đầu\n * dùng thẳng `text-teal/green/red` trên nền tint 15% và HỎNG ở light (đo theo WCAG, nền tint đã\n * trộn với `--card` trắng):\n *\n * teal #1F8A8B trên bg-teal/15 = 3,44:1 ✗\n * green #2EA56A trên bg-green/15 = 2,69:1 ✗ (dưới cả ngưỡng 3:1)\n * red #D64545 trên bg-red/15 = 3,57:1 ✗\n *\n * Sửa đúng khuôn `Badge`: dùng bậc `--x-ink` cho light, giữ token đã nâng sáng cho dark. Đo lại\n * trên đúng hex mà `getAvatarToneColors` trả ra (styles.css đã đóng băng bậc ink, xem dưới):\n *\n * teal #1A6668 trên #DDEDEE = 5,54:1 ✓ · dark #2DD4BF trên #193435 = 7,12:1 ✓\n * green #216A48 trên #E0F2E9 = 5,60:1 ✓ · dark #4ADE80 trên #1E362C = 7,44:1 ✓\n * red #9A3537 trên #F9E3E3 = 5,83:1 ✓ · dark #F87171 trên #38252A = 5,18:1 ✓\n *\n * (Bộ số 5,56 / 5,58 / 7,10 / 7,47 / 5,17 ghi ở bản trước là kết quả của chuỗi `color-mix` TRƯỚC\n * khi hex được đóng băng vào token — lệch ở chữ, không lệch ở màu.)\n *\n * Class viết thẳng `text-teal-ink` / `text-green-ink` / `text-red-ink` / `text-yellow-ink` chứ\n * KHÔNG nhắc lại chuỗi `color-mix(...)` — hai cách cho ra đúng cùng một hex (styles.css đã đóng\n * băng kết quả: #1A6668 · #216A48 · #9A3537), và ở `.dark` thì `--teal-ink` vốn trỏ về `--teal`\n * nên không cần biến thể `dark:` nào. Nhờ vậy tên trong class khớp đúng tên trong công thức hex\n * ngay bên dưới (`p.accentInk.teal`) — soi hai vế cạnh nhau là thấy lệch ngay.\n */\n/**\n * Tên của 8 tone, ĐÚNG THỨ TỰ bảng bên dưới — thứ tự ấy cũng là chỉ số băm, nên hai thứ không\n * được trôi khỏi nhau. Đây là bảng màu mà chỗ dùng chọn tay khi không muốn để tên quyết định.\n *\n * Tên lấy theo SẮC, không theo vai trò ngữ nghĩa: đây là màu ĐỊNH DANH, không phải màu trạng\n * thái — xem lý do dài ở docblock của `AVATAR_TONES`.\n */\nexport const AVATAR_TONE_NAMES = [\"gold\", \"teal\", \"green\", \"red\", \"yellow\", \"blue\", \"purple\", \"pink\"] as const;\n\nexport type AvatarToneName = (typeof AVATAR_TONE_NAMES)[number];\n\n/**\n * Tên tone CŨ → tên tone hôm nay (3.10.0). Ba tone cuối từng là `navy` · `ash` · `neutral` — cả ba\n * đều là xám ám xanh ở các độ đậm khác nhau, nên ô icon của ba app khác nhau trông như cùng một\n * màu. Chúng được THAY bằng ba sắc thật, đứng ĐÚNG vị trí cũ trong bảng (chỉ số băm giữ nguyên:\n * người/app nào trước ra `navy` thì nay ra `blue`, không ai nhảy sang tone khác).\n *\n * Bí danh thay vì giữ tên cũ cho màu mới: tên là hợp đồng (cột `apps.icon_color` của Account), và\n * một tên nói dối về màu (\"ash\" mà tím) còn tệ hơn một lần đổi tên có lối lùi. Bí danh đi theo\n * cùng VỊ TRÍ, nên hàng database còn ghi tên cũ ra đúng màu mà phép băm cũng sẽ cho ở chỗ đó.\n */\nexport const LEGACY_AVATAR_TONE_ALIASES: Readonly<Record<string, AvatarToneName>> = {\n navy: \"blue\",\n ash: \"purple\",\n neutral: \"pink\"\n};\n\nexport const AVATAR_TONES: AvatarToneTable<AvatarTone> = [\n {\n name: \"gold\",\n classes: \"bg-gold-100 text-gold-600 dark:bg-gold-500/30 dark:text-gold-100\",\n light: (p) => ({ background: p.gold[100], foreground: p.gold[600] }),\n dark: (p) => ({ background: tintOver(p.gold[500], p.card, 0.3), foreground: p.gold[100] })\n },\n {\n name: \"teal\",\n classes: \"bg-teal/15 text-teal-ink\",\n light: (p) => ({ background: tintOver(p.accent.teal, p.card, 0.15), foreground: p.accentInk.teal }),\n dark: (p) => ({ background: tintOver(p.accent.teal, p.card, 0.15), foreground: p.accentInk.teal })\n },\n {\n name: \"green\",\n classes: \"bg-green/15 text-green-ink\",\n light: (p) => ({ background: tintOver(p.accent.green, p.card, 0.15), foreground: p.accentInk.green }),\n dark: (p) => ({ background: tintOver(p.accent.green, p.card, 0.15), foreground: p.accentInk.green })\n },\n {\n name: \"red\",\n classes: \"bg-red/15 text-red-ink\",\n light: (p) => ({ background: tintOver(p.accent.red, p.card, 0.15), foreground: p.accentInk.red }),\n dark: (p) => ({ background: tintOver(p.accent.red, p.card, 0.15), foreground: p.accentInk.red })\n },\n {\n name: \"yellow\",\n classes: \"bg-yellow/25 text-yellow-ink dark:bg-yellow/20\",\n light: (p) => ({ background: tintOver(p.accent.yellow, p.card, 0.25), foreground: p.accentInk.yellow }),\n dark: (p) => ({ background: tintOver(p.accent.yellow, p.card, 0.2), foreground: p.accentInk.yellow })\n },\n {\n // Ba tone cuối: ba sắc ĐỊNH DANH riêng của bảng này (styles.css, khối \"Màu phụ\") — thay navy ·\n // ash · neutral từ 3.10.0, xem `LEGACY_AVATAR_TONE_ALIASES`. Cùng khuôn teal/green/red: tint /15\n // + bậc ink; ở dark `--x-ink` trỏ về bản dark riêng của fill nên một chuỗi class đủ cả hai theme.\n // Chữ trên tint /15: blue 5,67 · purple 5,72 · pink 5,33:1 (light) · 5,76 · 5,17 · 5,37:1 (dark).\n name: \"blue\",\n classes: \"bg-blue/15 text-blue-ink\",\n light: (p) => ({ background: tintOver(p.accent.blue, p.card, 0.15), foreground: p.accentInk.blue }),\n dark: (p) => ({ background: tintOver(p.accent.blue, p.card, 0.15), foreground: p.accentInk.blue })\n },\n {\n name: \"purple\",\n classes: \"bg-purple/15 text-purple-ink\",\n light: (p) => ({ background: tintOver(p.accent.purple, p.card, 0.15), foreground: p.accentInk.purple }),\n dark: (p) => ({ background: tintOver(p.accent.purple, p.card, 0.15), foreground: p.accentInk.purple })\n },\n {\n name: \"pink\",\n classes: \"bg-pink/15 text-pink-ink\",\n light: (p) => ({ background: tintOver(p.accent.pink, p.card, 0.15), foreground: p.accentInk.pink }),\n dark: (p) => ({ background: tintOver(p.accent.pink, p.card, 0.15), foreground: p.accentInk.pink })\n }\n];\n\n/**\n * Hash chuỗi → số nguyên không âm (biến thể FNV-1a 32 bit).\n *\n * Nội bộ, KHÔNG xuất ra API: công dụng duy nhất của nó là chọn tone avatar ngay dưới đây. Xuất\n * một hàm băm ra ngoài chỉ mời app dựng lại phép chọn tone của riêng nó — đúng thứ sinh ra chuyện\n * cùng một người hai màu. Chạy trên code point (không phải byte) để tên có dấu vẫn ổn định giữa\n * server và client.\n */\nfunction hashString(input: string): number {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return Math.abs(hash | 0);\n}\n\n/**\n * Chỉ số tone theo tên — phép chọn màu avatar DUY NHẤT của cả gói.\n *\n * Khoá băm là **tên hiển thị** đã `trim` + hạ chữ thường theo locale `vi-VN` (không phải\n * email/slug: chỗ nào hiển thị avatar cũng chắc chắn có tên, còn email/slug thì không — lấy khoá\n * khác nhau ở từng chỗ là cách nhanh nhất để cùng một người ra hai màu).\n */\nexport function getAvatarToneIndex(name: string): number {\n return hashString(name.trim().toLocaleLowerCase(\"vi-VN\")) % AVATAR_TONES.length;\n}\n\n/** Tone (cả class lẫn công thức hex) ứng với một tên. */\nexport function getAvatarTone(name: string): AvatarTone {\n // `noUncheckedIndexedAccess` bật → phải có nhánh lùi, dù chỉ số luôn nằm trong [0, 8).\n return AVATAR_TONES[getAvatarToneIndex(name)] ?? AVATAR_TONES[0];\n}\n\n/**\n * Tone theo TÊN — cửa cho chỗ dùng chọn tay một màu.\n *\n * Trả `undefined` cho tên lạ, và nơi gọi hiểu đó là \"rơi về màu suy theo tên\". KHÔNG ném: giá trị\n * này đến từ database (`apps.icon_color`), nơi một hàng cũ hoàn toàn có thể mang tên của một tone\n * đã bị xoá — làm sập cả dải ứng dụng vì một chữ trong một hàng là đổi sai lấy tệ hơn. Tên đã\n * ĐỔI thì đi qua `LEGACY_AVATAR_TONE_ALIASES` trước khi tra.\n *\n * ⚠ `LEGACY_AVATAR_TONE_ALIASES` dùng `Record` trần: `\"constructor\"` tra ra một hàm, không phải\n * tên tone — nhưng hàm thì không bằng `tone.name` nào, nên kết quả vẫn là `undefined`.\n */\nexport function getAvatarToneByName(name: string | null | undefined): AvatarTone | undefined {\n if (!name) return undefined;\n const current = LEGACY_AVATAR_TONE_ALIASES[name] ?? name;\n return AVATAR_TONES.find((tone) => tone.name === current);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/avatar-tone.ts"],"sourcesContent":["/**\n * NGUỒN SỰ THẬT DUY NHẤT của màu avatar chữ cái — cả gói chỉ có bảng này.\n *\n * File nội bộ (không nằm trong barrel nào): app không import trực tiếp được, mà đi qua đúng\n * hai cửa tuỳ theo chỗ vẽ:\n * • có Tailwind (DOM) → `getAvatarToneClasses(name)` ở `composite/letter-avatar.tsx`\n * • không có Tailwind (canvas, → `getAvatarToneColors(name, mode)` ở `tokens.ts`\n * email HTML, PDF)\n *\n * Vì sao MỘT bảng chứ không phải hai (một bảng class + một bảng hex \"ảnh chiếu\"): hai bảng viết\n * tay song song thì chỉ cần một lần thêm/xoá/đảo tone là cùng một người ra hai màu khác nhau giữa\n * web và email — mà không có gì báo. Ở đây mỗi tone là MỘT phần tử mang cả chuỗi class lẫn công\n * thức hex tương ứng, nằm sát nhau trên vài dòng liền: thêm tone là thêm cả ba, không quên được.\n * Thứ tự phần tử = chỉ số băm, nên thứ tự cũng chỉ tồn tại ở một chỗ.\n *\n * Vì sao công thức hex là HÀM nhận `palette` chứ không đọc thẳng token: `tokens.ts` là entry\n * `@comitor/ui/tokens`; nếu file này `import { accent } from \"../tokens.js\"` thì thành vòng import\n * (tokens.ts phải import ngược lại bảng này) — vòng có khởi tạo hằng ở giữa là lỗi TDZ lúc chạy.\n * Nhận bảng màu qua tham số thì phụ thuộc chỉ đi MỘT chiều: tokens.ts → file này.\n */\n\n/** Bảy sắc màu phụ mà bảng tone đọc (gold đi riêng qua thang của nó). */\ninterface AvatarToneHues {\n readonly teal: string;\n readonly green: string;\n readonly red: string;\n readonly yellow: string;\n readonly blue: string;\n readonly purple: string;\n readonly pink: string;\n}\n\n/** Đúng 8 tone — chốt ở tầng kiểu: `getAvatarToneIndex` băm theo độ dài này. */\ntype AvatarToneTable<T> = readonly [T, T, T, T, T, T, T, T];\n\n/** Cặp nền + màu chữ (hex đục) của một tone avatar. */\nexport interface AvatarToneColors {\n background: string;\n foreground: string;\n}\n\n/**\n * Các bậc màu mà công thức hex bên dưới đọc vào — `tokens.ts` truyền xuống một bộ cho light,\n * một bộ cho dark. Khai tường minh bằng `string` (không `typeof gold`) vì bản light và bản dark\n * là hai bộ giá trị khác nhau, không cùng một kiểu literal.\n */\nexport interface AvatarTonePalette {\n /** Thang gold — cần bậc 100 · 500 · 600. */\n gold: { readonly 100: string; readonly 500: string; readonly 600: string };\n /** Màu phụ ở vai trò TÔ NỀN (`accent.light` hoặc `accent.dark`). */\n accent: AvatarToneHues;\n /** Cùng màu phụ đó ở vai trò CHỮ trên nền trang (`accentInk.light` hoặc `accentInk.dark`). */\n accentInk: AvatarToneHues;\n /** Nền mà tint hợp thành lên — `--card` của theme. */\n card: string;\n}\n\ninterface AvatarTone {\n /**\n * Tên gọi của tone — để chỗ dùng CHỌN TAY một màu thay vì để hàm băm quyết.\n *\n * ⚠ Tên là một phần của HỢP ĐỒNG: nó đi vào cột database của app (`apps.icon_color` bên\n * `comitor-account`) và vào prop `tone` của `IconAvatar`. Đổi tên một tone là làm mọi hàng đã\n * lưu trỏ vào hư không — và hỏng đó IM LẶNG, vì giá trị lạ chỉ rơi về màu suy theo tên.\n * Thêm tone thì thêm tên mới; đừng đổi tên cũ.\n */\n readonly name: AvatarToneName;\n /** Vai trò DOM: chuỗi class token tĩnh (kèm biến thể `dark:`). */\n readonly classes: string;\n /** Vai trò canvas/email/PDF: đúng những gì `classes` nói, tính ra hex đục ở light. */\n readonly light: (p: AvatarTonePalette) => AvatarToneColors;\n /** Nhánh `dark:` của `classes`. */\n readonly dark: (p: AvatarTonePalette) => AvatarToneColors;\n}\n\n/**\n * Trộn `color` phủ `alpha` lên nền `over`, trả về hex đục.\n *\n * Tái hiện đúng phép mà trình duyệt làm với `bg-teal/15`: Tailwind sinh ra màu CÓ alpha, còn nền\n * phía dưới (`--card`) mới quyết định hex cuối cùng. Nơi không có CSS thì không có gì để hợp\n * thành, nên hex phải mang sẵn kết quả đã trộn.\n */\nfunction tintOver(color: string, over: string, alpha: number): string {\n const channel = (hex: string, i: number) => Number.parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16);\n const mixed = [0, 1, 2].map((i) => Math.round(channel(color, i) * alpha + channel(over, i) * (1 - alpha)));\n return `#${mixed.map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`.toUpperCase();\n}\n\n/**\n * 8 tone deterministic. Phía class TẤT CẢ là token tĩnh — không ghép chuỗi lúc chạy (`bg-${c}-100`)\n * vì Tailwind quét tĩnh, class ghép động sẽ không có mặt trong CSS của app; cũng không dùng hex\n * inline vì như vậy là mất dark mode.\n *\n * ─ VÌ SAO LÀ MÀU PHỤ + CHART CHỨ KHÔNG PHẢI TOKEN SEMANTIC ─\n * Đây là màu ĐỊNH DANH (phân biệt người), không phải màu TRẠNG THÁI — gán `--success` cho một con\n * người là sai nghĩa. Bộ màu phụ + bảng chart là nhóm token DUY NHẤT có đủ 8 sắc khác nhau và có\n * bản riêng cho dark, nên vẫn đổi theo theme và app vẫn ghi đè được.\n *\n * ─ TƯƠNG PHẢN: VÌ SAO BA TONE PHẢI HẠ XUỐNG BẬC INK Ở LIGHT ─\n * Chữ initials tuy `aria-hidden` nhưng người sáng mắt vẫn đọc, nên vẫn phải đạt AA 4,5:1. Bản đầu\n * dùng thẳng `text-teal/green/red` trên nền tint 15% và HỎNG ở light (đo theo WCAG, nền tint đã\n * trộn với `--card` trắng):\n *\n * teal #1F8A8B trên bg-teal/15 = 3,44:1 ✗\n * green #2EA56A trên bg-green/15 = 2,69:1 ✗ (dưới cả ngưỡng 3:1)\n * red #D64545 trên bg-red/15 = 3,57:1 ✗\n *\n * Sửa đúng khuôn `Badge`: dùng bậc `--x-ink` cho light, giữ token đã nâng sáng cho dark. Đo lại\n * trên đúng hex mà `getAvatarToneColors` trả ra (styles.css đã đóng băng bậc ink, xem dưới):\n *\n * teal #1A6668 trên #DDEDEE = 5,54:1 ✓ · dark #2DD4BF trên #193435 = 7,12:1 ✓\n * green #216A48 trên #E0F2E9 = 5,60:1 ✓ · dark #4ADE80 trên #1E362C = 7,44:1 ✓\n * red #9A3537 trên #F9E3E3 = 5,83:1 ✓ · dark #F87171 trên #38252A = 5,18:1 ✓\n *\n * (Bộ số 5,56 / 5,58 / 7,10 / 7,47 / 5,17 ghi ở bản trước là kết quả của chuỗi `color-mix` TRƯỚC\n * khi hex được đóng băng vào token — lệch ở chữ, không lệch ở màu.)\n *\n * Class viết thẳng `text-teal-ink` / `text-green-ink` / `text-red-ink` / `text-yellow-ink` chứ\n * KHÔNG nhắc lại chuỗi `color-mix(...)` — hai cách cho ra đúng cùng một hex (styles.css đã đóng\n * băng kết quả: #1A6668 · #216A48 · #9A3537), và ở `.dark` thì `--teal-ink` vốn trỏ về `--teal`\n * nên không cần biến thể `dark:` nào. Nhờ vậy tên trong class khớp đúng tên trong công thức hex\n * ngay bên dưới (`p.accentInk.teal`) — soi hai vế cạnh nhau là thấy lệch ngay.\n */\n/**\n * Tên của 8 tone, ĐÚNG THỨ TỰ bảng bên dưới — thứ tự ấy cũng là chỉ số băm, nên hai thứ không\n * được trôi khỏi nhau. Đây là bảng màu mà chỗ dùng chọn tay khi không muốn để tên quyết định.\n *\n * Tên lấy theo SẮC, không theo vai trò ngữ nghĩa: đây là màu ĐỊNH DANH, không phải màu trạng\n * thái — xem lý do dài ở docblock của `AVATAR_TONES`.\n */\nexport const AVATAR_TONE_NAMES = [\"gold\", \"teal\", \"green\", \"red\", \"yellow\", \"blue\", \"purple\", \"pink\"] as const;\n\nexport type AvatarToneName = (typeof AVATAR_TONE_NAMES)[number];\n\n/**\n * Tên tone CŨ → tên tone hôm nay (3.10.0). Ba tone cuối từng là `navy` · `ash` · `neutral` — cả ba\n * đều là xám ám xanh ở các độ đậm khác nhau, nên ô icon của ba app khác nhau trông như cùng một\n * màu. Chúng được THAY bằng ba sắc thật, đứng ĐÚNG vị trí cũ trong bảng (chỉ số băm giữ nguyên:\n * người/app nào trước ra `navy` thì nay ra `blue`, không ai nhảy sang tone khác).\n *\n * Bí danh thay vì giữ tên cũ cho màu mới: tên là hợp đồng (cột `apps.icon_color` của Account), và\n * một tên nói dối về màu (\"ash\" mà tím) còn tệ hơn một lần đổi tên có lối lùi. Bí danh đi theo\n * cùng VỊ TRÍ, nên hàng database còn ghi tên cũ ra đúng màu mà phép băm cũng sẽ cho ở chỗ đó.\n *\n * Xuất ở `@comitor/ui` từ 3.12.1 — app ánh xạ tên cũ đọc từ database bằng chính bảng này thay vì\n * chép một bản. ⚠ `Record` trần: khoá đến từ dữ liệu ngoài thì tra qua `Object.hasOwn` —\n * `LEGACY_AVATAR_TONE_ALIASES[\"constructor\"]` ra một hàm, không ra `undefined`.\n */\nexport const LEGACY_AVATAR_TONE_ALIASES: Readonly<Record<string, AvatarToneName>> = {\n navy: \"blue\",\n ash: \"purple\",\n neutral: \"pink\"\n};\n\nexport const AVATAR_TONES: AvatarToneTable<AvatarTone> = [\n {\n name: \"gold\",\n classes: \"bg-gold-100 text-gold-600 dark:bg-gold-500/30 dark:text-gold-100\",\n light: (p) => ({ background: p.gold[100], foreground: p.gold[600] }),\n dark: (p) => ({ background: tintOver(p.gold[500], p.card, 0.3), foreground: p.gold[100] })\n },\n {\n name: \"teal\",\n classes: \"bg-teal/15 text-teal-ink\",\n light: (p) => ({ background: tintOver(p.accent.teal, p.card, 0.15), foreground: p.accentInk.teal }),\n dark: (p) => ({ background: tintOver(p.accent.teal, p.card, 0.15), foreground: p.accentInk.teal })\n },\n {\n name: \"green\",\n classes: \"bg-green/15 text-green-ink\",\n light: (p) => ({ background: tintOver(p.accent.green, p.card, 0.15), foreground: p.accentInk.green }),\n dark: (p) => ({ background: tintOver(p.accent.green, p.card, 0.15), foreground: p.accentInk.green })\n },\n {\n name: \"red\",\n classes: \"bg-red/15 text-red-ink\",\n light: (p) => ({ background: tintOver(p.accent.red, p.card, 0.15), foreground: p.accentInk.red }),\n dark: (p) => ({ background: tintOver(p.accent.red, p.card, 0.15), foreground: p.accentInk.red })\n },\n {\n name: \"yellow\",\n classes: \"bg-yellow/25 text-yellow-ink dark:bg-yellow/20\",\n light: (p) => ({ background: tintOver(p.accent.yellow, p.card, 0.25), foreground: p.accentInk.yellow }),\n dark: (p) => ({ background: tintOver(p.accent.yellow, p.card, 0.2), foreground: p.accentInk.yellow })\n },\n {\n // Ba tone cuối: ba sắc ĐỊNH DANH riêng của bảng này (styles.css, khối \"Màu phụ\") — thay navy ·\n // ash · neutral từ 3.10.0, xem `LEGACY_AVATAR_TONE_ALIASES`. Cùng khuôn teal/green/red: tint /15\n // + bậc ink; ở dark `--x-ink` trỏ về bản dark riêng của fill nên một chuỗi class đủ cả hai theme.\n // Chữ trên tint /15: blue 5,67 · purple 5,72 · pink 5,33:1 (light) · 5,76 · 5,17 · 5,37:1 (dark).\n name: \"blue\",\n classes: \"bg-blue/15 text-blue-ink\",\n light: (p) => ({ background: tintOver(p.accent.blue, p.card, 0.15), foreground: p.accentInk.blue }),\n dark: (p) => ({ background: tintOver(p.accent.blue, p.card, 0.15), foreground: p.accentInk.blue })\n },\n {\n name: \"purple\",\n classes: \"bg-purple/15 text-purple-ink\",\n light: (p) => ({ background: tintOver(p.accent.purple, p.card, 0.15), foreground: p.accentInk.purple }),\n dark: (p) => ({ background: tintOver(p.accent.purple, p.card, 0.15), foreground: p.accentInk.purple })\n },\n {\n name: \"pink\",\n classes: \"bg-pink/15 text-pink-ink\",\n light: (p) => ({ background: tintOver(p.accent.pink, p.card, 0.15), foreground: p.accentInk.pink }),\n dark: (p) => ({ background: tintOver(p.accent.pink, p.card, 0.15), foreground: p.accentInk.pink })\n }\n];\n\n/**\n * Hash chuỗi → số nguyên không âm (biến thể FNV-1a 32 bit).\n *\n * Nội bộ, KHÔNG xuất ra API: công dụng duy nhất của nó là chọn tone avatar ngay dưới đây. Xuất\n * một hàm băm ra ngoài chỉ mời app dựng lại phép chọn tone của riêng nó — đúng thứ sinh ra chuyện\n * cùng một người hai màu. Chạy trên code point (không phải byte) để tên có dấu vẫn ổn định giữa\n * server và client.\n */\nfunction hashString(input: string): number {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return Math.abs(hash | 0);\n}\n\n/**\n * Chỉ số tone theo tên — phép chọn màu avatar DUY NHẤT của cả gói.\n *\n * Khoá băm là **tên hiển thị** đã `trim` + hạ chữ thường theo locale `vi-VN` (không phải\n * email/slug: chỗ nào hiển thị avatar cũng chắc chắn có tên, còn email/slug thì không — lấy khoá\n * khác nhau ở từng chỗ là cách nhanh nhất để cùng một người ra hai màu).\n */\nexport function getAvatarToneIndex(name: string): number {\n return hashString(name.trim().toLocaleLowerCase(\"vi-VN\")) % AVATAR_TONES.length;\n}\n\n/** Tone (cả class lẫn công thức hex) ứng với một tên. */\nexport function getAvatarTone(name: string): AvatarTone {\n // `noUncheckedIndexedAccess` bật → phải có nhánh lùi, dù chỉ số luôn nằm trong [0, 8).\n return AVATAR_TONES[getAvatarToneIndex(name)] ?? AVATAR_TONES[0];\n}\n\n/**\n * Tone theo TÊN — cửa cho chỗ dùng chọn tay một màu.\n *\n * Trả `undefined` cho tên lạ, và nơi gọi hiểu đó là \"rơi về màu suy theo tên\". KHÔNG ném: giá trị\n * này đến từ database (`apps.icon_color`), nơi một hàng cũ hoàn toàn có thể mang tên của một tone\n * đã bị xoá — làm sập cả dải ứng dụng vì một chữ trong một hàng là đổi sai lấy tệ hơn. Tên đã\n * ĐỔI thì đi qua `LEGACY_AVATAR_TONE_ALIASES` trước khi tra.\n *\n * ⚠ `LEGACY_AVATAR_TONE_ALIASES` dùng `Record` trần: `\"constructor\"` tra ra một hàm, không phải\n * tên tone — nhưng hàm thì không bằng `tone.name` nào, nên kết quả vẫn là `undefined`.\n */\nexport function getAvatarToneByName(name: string | null | undefined): AvatarTone | undefined {\n if (!name) return undefined;\n const current = LEGACY_AVATAR_TONE_ALIASES[name] ?? name;\n return AVATAR_TONES.find((tone) => tone.name === current);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
5
5
  "license": "MIT",
6
6
  "type": "module",