@comitor/ui 1.8.3 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ import * as React from 'react';
2
+ import { ComponentProps, ComponentType } from 'react';
3
+ import { AvatarToneName } from '../../lib/avatar-tone.js';
4
+ import { LetterAvatarSize } from './letter-avatar.js';
5
+
6
+ /**
7
+ * Icon của một app/thực thể, đặt trong ô nhận diện tô màu — EM RUỘT của `LetterAvatar`.
8
+ *
9
+ * ── VÌ SAO KHÔNG PHẢI MỘT PROP CỦA `LetterAvatar` ────────────────────────────────────────
10
+ * Hai component chia nhau TOÀN BỘ phần màu (cùng bảng 8 tone, cùng hàm băm, cùng ba nhánh
11
+ * ảnh/nội dung/`sr-only`) và chỉ khác ở thứ nằm giữa ô: chữ cái đầu, hay một icon. Gộp lại thành
12
+ * `<LetterAvatar icon={…}>` thì `name` mang hai nghĩa cùng lúc — vừa là chuỗi để lấy initials,
13
+ * vừa là hạt giống màu — và mọi chỗ gọi phải đọc JSDoc mới biết `initials` có được vẽ hay không.
14
+ * Tách ra thì mỗi bên có đúng một hình dạng, và phần dùng chung nằm ở `lib/avatar-tone.ts`, nơi
15
+ * nó vốn đã ở.
16
+ *
17
+ * ── MÀU: SUY THEO TÊN, TRỪ KHI ĐƯỢC CHỌN TAY ────────────────────────────────────────────
18
+ * Mặc định màu suy từ `seed` (rơi về `name` khi không có) bằng đúng hàm băm của `LetterAvatar`,
19
+ * nên cùng một app luôn ra cùng một màu ở mọi sản phẩm Comitor — và ở cả bản canvas/email/PDF, vì
20
+ * `getAvatarToneColors` của `@comitor/ui/tokens` đọc cùng bảng tone.
21
+ *
22
+ * ⚠ Với app thì `seed` phải là KHOÁ, không phải tên: tên đổi theo ngôn ngữ giao diện. Đo được
23
+ * 4/6 app đổi màu khi chuyển vi ⇄ en — xem JSDoc của `seed`.
24
+ *
25
+ * ⚠ Đó là điều bảng màu ghi cứng KHÔNG làm được, và là lý do component này ra đời: một danh mục
26
+ * app tô bằng năm token chọn tay thì sản phẩm thứ sáu hoặc phải trùng màu với sản phẩm khác, hoặc
27
+ * phải chờ ai đó vào sửa bảng. Ở đây thêm một app là nó tự có màu.
28
+ *
29
+ * `tone` cho phép ghi đè bằng MỘT TÊN trong 8 tone (`AVATAR_TONE_NAMES`) — cả bảng, không phải
30
+ * vài màu. Tên lạ thì rơi về màu suy theo `name`; xem `getAvatarToneByName` để biết vì sao không ném.
31
+ *
32
+ * ── HÌNH: VUÔNG BO GÓC, KHÔNG TRÒN ──────────────────────────────────────────────────────
33
+ * Tròn là hình của NGƯỜI. Ô vuông bo góc là hình của một THỨ (app, workspace, dự án) — cùng quy
34
+ * ước mà `LetterAvatar square` đang dùng cho workspace. `rounded` để mở lại nhánh tròn cho chỗ
35
+ * hiếm cần nó, nhưng mặc định là vuông.
36
+ *
37
+ * ── ⚠ ICON KHÔNG BAO GIỜ NHẬN MÀU CÓ ALPHA ─────────────────────────────────────────────
38
+ * Nền của tone là tint có alpha (`bg-teal/15`), nhưng CHỮ/ICON thì luôn là một bậc màu ĐẶC
39
+ * (`text-teal-ink`). Đó không phải tình cờ: nét icon cắt nhau mà mang alpha thì chỗ giao bị tô hai
40
+ * lần và lộ vệt nối — đo được và ghi ở `docs/hop-dong-mau.md` §7b. Bảng tone vốn đã đúng, và
41
+ * component này không được thêm `opacity-*` hay `/NN` lên `<Icon>`.
42
+ */
43
+ /**
44
+ * Hình dạng icon mà component nhận: một component React lấy `className`.
45
+ *
46
+ * Đủ rộng cho `lucide-react` và cho icon tự vẽ, và KHÔNG nhận `ReactNode` (một element đã dựng
47
+ * sẵn) — nhận element thì chỗ gọi tự đặt cỡ, và cỡ icon sẽ trôi khỏi `size` của ô.
48
+ */
49
+ type IconAvatarIcon = ComponentType<{
50
+ className?: string;
51
+ }>;
52
+ interface IconAvatarProps extends Omit<ComponentProps<"span">, "children"> {
53
+ /** Tên hiển thị — tên gọi TRỢ NĂNG của ô, và là hạt giống màu mặc định khi không có `seed`. */
54
+ name: string;
55
+ /**
56
+ * Hạt giống màu, khi nó KHÁC tên hiển thị. Mặc định dùng `name`.
57
+ *
58
+ * ── ⚠ VÌ SAO PROP NÀY BẮT BUỘC PHẢI CÓ, ĐÃ ĐO ─────────────────────────────────────────
59
+ * `LetterAvatar` băm theo tên và điều đó đúng cho NGƯỜI: tên người không đổi theo ngôn ngữ
60
+ * giao diện. Tên APP thì có — và nó phá đúng lời hứa "cùng một app luôn cùng màu".
61
+ *
62
+ * Đo trên đúng danh mục Comitor hiện có, băm theo tên đã dịch: **4/6 app đổi màu** khi người
63
+ * dùng chuyển vi ⇄ en (`starter` #E7E7E8 → #FCF2D2, `tasks` #FCF2D2 → #F9E3E3, `hr` và `mes`
64
+ * cũng đổi). Một người đổi ngôn ngữ và thấy cả dải ứng dụng đổi màu sẽ không nghĩ đó là tính
65
+ * năng.
66
+ *
67
+ * Vì vậy chỗ nào có một định danh BẤT BIẾN thì truyền nó vào đây: với app là `key`
68
+ * (`"tasks"`, `"crm"`), với workspace là `slug`. `name` vẫn là thứ trình đọc màn hình đọc lên.
69
+ */
70
+ seed?: string;
71
+ /** Icon vẽ ở giữa ô. Thiếu cả `icon` lẫn `src` thì ô vẫn vẽ, chỉ trống — không ném. */
72
+ icon?: IconAvatarIcon;
73
+ /** Ảnh tải lên. Có thì nó THẮNG `icon` — ảnh là thứ người dùng chủ động chọn cho đúng app này. */
74
+ src?: string | null;
75
+ size?: LetterAvatarSize;
76
+ /** Chọn tay một tone trong `AVATAR_TONE_NAMES`. Tên lạ hoặc bỏ trống → suy theo `name`. */
77
+ tone?: AvatarToneName | string | null;
78
+ /** `md` = vuông bo góc (mặc định, hình của một THỨ) · `full` = tròn (hình của NGƯỜI). */
79
+ rounded?: "md" | "full";
80
+ }
81
+ declare function IconAvatar({ name, seed, icon: Icon, src, size, tone, rounded, className, ...props }: IconAvatarProps): React.JSX.Element;
82
+
83
+ export { IconAvatar, type IconAvatarIcon, type IconAvatarProps };
@@ -0,0 +1,57 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { getAvatarTone, getAvatarToneByName } from "../../lib/avatar-tone.js";
3
+ import { cn } from "../../lib/cn.js";
4
+ const iconSizeClasses = {
5
+ xs: "size-3.5",
6
+ sm: "size-4",
7
+ md: "size-4",
8
+ lg: "size-5",
9
+ xl: "size-6"
10
+ };
11
+ const boxSizeClasses = {
12
+ xs: "size-6",
13
+ sm: "size-7",
14
+ md: "size-9",
15
+ lg: "size-11",
16
+ xl: "size-14"
17
+ };
18
+ function IconAvatar({
19
+ name,
20
+ seed,
21
+ icon: Icon,
22
+ src,
23
+ size = "md",
24
+ tone,
25
+ rounded = "md",
26
+ className,
27
+ ...props
28
+ }) {
29
+ const toneClasses = (getAvatarToneByName(tone) ?? getAvatarTone(seed ?? name)).classes;
30
+ return /* @__PURE__ */ jsxs(
31
+ "span",
32
+ {
33
+ "data-slot": "icon-avatar",
34
+ title: name,
35
+ className: cn(
36
+ "relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden",
37
+ rounded === "full" ? "rounded-full" : "rounded-md",
38
+ boxSizeClasses[size],
39
+ !src && toneClasses,
40
+ className
41
+ ),
42
+ ...props,
43
+ children: [
44
+ src ? (
45
+ // Cùng lý do với `LetterAvatar`: gói dùng chung không phụ thuộc `next/*`.
46
+ // biome-ignore lint/performance/noImgElement: package dùng chung không phụ thuộc next/*
47
+ /* @__PURE__ */ jsx("img", { src, alt: "", className: "size-full object-cover", loading: "lazy", decoding: "async" })
48
+ ) : Icon ? /* @__PURE__ */ jsx(Icon, { className: iconSizeClasses[size] }) : null,
49
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: name })
50
+ ]
51
+ }
52
+ );
53
+ }
54
+ export {
55
+ IconAvatar
56
+ };
57
+ //# sourceMappingURL=icon-avatar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/icon-avatar.tsx"],"sourcesContent":["import type { ComponentProps, ComponentType } from \"react\";\nimport { type AvatarToneName, getAvatarTone, getAvatarToneByName } from \"../../lib/avatar-tone.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport type { LetterAvatarSize } from \"./letter-avatar.js\";\n\n/**\n * Icon của một app/thực thể, đặt trong ô nhận diện tô màu — EM RUỘT của `LetterAvatar`.\n *\n * ── VÌ SAO KHÔNG PHẢI MỘT PROP CỦA `LetterAvatar` ────────────────────────────────────────\n * Hai component chia nhau TOÀN BỘ phần màu (cùng bảng 8 tone, cùng hàm băm, cùng ba nhánh\n * ảnh/nội dung/`sr-only`) và chỉ khác ở thứ nằm giữa ô: chữ cái đầu, hay một icon. Gộp lại thành\n * `<LetterAvatar icon={…}>` thì `name` mang hai nghĩa cùng lúc — vừa là chuỗi để lấy initials,\n * vừa là hạt giống màu — và mọi chỗ gọi phải đọc JSDoc mới biết `initials` có được vẽ hay không.\n * Tách ra thì mỗi bên có đúng một hình dạng, và phần dùng chung nằm ở `lib/avatar-tone.ts`, nơi\n * nó vốn đã ở.\n *\n * ── MÀU: SUY THEO TÊN, TRỪ KHI ĐƯỢC CHỌN TAY ────────────────────────────────────────────\n * Mặc định màu suy từ `seed` (rơi về `name` khi không có) bằng đúng hàm băm của `LetterAvatar`,\n * nên cùng một app luôn ra cùng một màu ở mọi sản phẩm Comitor — và ở cả bản canvas/email/PDF, vì\n * `getAvatarToneColors` của `@comitor/ui/tokens` đọc cùng bảng tone.\n *\n * ⚠ Với app thì `seed` phải là KHOÁ, không phải tên: tên đổi theo ngôn ngữ giao diện. Đo được\n * 4/6 app đổi màu khi chuyển vi ⇄ en — xem JSDoc của `seed`.\n *\n * ⚠ Đó là điều bảng màu ghi cứng KHÔNG làm được, và là lý do component này ra đời: một danh mục\n * app tô bằng năm token chọn tay thì sản phẩm thứ sáu hoặc phải trùng màu với sản phẩm khác, hoặc\n * phải chờ ai đó vào sửa bảng. Ở đây thêm một app là nó tự có màu.\n *\n * `tone` cho phép ghi đè bằng MỘT TÊN trong 8 tone (`AVATAR_TONE_NAMES`) — cả bảng, không phải\n * vài màu. Tên lạ thì rơi về màu suy theo `name`; xem `getAvatarToneByName` để biết vì sao không ném.\n *\n * ── HÌNH: VUÔNG BO GÓC, KHÔNG TRÒN ──────────────────────────────────────────────────────\n * Tròn là hình của NGƯỜI. Ô vuông bo góc là hình của một THỨ (app, workspace, dự án) — cùng quy\n * ước mà `LetterAvatar square` đang dùng cho workspace. `rounded` để mở lại nhánh tròn cho chỗ\n * hiếm cần nó, nhưng mặc định là vuông.\n *\n * ── ⚠ ICON KHÔNG BAO GIỜ NHẬN MÀU CÓ ALPHA ─────────────────────────────────────────────\n * Nền của tone là tint có alpha (`bg-teal/15`), nhưng CHỮ/ICON thì luôn là một bậc màu ĐẶC\n * (`text-teal-ink`). Đó không phải tình cờ: nét icon cắt nhau mà mang alpha thì chỗ giao bị tô hai\n * lần và lộ vệt nối — đo được và ghi ở `docs/hop-dong-mau.md` §7b. Bảng tone vốn đã đúng, và\n * component này không được thêm `opacity-*` hay `/NN` lên `<Icon>`.\n */\n\n/**\n * Hình dạng icon mà component nhận: một component React lấy `className`.\n *\n * Đủ rộng cho `lucide-react` và cho icon tự vẽ, và KHÔNG nhận `ReactNode` (một element đã dựng\n * sẵn) — nhận element thì chỗ gọi tự đặt cỡ, và cỡ icon sẽ trôi khỏi `size` của ô.\n */\nexport type IconAvatarIcon = ComponentType<{ className?: string }>;\n\n/** Cỡ icon bên trong, theo cỡ ô. Ô `md` (36px) + icon 16px là tỉ lệ đang dùng ở dải ứng dụng. */\nconst iconSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-3.5\",\n sm: \"size-4\",\n md: \"size-4\",\n lg: \"size-5\",\n xl: \"size-6\"\n};\n\nconst boxSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6\",\n sm: \"size-7\",\n md: \"size-9\",\n lg: \"size-11\",\n xl: \"size-14\"\n};\n\nexport interface IconAvatarProps extends Omit<ComponentProps<\"span\">, \"children\"> {\n /** Tên hiển thị — tên gọi TRỢ NĂNG của ô, và là hạt giống màu mặc định khi không có `seed`. */\n name: string;\n /**\n * Hạt giống màu, khi nó KHÁC tên hiển thị. Mặc định dùng `name`.\n *\n * ── ⚠ VÌ SAO PROP NÀY BẮT BUỘC PHẢI CÓ, ĐÃ ĐO ─────────────────────────────────────────\n * `LetterAvatar` băm theo tên và điều đó đúng cho NGƯỜI: tên người không đổi theo ngôn ngữ\n * giao diện. Tên APP thì có — và nó phá đúng lời hứa \"cùng một app luôn cùng màu\".\n *\n * Đo trên đúng danh mục Comitor hiện có, băm theo tên đã dịch: **4/6 app đổi màu** khi người\n * dùng chuyển vi ⇄ en (`starter` #E7E7E8 → #FCF2D2, `tasks` #FCF2D2 → #F9E3E3, `hr` và `mes`\n * cũng đổi). Một người đổi ngôn ngữ và thấy cả dải ứng dụng đổi màu sẽ không nghĩ đó là tính\n * năng.\n *\n * Vì vậy chỗ nào có một định danh BẤT BIẾN thì truyền nó vào đây: với app là `key`\n * (`\"tasks\"`, `\"crm\"`), với workspace là `slug`. `name` vẫn là thứ trình đọc màn hình đọc lên.\n */\n seed?: string;\n /** Icon vẽ ở giữa ô. Thiếu cả `icon` lẫn `src` thì ô vẫn vẽ, chỉ trống — không ném. */\n icon?: IconAvatarIcon;\n /** Ảnh tải lên. Có thì nó THẮNG `icon` — ảnh là thứ người dùng chủ động chọn cho đúng app này. */\n src?: string | null;\n size?: LetterAvatarSize;\n /** Chọn tay một tone trong `AVATAR_TONE_NAMES`. Tên lạ hoặc bỏ trống → suy theo `name`. */\n tone?: AvatarToneName | string | null;\n /** `md` = vuông bo góc (mặc định, hình của một THỨ) · `full` = tròn (hình của NGƯỜI). */\n rounded?: \"md\" | \"full\";\n}\n\nexport function IconAvatar({\n name,\n seed,\n icon: Icon,\n src,\n size = \"md\",\n tone,\n rounded = \"md\",\n className,\n ...props\n}: IconAvatarProps) {\n const toneClasses = (getAvatarToneByName(tone) ?? getAvatarTone(seed ?? name)).classes;\n\n return (\n <span\n data-slot=\"icon-avatar\"\n title={name}\n className={cn(\n \"relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden\",\n rounded === \"full\" ? \"rounded-full\" : \"rounded-md\",\n boxSizeClasses[size],\n !src && toneClasses,\n className\n )}\n {...props}\n >\n {src ? (\n // Cùng lý do với `LetterAvatar`: gói dùng chung không phụ thuộc `next/*`.\n // biome-ignore lint/performance/noImgElement: package dùng chung không phụ thuộc next/*\n <img src={src} alt=\"\" className=\"size-full object-cover\" loading=\"lazy\" decoding=\"async\" />\n ) : Icon ? (\n <Icon className={iconSizeClasses[size]} />\n ) : null}\n {/* Tên cho trình đọc màn hình ở MỌI nhánh — icon là hình trang trí, ô màu không có chữ nào,\n nên thiếu dòng này thì đây là một ô câm. Cùng khuôn với `LetterAvatar`. */}\n <span className=\"sr-only\">{name}</span>\n </span>\n );\n}\n"],"mappings":"AAgHI,SAeI,KAfJ;AA/GJ,SAA8B,eAAe,2BAA2B;AACxE,SAAS,UAAU;AAkDnB,MAAM,kBAAoD;AAAA,EACxD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,iBAAmD;AAAA,EACvD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAgCO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,eAAe,oBAAoB,IAAI,KAAK,cAAc,QAAQ,IAAI,GAAG;AAE/E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,OAAO;AAAA,MACP,WAAW;AAAA,QACT;AAAA,QACA,YAAY,SAAS,iBAAiB;AAAA,QACtC,eAAe,IAAI;AAAA,QACnB,CAAC,OAAO;AAAA,QACR;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA;AAAA;AAAA,UAGC,oBAAC,SAAI,KAAU,KAAI,IAAG,WAAU,0BAAyB,SAAQ,QAAO,UAAS,SAAQ;AAAA,YACvF,OACF,oBAAC,QAAK,WAAW,gBAAgB,IAAI,GAAG,IACtC;AAAA,QAGJ,oBAAC,UAAK,WAAU,WAAW,gBAAK;AAAA;AAAA;AAAA,EAClC;AAEJ;","names":[]}
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { FilterChipItem, FilterChips, FilterChipsProps } from './components/comp
13
13
  export { FormField, FormFieldColSpan, FormFieldControlProps, FormFieldProps } from './components/composite/form-field.js';
14
14
  export { SELECT_EMPTY_VALUE, fromSelectValue, toSelectValue } from './components/composite/form-field-constants.js';
15
15
  export { FormSection, FormSectionAccent, FormSectionProps } from './components/composite/form-section.js';
16
+ export { IconAvatar, IconAvatarIcon, IconAvatarProps } from './components/composite/icon-avatar.js';
16
17
  export { InlineEdit, InlineEditProps } from './components/composite/inline-edit.js';
17
18
  export { KeyboardHint, KeyboardHintProps, useIsMac } from './components/composite/keyboard-hint.js';
18
19
  export { LetterAvatar, LetterAvatarProps, LetterAvatarSize, getAvatarToneClasses } from './components/composite/letter-avatar.js';
@@ -79,9 +80,9 @@ export { Textarea, TextareaProps } from './components/ui/textarea.js';
79
80
  export { Toggle, ToggleProps, ToggleVariantProps, toggleVariants } from './components/ui/toggle.js';
80
81
  export { ToggleGroup, ToggleGroupItem, ToggleGroupItemProps, ToggleGroupProps, ToggleGroupVariantProps } from './components/ui/toggle-group.js';
81
82
  export { Tooltip, TooltipContent, TooltipContentProps, TooltipProps, TooltipProvider, TooltipProviderProps, TooltipTrigger, TooltipTriggerProps } from './components/ui/tooltip.js';
83
+ export { AVATAR_TONE_NAMES, AvatarToneColors, AvatarToneName, getAvatarToneIndex } from './lib/avatar-tone.js';
82
84
  export { cn } from './lib/cn.js';
83
85
  export { ThemeMode, accent, accentForeground, accentInk, ash, brandNeutral, chart, getAvatarToneColors, gold, highContrast, layout, semantic, status, typography } from './tokens.js';
84
- export { AvatarToneColors, getAvatarToneIndex } from './lib/avatar-tone.js';
85
86
  export { DateRange } from 'react-day-picker';
86
87
  import 'react';
87
88
  import 'date-fns';
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * from "./components/composite/filter-chips.js";
13
13
  export * from "./components/composite/form-field.js";
14
14
  export * from "./components/composite/form-field-constants.js";
15
15
  export * from "./components/composite/form-section.js";
16
+ import { IconAvatar } from "./components/composite/icon-avatar.js";
16
17
  export * from "./components/composite/inline-edit.js";
17
18
  export * from "./components/composite/keyboard-hint.js";
18
19
  import { getAvatarToneClasses, LetterAvatar } from "./components/composite/letter-avatar.js";
@@ -85,9 +86,12 @@ export * from "./components/ui/textarea.js";
85
86
  export * from "./components/ui/toggle.js";
86
87
  export * from "./components/ui/toggle-group.js";
87
88
  export * from "./components/ui/tooltip.js";
89
+ import { AVATAR_TONE_NAMES } from "./lib/avatar-tone.js";
88
90
  export * from "./lib/cn.js";
89
91
  export * from "./tokens.js";
90
92
  export {
93
+ AVATAR_TONE_NAMES,
94
+ IconAvatar,
91
95
  LetterAvatar,
92
96
  getAvatarToneClasses,
93
97
  getVietnameseInitials,
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 * ⚠ Sáu file `*-constants` bên dưới KHÔNG phải chỗ để gom code cho gọn — chúng là RANH GIỚI\n * SERVER/CLIENT. Component chủ (`command-palette.tsx`, `data-table.tsx`, `date-picker.tsx`,\n * `form-field.tsx`, `search-filter-bar.tsx`, `table-pagination.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/inline-edit.js\";\nexport * from \"./components/composite/keyboard-hint.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\";\n// ─── Tier 1 — primitive (shadcn/ui \"new-york\", đã custom theo brand Comitor) ───\nexport * from \"./components/ui/accordion.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/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/collapsible.js\";\nexport * from \"./components/ui/command.js\";\nexport * from \"./components/ui/context-menu.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/kbd.js\";\nexport * from \"./components/ui/label.js\";\nexport * from \"./components/ui/menubar.js\";\nexport * from \"./components/ui/navigation-menu.js\";\nexport * from \"./components/ui/pagination.js\";\nexport * from \"./components/ui/popover.js\";\nexport * from \"./components/ui/progress.js\";\nexport * from \"./components/ui/radio-group.js\";\nexport * from \"./components/ui/resizable.js\";\nexport * from \"./components/ui/scroll-area.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/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/textarea.js\";\nexport * from \"./components/ui/toggle.js\";\nexport * from \"./components/ui/toggle-group.js\";\nexport * from \"./components/ui/tooltip.js\";\n\n// ─── lib + token ───\nexport * from \"./lib/cn.js\";\nexport * from \"./tokens.js\";\n"],"mappings":"AAqCA,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;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;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;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,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 * ⚠ Sáu file `*-constants` bên dưới KHÔNG phải chỗ để gom code cho gọn — chúng là RANH GIỚI\n * SERVER/CLIENT. Component chủ (`command-palette.tsx`, `data-table.tsx`, `date-picker.tsx`,\n * `form-field.tsx`, `search-filter-bar.tsx`, `table-pagination.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 { IconAvatar, type IconAvatarIcon, type IconAvatarProps } from \"./components/composite/icon-avatar.js\";\nexport * from \"./components/composite/inline-edit.js\";\nexport * from \"./components/composite/keyboard-hint.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\";\n// ─── Tier 1 — primitive (shadcn/ui \"new-york\", đã custom theo brand Comitor) ───\nexport * from \"./components/ui/accordion.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/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/collapsible.js\";\nexport * from \"./components/ui/command.js\";\nexport * from \"./components/ui/context-menu.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/kbd.js\";\nexport * from \"./components/ui/label.js\";\nexport * from \"./components/ui/menubar.js\";\nexport * from \"./components/ui/navigation-menu.js\";\nexport * from \"./components/ui/pagination.js\";\nexport * from \"./components/ui/popover.js\";\nexport * from \"./components/ui/progress.js\";\nexport * from \"./components/ui/radio-group.js\";\nexport * from \"./components/ui/resizable.js\";\nexport * from \"./components/ui/scroll-area.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/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/textarea.js\";\nexport * from \"./components/ui/toggle.js\";\nexport * from \"./components/ui/toggle-group.js\";\nexport * from \"./components/ui/tooltip.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":"AAqCA,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;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;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;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAUd,SAAS,yBAA8C;AAGvD,cAAc;AACd,cAAc;","names":[]}
@@ -69,6 +69,15 @@ interface AvatarTonePalette {
69
69
  card: string;
70
70
  }
71
71
  interface AvatarTone {
72
+ /**
73
+ * 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.
74
+ *
75
+ * ⚠ 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
76
+ * `comitor-account`) và vào prop `tone` của `IconAvatar`. Đổi tên một tone là làm mọi hàng đã
77
+ * 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.
78
+ * Thêm tone thì thêm tên mới; đừng đổi tên cũ.
79
+ */
80
+ readonly name: AvatarToneName;
72
81
  /** Vai trò DOM: chuỗi class token tĩnh (kèm biến thể `dark:`). */
73
82
  readonly classes: string;
74
83
  /** Vai trò canvas/email/PDF: đúng những gì `classes` nói, tính ra hex đục ở light. */
@@ -111,6 +120,15 @@ interface AvatarTone {
111
120
  * 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
112
121
  * ngay bên dưới (`p.accentInk.teal`) — soi hai vế cạnh nhau là thấy lệch ngay.
113
122
  */
123
+ /**
124
+ * 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
125
+ * đượ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.
126
+ *
127
+ * 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
128
+ * thái — xem lý do dài ở docblock của `AVATAR_TONES`.
129
+ */
130
+ declare const AVATAR_TONE_NAMES: readonly ["gold", "teal", "green", "red", "yellow", "navy", "ash", "neutral"];
131
+ type AvatarToneName = (typeof AVATAR_TONE_NAMES)[number];
114
132
  declare const AVATAR_TONES: AvatarToneTable<AvatarTone>;
115
133
  /**
116
134
  * Chỉ số tone theo tên — phép chọn màu avatar DUY NHẤT của cả gói.
@@ -122,5 +140,13 @@ declare const AVATAR_TONES: AvatarToneTable<AvatarTone>;
122
140
  declare function getAvatarToneIndex(name: string): number;
123
141
  /** Tone (cả class lẫn công thức hex) ứng với một tên. */
124
142
  declare function getAvatarTone(name: string): AvatarTone;
143
+ /**
144
+ * Tone theo TÊN — cửa cho chỗ dùng chọn tay một màu.
145
+ *
146
+ * 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ị
147
+ * 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
148
+ * đã 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.
149
+ */
150
+ declare function getAvatarToneByName(name: string | null | undefined): AvatarTone | undefined;
125
151
 
126
- export { AVATAR_TONES, type AvatarToneColors, type AvatarTonePalette, getAvatarTone, getAvatarToneIndex };
152
+ export { AVATAR_TONES, AVATAR_TONE_NAMES, type AvatarToneColors, type AvatarToneName, type AvatarTonePalette, getAvatarTone, getAvatarToneByName, getAvatarToneIndex };
@@ -3,44 +3,53 @@ function tintOver(color, over, alpha) {
3
3
  const mixed = [0, 1, 2].map((i) => Math.round(channel(color, i) * alpha + channel(over, i) * (1 - alpha)));
4
4
  return `#${mixed.map((v) => v.toString(16).padStart(2, "0")).join("")}`.toUpperCase();
5
5
  }
6
+ const AVATAR_TONE_NAMES = ["gold", "teal", "green", "red", "yellow", "navy", "ash", "neutral"];
6
7
  const AVATAR_TONES = [
7
8
  {
9
+ name: "gold",
8
10
  classes: "bg-gold-100 text-gold-600 dark:bg-gold-500/30 dark:text-gold-100",
9
11
  light: (p) => ({ background: p.gold[100], foreground: p.gold[600] }),
10
12
  dark: (p) => ({ background: tintOver(p.gold[500], p.card, 0.3), foreground: p.gold[100] })
11
13
  },
12
14
  {
15
+ name: "teal",
13
16
  classes: "bg-teal/15 text-teal-ink",
14
17
  light: (p) => ({ background: tintOver(p.accent.teal, p.card, 0.15), foreground: p.accentInk.teal }),
15
18
  dark: (p) => ({ background: tintOver(p.accent.teal, p.card, 0.15), foreground: p.accentInk.teal })
16
19
  },
17
20
  {
21
+ name: "green",
18
22
  classes: "bg-green/15 text-green-ink",
19
23
  light: (p) => ({ background: tintOver(p.accent.green, p.card, 0.15), foreground: p.accentInk.green }),
20
24
  dark: (p) => ({ background: tintOver(p.accent.green, p.card, 0.15), foreground: p.accentInk.green })
21
25
  },
22
26
  {
27
+ name: "red",
23
28
  classes: "bg-red/15 text-red-ink",
24
29
  light: (p) => ({ background: tintOver(p.accent.red, p.card, 0.15), foreground: p.accentInk.red }),
25
30
  dark: (p) => ({ background: tintOver(p.accent.red, p.card, 0.15), foreground: p.accentInk.red })
26
31
  },
27
32
  {
33
+ name: "yellow",
28
34
  classes: "bg-yellow/25 text-yellow-ink dark:bg-yellow/20",
29
35
  light: (p) => ({ background: tintOver(p.accent.yellow, p.card, 0.25), foreground: p.accentInk.yellow }),
30
36
  dark: (p) => ({ background: tintOver(p.accent.yellow, p.card, 0.2), foreground: p.accentInk.yellow })
31
37
  },
32
38
  {
33
39
  // chart-3 = navy ở light, = navy-ink (bậc sáng) ở dark — nền tint cũng đi theo đúng màu đó.
40
+ name: "navy",
34
41
  classes: "bg-chart-3/15 text-chart-3",
35
42
  light: (p) => ({ background: tintOver(p.accent.navy, p.card, 0.15), foreground: p.accentInk.navy }),
36
43
  dark: (p) => ({ background: tintOver(p.accentInk.navy, p.card, 0.15), foreground: p.accentInk.navy })
37
44
  },
38
45
  {
46
+ name: "ash",
39
47
  classes: "bg-ash-100 text-ash-700 dark:bg-ash-500/25 dark:text-ash-100",
40
48
  light: (p) => ({ background: p.ash[100], foreground: p.ash[700] }),
41
49
  dark: (p) => ({ background: tintOver(p.ash[500], p.card, 0.25), foreground: p.ash[100] })
42
50
  },
43
51
  {
52
+ name: "neutral",
44
53
  classes: "bg-ink/10 text-ink-soft dark:bg-paper/15 dark:text-paper",
45
54
  light: (p) => ({ background: tintOver(p.brandNeutral.ink, p.card, 0.1), foreground: p.brandNeutral.inkSoft }),
46
55
  dark: (p) => ({ background: tintOver(p.brandNeutral.paper, p.card, 0.15), foreground: p.brandNeutral.paper })
@@ -60,9 +69,15 @@ function getAvatarToneIndex(name) {
60
69
  function getAvatarTone(name) {
61
70
  return AVATAR_TONES[getAvatarToneIndex(name)] ?? AVATAR_TONES[0];
62
71
  }
72
+ function getAvatarToneByName(name) {
73
+ if (!name) return void 0;
74
+ return AVATAR_TONES.find((tone) => tone.name === name);
75
+ }
63
76
  export {
64
77
  AVATAR_TONES,
78
+ AVATAR_TONE_NAMES,
65
79
  getAvatarTone,
80
+ getAvatarToneByName,
66
81
  getAvatarToneIndex
67
82
  };
68
83
  //# sourceMappingURL=avatar-tone.js.map
@@ -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/** Đú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 /** Thang xám ash — cần bậc 100 · 500 · 700. */\n ash: { readonly 100: string; readonly 500: string; readonly 700: string };\n /** Trung tính brand — mực và giấy. */\n brandNeutral: { readonly ink: string; readonly inkSoft: string; readonly paper: string };\n /** Màu phụ ở vai trò TÔ NỀN (`accent.light` hoặc `accent.dark`). */\n accent: {\n readonly teal: string;\n readonly green: string;\n readonly red: string;\n readonly yellow: string;\n readonly navy: string;\n };\n /** Cùng màu phụ đó ở vai trò CHỮ trên nền trang (`accentInk.light` hoặc `accentInk.dark`). */\n accentInk: {\n readonly teal: string;\n readonly green: string;\n readonly red: string;\n readonly yellow: string;\n readonly navy: string;\n };\n /** Nền mà tint hợp thành lên — `--card` của theme. */\n card: string;\n}\n\ninterface AvatarTone {\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 */\nexport const AVATAR_TONES: AvatarToneTable<AvatarTone> = [\n {\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 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 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 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 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 // chart-3 = navy ở light, = navy-ink (bậc sáng) ở dark — nền tint cũng đi theo đúng màu đó.\n classes: \"bg-chart-3/15 text-chart-3\",\n light: (p) => ({ background: tintOver(p.accent.navy, p.card, 0.15), foreground: p.accentInk.navy }),\n dark: (p) => ({ background: tintOver(p.accentInk.navy, p.card, 0.15), foreground: p.accentInk.navy })\n },\n {\n classes: \"bg-ash-100 text-ash-700 dark:bg-ash-500/25 dark:text-ash-100\",\n light: (p) => ({ background: p.ash[100], foreground: p.ash[700] }),\n dark: (p) => ({ background: tintOver(p.ash[500], p.card, 0.25), foreground: p.ash[100] })\n },\n {\n classes: \"bg-ink/10 text-ink-soft dark:bg-paper/15 dark:text-paper\",\n light: (p) => ({ background: tintOver(p.brandNeutral.ink, p.card, 0.1), foreground: p.brandNeutral.inkSoft }),\n dark: (p) => ({ background: tintOver(p.brandNeutral.paper, p.card, 0.15), foreground: p.brandNeutral.paper })\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"],"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/** Đú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 /** Thang xám ash — cần bậc 100 · 500 · 700. */\n ash: { readonly 100: string; readonly 500: string; readonly 700: string };\n /** Trung tính brand — mực và giấy. */\n brandNeutral: { readonly ink: string; readonly inkSoft: string; readonly paper: string };\n /** Màu phụ ở vai trò TÔ NỀN (`accent.light` hoặc `accent.dark`). */\n accent: {\n readonly teal: string;\n readonly green: string;\n readonly red: string;\n readonly yellow: string;\n readonly navy: string;\n };\n /** Cùng màu phụ đó ở vai trò CHỮ trên nền trang (`accentInk.light` hoặc `accentInk.dark`). */\n accentInk: {\n readonly teal: string;\n readonly green: string;\n readonly red: string;\n readonly yellow: string;\n readonly navy: string;\n };\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\", \"navy\", \"ash\", \"neutral\"] as const;\n\nexport type AvatarToneName = (typeof AVATAR_TONE_NAMES)[number];\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 // chart-3 = navy ở light, = navy-ink (bậc sáng) ở dark — nền tint cũng đi theo đúng màu đó.\n name: \"navy\",\n classes: \"bg-chart-3/15 text-chart-3\",\n light: (p) => ({ background: tintOver(p.accent.navy, p.card, 0.15), foreground: p.accentInk.navy }),\n dark: (p) => ({ background: tintOver(p.accentInk.navy, p.card, 0.15), foreground: p.accentInk.navy })\n },\n {\n name: \"ash\",\n classes: \"bg-ash-100 text-ash-700 dark:bg-ash-500/25 dark:text-ash-100\",\n light: (p) => ({ background: p.ash[100], foreground: p.ash[700] }),\n dark: (p) => ({ background: tintOver(p.ash[500], p.card, 0.25), foreground: p.ash[100] })\n },\n {\n name: \"neutral\",\n classes: \"bg-ink/10 text-ink-soft dark:bg-paper/15 dark:text-paper\",\n light: (p) => ({ background: tintOver(p.brandNeutral.ink, p.card, 0.1), foreground: p.brandNeutral.inkSoft }),\n dark: (p) => ({ background: tintOver(p.brandNeutral.paper, p.card, 0.15), foreground: p.brandNeutral.paper })\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.\n */\nexport function getAvatarToneByName(name: string | null | undefined): AvatarTone | undefined {\n if (!name) return undefined;\n return AVATAR_TONES.find((tone) => tone.name === name);\n}\n"],"mappings":"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","names":[]}
@@ -64,12 +64,12 @@ function Sidebar({
64
64
  onClick: toggleSidebar,
65
65
  "aria-label": sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar,
66
66
  className: cn(
67
- "flex items-center gap-2 rounded-md text-sidebar-foreground/60",
67
+ "group flex items-center gap-2 rounded-md text-sidebar-foreground/60",
68
68
  "transition-colors duration-150 ease-out hover:bg-sidebar-accent hover:text-sidebar-foreground",
69
69
  sidebarCollapsed ? "size-9 justify-center" : "h-8 w-full px-2 text-xs"
70
70
  ),
71
- children: sidebarCollapsed ? /* @__PURE__ */ jsx(PanelLeftOpen, { className: "size-4" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
72
- /* @__PURE__ */ jsx(PanelLeftClose, { className: "size-4" }),
71
+ children: sidebarCollapsed ? /* @__PURE__ */ jsx(PanelLeftOpen, { className: "size-4 text-muted-foreground group-hover:text-sidebar-foreground" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ /* @__PURE__ */ jsx(PanelLeftClose, { className: "size-4 text-muted-foreground group-hover:text-sidebar-foreground" }),
73
73
  /* @__PURE__ */ jsx("span", { className: "truncate", children: labels.collapseSidebarShort })
74
74
  ] })
75
75
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/sidebar.tsx"],"sourcesContent":["\"use client\";\n\nimport { PanelLeftClose, PanelLeftOpen } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface SidebarProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher (vd nút \"Tạo mới\", ô tìm nhanh). */\n header?: ReactNode;\n /** Slot đáy sidebar (dung lượng gói, \"Mời thành viên\", phiên bản app…). */\n footer?: ReactNode;\n /** Ẩn WorkspaceSwitcher ở đỉnh — dùng khi app đặt bộ chuyển workspace ở header. */\n showWorkspaceSwitcher?: boolean;\n /** Ẩn nút thu gọn ở đáy (khi header đã có nút này). */\n showCollapseButton?: boolean;\n}\n\n/**\n * Sidebar desktop: 256px (`--sidebar-width`) / 56px khi thu gọn (`--sidebar-width-icon`).\n * Đỉnh: `WorkspaceSwitcher` (trục 1). Giữa: menu của app hiện tại. Đáy: slot footer.\n *\n * Ẩn hẳn dưới `md` — màn hình hẹp dùng `MobileMenu`.\n *\n * KHÔNG dùng `ui/sidebar.tsx` của shadcn (spec §7.6): primitive đó quản state bằng\n * cookie + provider riêng, sẽ thành nguồn sự thật thứ hai bên cạnh `ShellProvider`.\n */\nexport function Sidebar({\n className,\n header,\n footer,\n showWorkspaceSwitcher = true,\n showCollapseButton = true\n}: SidebarProps) {\n const { nav, currentApp, workspaces, sidebarCollapsed, toggleSidebar, labels } = useShell();\n\n // Đối chiếu khớp chính xác trên TOÀN BỘ mục của app, không chỉ mục cùng nhóm:\n // \"/tasks\" nằm nhóm A và \"/tasks/inbox\" nhóm B thì vẫn phải sáng đúng một mục.\n const allItems = flattenNavItems(nav);\n\n return (\n <aside\n data-collapsed={sidebarCollapsed || undefined}\n style={appAccentStyle(currentApp)}\n className={cn(\n \"hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex\",\n \"transition-[width] duration-200 ease-out\",\n sidebarCollapsed ? \"w-sidebar-icon\" : \"w-sidebar\",\n className\n )}\n >\n {/* Không có workspace nào (app đơn tổ chức) thì bỏ hẳn dải đỉnh, không để lại ô trống. */}\n {showWorkspaceSwitcher && workspaces.length > 0 ? (\n <div\n className={cn(\n \"flex h-header shrink-0 items-center border-b border-sidebar-border\",\n sidebarCollapsed ? \"justify-center px-1\" : \"px-2\"\n )}\n >\n <WorkspaceSwitcher collapsed={sidebarCollapsed} />\n </div>\n ) : null}\n\n {header ? <div className={cn(\"shrink-0\", sidebarCollapsed ? \"px-1.5 pt-2\" : \"px-2 pt-2\")}>{header}</div> : null}\n\n <nav\n aria-label={labels.navigationLabel}\n className={cn(\"flex-1 overflow-y-auto scrollbar-thin py-3\", sidebarCollapsed ? \"px-1.5\" : \"px-2\")}\n >\n <div className={cn(\"flex flex-col\", sidebarCollapsed ? \"gap-2\" : \"gap-4\")}>\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} collapsed={sidebarCollapsed} siblings={allItems} />\n ))}\n </div>\n </nav>\n\n {footer || showCollapseButton ? (\n <div\n className={cn(\n \"shrink-0 border-t border-sidebar-border\",\n sidebarCollapsed ? \"flex flex-col items-center gap-1 p-1.5\" : \"p-2\"\n )}\n >\n {!sidebarCollapsed && footer ? <div className=\"mb-1\">{footer}</div> : null}\n\n {showCollapseButton ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n className={cn(\n \"flex items-center gap-2 rounded-md text-sidebar-foreground/60\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent hover:text-sidebar-foreground\",\n sidebarCollapsed ? \"size-9 justify-center\" : \"h-8 w-full px-2 text-xs\"\n )}\n >\n {sidebarCollapsed ? (\n <PanelLeftOpen className=\"size-4\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4\" />\n {/* Chữ NHÌN THẤY dùng bản ngắn; `aria-label` và tooltip ở trên vẫn giữ bản\n dài. Xem hợp đồng của `collapseSidebarShort` trong types.ts. */}\n <span className=\"truncate\">{labels.collapseSidebarShort}</span>\n </>\n )}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"right\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · Ctrl/⌘ + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n ) : null}\n </aside>\n );\n}\n"],"mappings":";AAgEU,SA0CU,UA1CV,KA0CU,YA1CV;AA9DV,SAAS,gBAAgB,qBAAqB;AAE9C,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAuB3B,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,wBAAwB;AAAA,EACxB,qBAAqB;AACvB,GAAiB;AACf,QAAM,EAAE,KAAK,YAAY,YAAY,kBAAkB,eAAe,OAAO,IAAI,SAAS;AAI1F,QAAM,WAAW,gBAAgB,GAAG;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,kBAAgB,oBAAoB;AAAA,MACpC,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,mBAAmB,mBAAmB;AAAA,QACtC;AAAA,MACF;AAAA,MAGC;AAAA,iCAAyB,WAAW,SAAS,IAC5C;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,wBAAwB;AAAA,YAC7C;AAAA,YAEA,8BAAC,qBAAkB,WAAW,kBAAkB;AAAA;AAAA,QAClD,IACE;AAAA,QAEH,SAAS,oBAAC,SAAI,WAAW,GAAG,YAAY,mBAAmB,gBAAgB,WAAW,GAAI,kBAAO,IAAS;AAAA,QAE3G;AAAA,UAAC;AAAA;AAAA,YACC,cAAY,OAAO;AAAA,YACnB,WAAW,GAAG,8CAA8C,mBAAmB,WAAW,MAAM;AAAA,YAEhG,8BAAC,SAAI,WAAW,GAAG,iBAAiB,mBAAmB,UAAU,OAAO,GACrE,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,WAAW,kBAAkB,UAAU,YAA/D,MAAM,EAAmE,CAC7F,GACH;AAAA;AAAA,QACF;AAAA,QAEC,UAAU,qBACT;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,2CAA2C;AAAA,YAChE;AAAA,YAEC;AAAA,eAAC,oBAAoB,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,cAErE,qBACC,qBAAC,WACC;AAAA,oCAAC,kBAAe,SAAO,MACrB;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS;AAAA,oBACT,cAAY,mBAAmB,OAAO,gBAAgB,OAAO;AAAA,oBAC7D,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA,mBAAmB,0BAA0B;AAAA,oBAC/C;AAAA,oBAEC,6BACC,oBAAC,iBAAc,WAAU,UAAS,IAElC,iCACE;AAAA,0CAAC,kBAAe,WAAU,UAAS;AAAA,sBAGnC,oBAAC,UAAK,WAAU,YAAY,iBAAO,sBAAqB;AAAA,uBAC1D;AAAA;AAAA,gBAEJ,GACF;AAAA,gBACA,qBAAC,kBAAe,MAAK,SAClB;AAAA,qCAAmB,OAAO,gBAAgB,OAAO;AAAA,kBAAgB;AAAA,mBACpE;AAAA,iBACF,IACE;AAAA;AAAA;AAAA,QACN,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/sidebar.tsx"],"sourcesContent":["\"use client\";\n\nimport { PanelLeftClose, PanelLeftOpen } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { SidebarGroup } from \"./sidebar-group.js\";\nimport { appAccentStyle, flattenNavItems } from \"./utils.js\";\nimport { WorkspaceSwitcher } from \"./workspace-switcher.js\";\n\nexport interface SidebarProps {\n className?: string;\n /** Slot ngay dưới WorkspaceSwitcher (vd nút \"Tạo mới\", ô tìm nhanh). */\n header?: ReactNode;\n /** Slot đáy sidebar (dung lượng gói, \"Mời thành viên\", phiên bản app…). */\n footer?: ReactNode;\n /** Ẩn WorkspaceSwitcher ở đỉnh — dùng khi app đặt bộ chuyển workspace ở header. */\n showWorkspaceSwitcher?: boolean;\n /** Ẩn nút thu gọn ở đáy (khi header đã có nút này). */\n showCollapseButton?: boolean;\n}\n\n/**\n * Sidebar desktop: 256px (`--sidebar-width`) / 56px khi thu gọn (`--sidebar-width-icon`).\n * Đỉnh: `WorkspaceSwitcher` (trục 1). Giữa: menu của app hiện tại. Đáy: slot footer.\n *\n * Ẩn hẳn dưới `md` — màn hình hẹp dùng `MobileMenu`.\n *\n * KHÔNG dùng `ui/sidebar.tsx` của shadcn (spec §7.6): primitive đó quản state bằng\n * cookie + provider riêng, sẽ thành nguồn sự thật thứ hai bên cạnh `ShellProvider`.\n */\nexport function Sidebar({\n className,\n header,\n footer,\n showWorkspaceSwitcher = true,\n showCollapseButton = true\n}: SidebarProps) {\n const { nav, currentApp, workspaces, sidebarCollapsed, toggleSidebar, labels } = useShell();\n\n // Đối chiếu khớp chính xác trên TOÀN BỘ mục của app, không chỉ mục cùng nhóm:\n // \"/tasks\" nằm nhóm A và \"/tasks/inbox\" nhóm B thì vẫn phải sáng đúng một mục.\n const allItems = flattenNavItems(nav);\n\n return (\n <aside\n data-collapsed={sidebarCollapsed || undefined}\n style={appAccentStyle(currentApp)}\n className={cn(\n \"hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex\",\n \"transition-[width] duration-200 ease-out\",\n sidebarCollapsed ? \"w-sidebar-icon\" : \"w-sidebar\",\n className\n )}\n >\n {/* Không có workspace nào (app đơn tổ chức) thì bỏ hẳn dải đỉnh, không để lại ô trống. */}\n {showWorkspaceSwitcher && workspaces.length > 0 ? (\n <div\n className={cn(\n \"flex h-header shrink-0 items-center border-b border-sidebar-border\",\n sidebarCollapsed ? \"justify-center px-1\" : \"px-2\"\n )}\n >\n <WorkspaceSwitcher collapsed={sidebarCollapsed} />\n </div>\n ) : null}\n\n {header ? <div className={cn(\"shrink-0\", sidebarCollapsed ? \"px-1.5 pt-2\" : \"px-2 pt-2\")}>{header}</div> : null}\n\n <nav\n aria-label={labels.navigationLabel}\n className={cn(\"flex-1 overflow-y-auto scrollbar-thin py-3\", sidebarCollapsed ? \"px-1.5\" : \"px-2\")}\n >\n <div className={cn(\"flex flex-col\", sidebarCollapsed ? \"gap-2\" : \"gap-4\")}>\n {nav.map((group) => (\n <SidebarGroup key={group.id} group={group} collapsed={sidebarCollapsed} siblings={allItems} />\n ))}\n </div>\n </nav>\n\n {footer || showCollapseButton ? (\n <div\n className={cn(\n \"shrink-0 border-t border-sidebar-border\",\n sidebarCollapsed ? \"flex flex-col items-center gap-1 p-1.5\" : \"p-2\"\n )}\n >\n {!sidebarCollapsed && footer ? <div className=\"mb-1\">{footer}</div> : null}\n\n {showCollapseButton ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n /*\n * ⚠ Màu của NÚT giữ nguyên `/60`, còn ICON được tô riêng bằng token ĐẶC. Hai\n * thứ khác nhau ở đây là CỐ Ý, và đây là chỗ duy nhất trong shell phải tách:\n *\n * · nút này chứa CẢ CHỮ khi mở rộng (`text-xs`, nhãn \"Thu gọn\"), nên màu của nó\n * chịu ngưỡng VĂN BẢN 4,5:1. `/60` phẳng ra `#6C6E71` = **4,90:1** ✓, còn\n * `--muted-foreground` chỉ **3,67:1** ✗ — đổi màu cả nút là làm rớt AA đúng\n * cái nhãn đang đạt.\n * · nhưng `/60` là màu có ALPHA, mà icon lấy màu qua `currentColor` — nét cắt\n * nhau sẽ bị tô hai lần và lộ vệt nối (xem `docs/hop-dong-mau.md` §7b, ca KẾ\n * THỪA). Icon chỉ chịu ngưỡng 3:1 nên `--muted-foreground` đủ cho nó.\n *\n * `group` + `group-hover:` là cái giá của việc tách: icon có màu riêng thì\n * `hover:text-sidebar-foreground` của nút không với tới nó nữa.\n */\n className={cn(\n \"group flex items-center gap-2 rounded-md text-sidebar-foreground/60\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent hover:text-sidebar-foreground\",\n sidebarCollapsed ? \"size-9 justify-center\" : \"h-8 w-full px-2 text-xs\"\n )}\n >\n {sidebarCollapsed ? (\n <PanelLeftOpen className=\"size-4 text-muted-foreground group-hover:text-sidebar-foreground\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4 text-muted-foreground group-hover:text-sidebar-foreground\" />\n {/* Chữ NHÌN THẤY dùng bản ngắn; `aria-label` và tooltip ở trên vẫn giữ bản\n dài. Xem hợp đồng của `collapseSidebarShort` trong types.ts. */}\n <span className=\"truncate\">{labels.collapseSidebarShort}</span>\n </>\n )}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"right\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · Ctrl/⌘ + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n </div>\n ) : null}\n </aside>\n );\n}\n"],"mappings":";AAgEU,SAyDU,UAzDV,KAyDU,YAzDV;AA9DV,SAAS,gBAAgB,qBAAqB;AAE9C,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;AAuB3B,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,wBAAwB;AAAA,EACxB,qBAAqB;AACvB,GAAiB;AACf,QAAM,EAAE,KAAK,YAAY,YAAY,kBAAkB,eAAe,OAAO,IAAI,SAAS;AAI1F,QAAM,WAAW,gBAAgB,GAAG;AAEpC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,kBAAgB,oBAAoB;AAAA,MACpC,OAAO,eAAe,UAAU;AAAA,MAChC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,mBAAmB,mBAAmB;AAAA,QACtC;AAAA,MACF;AAAA,MAGC;AAAA,iCAAyB,WAAW,SAAS,IAC5C;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,wBAAwB;AAAA,YAC7C;AAAA,YAEA,8BAAC,qBAAkB,WAAW,kBAAkB;AAAA;AAAA,QAClD,IACE;AAAA,QAEH,SAAS,oBAAC,SAAI,WAAW,GAAG,YAAY,mBAAmB,gBAAgB,WAAW,GAAI,kBAAO,IAAS;AAAA,QAE3G;AAAA,UAAC;AAAA;AAAA,YACC,cAAY,OAAO;AAAA,YACnB,WAAW,GAAG,8CAA8C,mBAAmB,WAAW,MAAM;AAAA,YAEhG,8BAAC,SAAI,WAAW,GAAG,iBAAiB,mBAAmB,UAAU,OAAO,GACrE,cAAI,IAAI,CAAC,UACR,oBAAC,gBAA4B,OAAc,WAAW,kBAAkB,UAAU,YAA/D,MAAM,EAAmE,CAC7F,GACH;AAAA;AAAA,QACF;AAAA,QAEC,UAAU,qBACT;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,mBAAmB,2CAA2C;AAAA,YAChE;AAAA,YAEC;AAAA,eAAC,oBAAoB,SAAS,oBAAC,SAAI,WAAU,QAAQ,kBAAO,IAAS;AAAA,cAErE,qBACC,qBAAC,WACC;AAAA,oCAAC,kBAAe,SAAO,MACrB;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS;AAAA,oBACT,cAAY,mBAAmB,OAAO,gBAAgB,OAAO;AAAA,oBAgB7D,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA,mBAAmB,0BAA0B;AAAA,oBAC/C;AAAA,oBAEC,6BACC,oBAAC,iBAAc,WAAU,oEAAmE,IAE5F,iCACE;AAAA,0CAAC,kBAAe,WAAU,oEAAmE;AAAA,sBAG7F,oBAAC,UAAK,WAAU,YAAY,iBAAO,sBAAqB;AAAA,uBAC1D;AAAA;AAAA,gBAEJ,GACF;AAAA,gBACA,qBAAC,kBAAe,MAAK,SAClB;AAAA,qCAAmB,OAAO,gBAAgB,OAAO;AAAA,kBAAgB;AAAA,mBACpE;AAAA,iBACF,IACE;AAAA;AAAA;AAAA,QACN,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "1.8.3",
3
+ "version": "1.9.0",
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",