@comitor/ui 3.9.0 → 3.11.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.
@@ -122,7 +122,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
122
122
  pasteDialog: PasteDialogState;
123
123
  onRowAdd: ((event?: React.MouseEvent<HTMLDivElement>) => Promise<void>) | undefined;
124
124
  adjustLayout: boolean;
125
- locale: "en" | "vi" | {
125
+ locale: {
126
126
  grid: {
127
127
  label: string;
128
128
  addRow: string;
@@ -397,7 +397,7 @@ declare function useDataGrid<TData>({ data, columns, rowHeight: rowHeightProp, o
397
397
  shortcuts: string;
398
398
  };
399
399
  };
400
- } | undefined;
400
+ } | "en" | "vi" | undefined;
401
401
  };
402
402
 
403
403
  export { type UseDataGridProps, useDataGrid };
@@ -18,6 +18,16 @@
18
18
  * (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.
19
19
  * Nhận bảng màu qua tham số thì phụ thuộc chỉ đi MỘT chiều: tokens.ts → file này.
20
20
  */
21
+ /** Bảy sắc màu phụ mà bảng tone đọc (gold đi riêng qua thang của nó). */
22
+ interface AvatarToneHues {
23
+ readonly teal: string;
24
+ readonly green: string;
25
+ readonly red: string;
26
+ readonly yellow: string;
27
+ readonly blue: string;
28
+ readonly purple: string;
29
+ readonly pink: string;
30
+ }
21
31
  /** Đúng 8 tone — chốt ở tầng kiểu: `getAvatarToneIndex` băm theo độ dài này. */
22
32
  type AvatarToneTable<T> = readonly [T, T, T, T, T, T, T, T];
23
33
  /** Cặp nền + màu chữ (hex đục) của một tone avatar. */
@@ -37,34 +47,10 @@ interface AvatarTonePalette {
37
47
  readonly 500: string;
38
48
  readonly 600: string;
39
49
  };
40
- /** Thang xám ash — cần bậc 100 · 500 · 700. */
41
- ash: {
42
- readonly 100: string;
43
- readonly 500: string;
44
- readonly 700: string;
45
- };
46
- /** Trung tính brand — mực và giấy. */
47
- brandNeutral: {
48
- readonly ink: string;
49
- readonly inkSoft: string;
50
- readonly paper: string;
51
- };
52
50
  /** Màu phụ ở vai trò TÔ NỀN (`accent.light` hoặc `accent.dark`). */
53
- accent: {
54
- readonly teal: string;
55
- readonly green: string;
56
- readonly red: string;
57
- readonly yellow: string;
58
- readonly navy: string;
59
- };
51
+ accent: AvatarToneHues;
60
52
  /** Cùng màu phụ đó ở vai trò CHỮ trên nền trang (`accentInk.light` hoặc `accentInk.dark`). */
61
- accentInk: {
62
- readonly teal: string;
63
- readonly green: string;
64
- readonly red: string;
65
- readonly yellow: string;
66
- readonly navy: string;
67
- };
53
+ accentInk: AvatarToneHues;
68
54
  /** Nền mà tint hợp thành lên — `--card` của theme. */
69
55
  card: string;
70
56
  }
@@ -127,8 +113,19 @@ interface AvatarTone {
127
113
  * 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
114
  * thái — xem lý do dài ở docblock của `AVATAR_TONES`.
129
115
  */
130
- declare const AVATAR_TONE_NAMES: readonly ["gold", "teal", "green", "red", "yellow", "navy", "ash", "neutral"];
116
+ declare const AVATAR_TONE_NAMES: readonly ["gold", "teal", "green", "red", "yellow", "blue", "purple", "pink"];
131
117
  type AvatarToneName = (typeof AVATAR_TONE_NAMES)[number];
118
+ /**
119
+ * 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
120
+ * đề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
121
+ * 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:
122
+ * người/app nào trước ra `navy` thì nay ra `blue`, không ai nhảy sang tone khác).
123
+ *
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
+ * 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
+ * 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
+ declare const LEGACY_AVATAR_TONE_ALIASES: Readonly<Record<string, AvatarToneName>>;
132
129
  declare const AVATAR_TONES: AvatarToneTable<AvatarTone>;
133
130
  /**
134
131
  * Chỉ số tone theo tên — phép chọn màu avatar DUY NHẤT của cả gói.
@@ -145,8 +142,12 @@ declare function getAvatarTone(name: string): AvatarTone;
145
142
  *
146
143
  * 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
144
  * 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.
145
+ * đã 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 đã
146
+ * ĐỔI thì đi qua `LEGACY_AVATAR_TONE_ALIASES` trước khi tra.
147
+ *
148
+ * ⚠ `LEGACY_AVATAR_TONE_ALIASES` dùng `Record` trần: `"constructor"` tra ra một hàm, không phải
149
+ * 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`.
149
150
  */
150
151
  declare function getAvatarToneByName(name: string | null | undefined): AvatarTone | undefined;
151
152
 
152
- export { AVATAR_TONES, AVATAR_TONE_NAMES, type AvatarToneColors, type AvatarToneName, type AvatarTonePalette, getAvatarTone, getAvatarToneByName, getAvatarToneIndex };
153
+ export { AVATAR_TONES, AVATAR_TONE_NAMES, type AvatarToneColors, type AvatarToneName, type AvatarTonePalette, LEGACY_AVATAR_TONE_ALIASES, getAvatarTone, getAvatarToneByName, getAvatarToneIndex };
@@ -3,7 +3,12 @@ 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
+ const AVATAR_TONE_NAMES = ["gold", "teal", "green", "red", "yellow", "blue", "purple", "pink"];
7
+ const LEGACY_AVATAR_TONE_ALIASES = {
8
+ navy: "blue",
9
+ ash: "purple",
10
+ neutral: "pink"
11
+ };
7
12
  const AVATAR_TONES = [
8
13
  {
9
14
  name: "gold",
@@ -36,23 +41,26 @@ const AVATAR_TONES = [
36
41
  dark: (p) => ({ background: tintOver(p.accent.yellow, p.card, 0.2), foreground: p.accentInk.yellow })
37
42
  },
38
43
  {
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",
41
- classes: "bg-chart-3/15 text-chart-3",
42
- light: (p) => ({ background: tintOver(p.accent.navy, p.card, 0.15), foreground: p.accentInk.navy }),
43
- dark: (p) => ({ background: tintOver(p.accentInk.navy, p.card, 0.15), foreground: p.accentInk.navy })
44
+ // 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 ·
45
+ // ash · neutral từ 3.10.0, xem `LEGACY_AVATAR_TONE_ALIASES`. Cùng khuôn teal/green/red: tint /15
46
+ // + 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.
47
+ // Chữ trên tint /15: blue 5,67 · purple 5,72 · pink 5,33:1 (light) · 5,76 · 5,17 · 5,37:1 (dark).
48
+ name: "blue",
49
+ classes: "bg-blue/15 text-blue-ink",
50
+ light: (p) => ({ background: tintOver(p.accent.blue, p.card, 0.15), foreground: p.accentInk.blue }),
51
+ dark: (p) => ({ background: tintOver(p.accent.blue, p.card, 0.15), foreground: p.accentInk.blue })
44
52
  },
45
53
  {
46
- name: "ash",
47
- classes: "bg-ash-100 text-ash-700 dark:bg-ash-500/25 dark:text-ash-100",
48
- light: (p) => ({ background: p.ash[100], foreground: p.ash[700] }),
49
- dark: (p) => ({ background: tintOver(p.ash[500], p.card, 0.25), foreground: p.ash[100] })
54
+ name: "purple",
55
+ classes: "bg-purple/15 text-purple-ink",
56
+ light: (p) => ({ background: tintOver(p.accent.purple, p.card, 0.15), foreground: p.accentInk.purple }),
57
+ dark: (p) => ({ background: tintOver(p.accent.purple, p.card, 0.15), foreground: p.accentInk.purple })
50
58
  },
51
59
  {
52
- name: "neutral",
53
- classes: "bg-ink/10 text-ink-soft dark:bg-paper/15 dark:text-paper",
54
- light: (p) => ({ background: tintOver(p.brandNeutral.ink, p.card, 0.1), foreground: p.brandNeutral.inkSoft }),
55
- dark: (p) => ({ background: tintOver(p.brandNeutral.paper, p.card, 0.15), foreground: p.brandNeutral.paper })
60
+ name: "pink",
61
+ classes: "bg-pink/15 text-pink-ink",
62
+ light: (p) => ({ background: tintOver(p.accent.pink, p.card, 0.15), foreground: p.accentInk.pink }),
63
+ dark: (p) => ({ background: tintOver(p.accent.pink, p.card, 0.15), foreground: p.accentInk.pink })
56
64
  }
57
65
  ];
58
66
  function hashString(input) {
@@ -71,11 +79,13 @@ function getAvatarTone(name) {
71
79
  }
72
80
  function getAvatarToneByName(name) {
73
81
  if (!name) return void 0;
74
- return AVATAR_TONES.find((tone) => tone.name === name);
82
+ const current = LEGACY_AVATAR_TONE_ALIASES[name] ?? name;
83
+ return AVATAR_TONES.find((tone) => tone.name === current);
75
84
  }
76
85
  export {
77
86
  AVATAR_TONES,
78
87
  AVATAR_TONE_NAMES,
88
+ LEGACY_AVATAR_TONE_ALIASES,
79
89
  getAvatarTone,
80
90
  getAvatarToneByName,
81
91
  getAvatarToneIndex
@@ -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 /**\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":[]}
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":[]}
@@ -42,8 +42,8 @@ function AppLauncherPanel({ variant = "grid", className, onSelected }) {
42
42
  const isCurrent = app.id === currentApp?.id;
43
43
  const Icon = app.icon;
44
44
  const tileClass = cn(
45
- "group relative flex rounded-md text-left transition-colors duration-150 ease-out",
46
- variant === "grid" ? "flex-col items-start gap-1.5 p-2.5" : "items-center gap-2.5 p-2",
45
+ "group relative flex rounded-md transition-colors duration-150 ease-out",
46
+ variant === "grid" ? "aspect-square flex-col items-center justify-center gap-1.5 p-1.5 text-center" : "items-center gap-2.5 p-2 text-left",
47
47
  isLocked ? "hover:bg-muted" : "hover:bg-accent",
48
48
  isCurrent && "bg-accent"
49
49
  );
@@ -56,54 +56,59 @@ function AppLauncherPanel({ variant = "grid", className, onSelected }) {
56
56
  seed: app.id,
57
57
  icon: Icon,
58
58
  tone: app.tone,
59
- size: variant === "grid" ? "md" : "sm",
60
- className: cn(isLocked && "bg-muted text-muted-foreground")
59
+ size: variant === "grid" ? "md" : "sm"
61
60
  }
62
61
  ),
63
62
  isLocked ? /* @__PURE__ */ jsx("span", { className: "absolute -bottom-1 -right-1 flex size-4 items-center justify-center rounded-full bg-muted text-muted-foreground ring-2 ring-popover", children: /* @__PURE__ */ jsx(Lock, { className: "size-2.5" }) }) : null
64
63
  ] }),
65
64
  /* @__PURE__ */ jsxs("span", { className: cn("flex min-w-0 flex-col", variant === "grid" ? "w-full" : "flex-1"), children: [
66
- /* @__PURE__ */ jsxs("span", { className: cn("flex min-w-0 gap-1.5", variant === "grid" ? "items-start" : "items-center"), children: [
67
- /* @__PURE__ */ jsx(
68
- "span",
69
- {
70
- className: cn(
71
- "text-sm font-medium text-foreground",
65
+ /* @__PURE__ */ jsxs(
66
+ "span",
67
+ {
68
+ className: cn("flex min-w-0 gap-1.5", variant === "grid" ? "items-start justify-center" : "items-center"),
69
+ children: [
70
+ /* @__PURE__ */ jsx(
71
+ "span",
72
+ {
73
+ className: cn(
74
+ "text-sm font-medium text-foreground",
75
+ /*
76
+ * Lưới: tên ứng dụng dài hơn ô là chuyện thường ("Tài khoản Comitor" ở ô 109px),
77
+ * nên cho hai dòng rồi mới cắt — `grid-cols-3` khai `items-stretch` mặc định nên
78
+ * ô cao thêm một dòng không làm hàng bị so le.
79
+ *
80
+ * `break-words` đi KÈM `line-clamp-2`, không phải phòng xa: `-webkit-line-clamp`
81
+ * chỉ đặt được dấu "…" khi chữ THẬT SỰ tràn sang dòng thứ ba. Một tên không có
82
+ * khoảng trắng thì không xuống dòng được, nên nó nằm gọn một dòng và bị
83
+ * `overflow: hidden` cắt PHẲNG giữa chữ — đo được: `soDong: 1`,
84
+ * `scrollHeight === clientHeight`, không có dấu ba chấm nào. Cho phép ngắt trong
85
+ * từ thì nó thành hai dòng và dấu "…" quay lại.
86
+ *
87
+ * Danh sách: mỗi mục là một hàng ngang trong popover hẹp; hai dòng ở đó làm chiều
88
+ * cao các mục nhấp nhô. Giữ một dòng — `truncate` vốn đã chạy đúng ở đó vì `flex-1`
89
+ * ghim sẵn bề rộng. Cùng lý do tác giả chỉ cho `description` xuống hai dòng ở lưới.
90
+ */
91
+ variant === "grid" ? "line-clamp-2 break-words" : "truncate"
92
+ ),
93
+ children: app.name
94
+ }
95
+ ),
96
+ app.badge ? (
72
97
  /*
73
- * Lưới: tên ứng dụng dài hơn ô là chuyện thường ("Tài khoản Comitor" ở ô 109px),
74
- * nên cho hai dòng rồi mới cắt — `grid-cols-3` khai `items-stretch` mặc định nên
75
- * ô cao thêm một dòng không làm hàng bị so le.
76
- *
77
- * `break-words` đi KÈM `line-clamp-2`, không phải phòng xa: `-webkit-line-clamp`
78
- * chỉ đặt được dấu "…" khi chữ THẬT SỰ tràn sang dòng thứ ba. Một tên không có
79
- * khoảng trắng thì không xuống dòng được, nên nó nằm gọn một dòng và bị
80
- * `overflow: hidden` cắt PHẲNG giữa chữ — đo được: `soDong: 1`,
81
- * `scrollHeight === clientHeight`, không có dấu ba chấm nào. Cho phép ngắt trong
82
- * từ thì nó thành hai dòng và dấu "…" quay lại.
98
+ * `shrink-0` KHÔNG phải để chống bóp méo — đo rồi: có hay không, badge đều 116,8px,
99
+ * vì `whitespace-nowrap` trong `Badge` đã giữ sàn `min-width: auto` bằng trọn bề rộng
100
+ * chữ, và phần co dồn hết sang ô tên (`overflow: hidden` → sàn 0).
83
101
  *
84
- * Danh sách: mỗi mục là một hàng ngang trong popover hẹp; hai dòng ở đó làm chiều
85
- * cao các mục nhấp nhô. Giữ một dòng — `truncate` vốn đã chạy đúng ở đó vì `flex-1`
86
- * ghim sẵn bề rộng. Cùng lý do tác giả chỉ cho `description` xuống hai dòng ở lưới.
102
+ * Nó ở đây vì hai lý do khác: khớp quy ước của mọi chỗ gọi `Badge` khác trong shell
103
+ * (`workspace-switcher`, `sidebar-menu-item` — chỗ này là chỗ DUY NHẤT thiếu), và để
104
+ * lá chắn thôi phụ thuộc vào một lớp ngầm trong `Badge`. Ai đó thêm `truncate` vào
105
+ * base cva của `Badge` là sàn tụt về 0 và badge méo, im lặng.
87
106
  */
88
- variant === "grid" ? "line-clamp-2 break-words" : "truncate"
89
- ),
90
- children: app.name
91
- }
92
- ),
93
- app.badge ? (
94
- /*
95
- * `shrink-0` KHÔNG phải để chống bóp méo — đo rồi: có hay không, badge đều 116,8px,
96
- * vì `whitespace-nowrap` trong `Badge` đã giữ sàn `min-width: auto` bằng trọn bề rộng
97
- * chữ, và phần co dồn hết sang ô tên (`overflow: hidden` → sàn 0).
98
- *
99
- * Nó ở đây vì hai lý do khác: khớp quy ước của mọi chỗ gọi `Badge` khác trong shell
100
- * (`workspace-switcher`, `sidebar-menu-item` — chỗ này là chỗ DUY NHẤT thiếu), và để
101
- * lá chắn thôi phụ thuộc vào một lớp ngầm trong `Badge`. Ai đó thêm `truncate` vào
102
- * base cva của `Badge` là sàn tụt về 0 và badge méo, im lặng.
103
- */
104
- /* @__PURE__ */ jsx(Badge, { variant: "destructive", size: "sm", className: "shrink-0", children: app.badge })
105
- ) : null
106
- ] }),
107
+ /* @__PURE__ */ jsx(Badge, { variant: "destructive", size: "sm", className: "shrink-0", children: app.badge })
108
+ ) : null
109
+ ]
110
+ }
111
+ ),
107
112
  app.description ? /* @__PURE__ */ jsx(
108
113
  "span",
109
114
  {
@@ -165,8 +170,8 @@ function AppLauncherPanel({ variant = "grid", className, onSelected }) {
165
170
  );
166
171
  };
167
172
  if (total === 0) return null;
168
- const groupClass = variant === "grid" ? "grid auto-rows-fr grid-cols-3 gap-1" : "flex flex-col gap-0.5";
169
- return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("flex flex-col gap-3", className), children: [
173
+ const groupClass = variant === "grid" ? "grid auto-rows-fr grid-cols-3 gap-2" : "flex flex-col gap-0.5";
174
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("flex flex-col", variant === "grid" ? "gap-4" : "gap-3", className), children: [
170
175
  entitled.length > 0 ? /* @__PURE__ */ jsxs("section", { children: [
171
176
  /* @__PURE__ */ jsx("p", { className: "px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: labels.myApps }),
172
177
  /* @__PURE__ */ jsx("div", { className: groupClass, children: entitled.map(renderTile) })
@@ -207,7 +212,7 @@ function AppLauncher({
207
212
  ) }) }),
208
213
  /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: labels.apps })
209
214
  ] }),
210
- /* @__PURE__ */ jsx(PopoverContent, { align, sideOffset: 8, className: "w-[22rem] p-2", children: /* @__PURE__ */ jsx(
215
+ /* @__PURE__ */ jsx(PopoverContent, { align, sideOffset: 8, className: "w-[21rem] px-4 py-3", children: /* @__PURE__ */ jsx(
211
216
  AppLauncherPanel,
212
217
  {
213
218
  variant,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/app-launcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { LayoutGrid, Lock } from \"lucide-react\";\nimport { type KeyboardEvent, useRef } from \"react\";\nimport { IconAvatar } from \"../components/composite/icon-avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { AppDescriptor } from \"./types.js\";\nimport { getEntitledApps, getLockedApps } from \"./utils.js\";\n\n/** Đánh dấu ô app để điều hướng bàn phím tìm được, kể cả ô render bằng LinkComponent của app. */\nconst TILE_ATTR = \"data-app-tile\";\n\nexport interface AppLauncherPanelProps {\n /** \"grid\" = lưới 3 cột; \"list\" = danh sách dọc (menu mobile). */\n variant?: \"grid\" | \"list\";\n className?: string;\n /** Gọi sau khi người dùng chọn một app — dùng để đóng popover/menu mobile. */\n onSelected?: () => void;\n}\n\n/**\n * Nội dung của AppLauncher, tách riêng để `MobileMenu` nhúng thẳng (không popover).\n *\n * Điều hướng bàn phím: ←/→/↑/↓ chạy giữa các ô (xuyên cả hai nhóm như một lưới liền),\n * Home/End nhảy đầu-cuối; Enter/Space để chính thẻ button/link xử lý.\n * Vị trí ô đọc từ DOM chứ không giữ mảng ref: ô \"đã mở khoá\" có thể là `LinkComponent`\n * do app cung cấp — không chắc component đó nhận `ref`.\n */\nexport function AppLauncherPanel({ variant = \"grid\", className, onSelected }: AppLauncherPanelProps) {\n const { apps, currentApp, onAppSelect, onUpsell, labels } = useShell();\n\n const entitled = getEntitledApps(apps);\n const locked = getLockedApps(apps);\n const total = entitled.length + locked.length;\n\n const rootRef = useRef<HTMLDivElement>(null);\n const columns = variant === \"grid\" ? 3 : 1;\n\n const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n const root = rootRef.current;\n if (!root) return;\n const tiles = Array.from(root.querySelectorAll<HTMLElement>(`[${TILE_ATTR}]`));\n const index = tiles.indexOf(event.currentTarget);\n if (index < 0) return;\n\n const moves: Record<string, number | undefined> = {\n ArrowRight: index + 1,\n ArrowLeft: index - 1,\n ArrowDown: index + columns,\n ArrowUp: index - columns,\n Home: 0,\n End: tiles.length - 1\n };\n const next = moves[event.key];\n if (next === undefined) return;\n\n event.preventDefault();\n tiles[Math.max(0, Math.min(tiles.length - 1, next))]?.focus();\n };\n\n const renderTile = (app: AppDescriptor) => {\n const isLocked = app.entitled === false;\n const isCurrent = app.id === currentApp?.id;\n const Icon = app.icon;\n\n // Ô KHOÁ không tô `opacity-60` nữa. Nó KHÔNG phải control vô hiệu — bấm được và gọi\n // `onUpsell(appId)` — nên không hưởng miễn trừ \"thành phần vô hiệu\" của WCAG 1.4.3/1.4.11.\n // Với 60% độ mờ thì mô tả app tụt còn 2,41:1 (light) / 2,74:1 (dark) ✗ và icon trong ô\n // `bg-muted` còn 2,31:1 / 2,60:1 ✗, chỉ phục hồi khi hover — tức người không rê chuột thì\n // không đọc được. Bỏ độ mờ: mô tả `text-muted-foreground` 5,23:1 (light) / 5,35:1 (dark) ✓,\n // icon trên `bg-muted` 4,89:1 / 4,86:1 ✓. Tín hiệu \"chưa mở khoá\" không mất gì: ô icon vẫn\n // xám (thay vì tô màu app), vẫn có huy hiệu ổ khoá, vẫn có `title`/`aria-label` nói rõ.\n const tileClass = cn(\n \"group relative flex rounded-md text-left transition-colors duration-150 ease-out\",\n variant === \"grid\" ? \"flex-col items-start gap-1.5 p-2.5\" : \"items-center gap-2.5 p-2\",\n isLocked ? \"hover:bg-muted\" : \"hover:bg-accent\",\n isCurrent && \"bg-accent\"\n );\n\n const body = (\n <>\n {/*\n * ── Ô icon: `IconAvatar`, màu TỰ SUY theo `id` ────────────────────────────────────\n * Từ 1.10.0 ô này thôi tô bằng `--app-accent`. Lý do không phải thẩm mỹ: bảng accent cũ\n * bắt mỗi app phải được ai đó gán màu tay, nên sản phẩm thứ sáu hoặc trùng màu với sản\n * phẩm khác, hoặc phải chờ một lần sửa mã. `IconAvatar` suy màu từ `id` bằng bảng tone\n * dùng chung với `LetterAvatar`, nên thêm app là nó tự có màu — và cùng màu ấy ở email,\n * canvas, PDF vì `getAvatarToneColors` đọc cùng bảng.\n *\n * ⚠ `seed={app.id}`, KHÔNG phải `app.name`: tên đổi theo ngôn ngữ giao diện. Đo được\n * 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên.\n *\n * ⚠ Nhánh KHOÁ vẫn tô xám bằng `className`, không bằng một tone: \"chưa mở khoá\" là\n * trạng thái, mà bảng tone là màu ĐỊNH DANH — mượn một tone làm tín hiệu trạng thái là\n * đúng cái nhầm vai mà `lib/avatar-tone.ts` cấm. `cn()` cho `bg-muted` thắng tone nhờ\n * tailwind-merge; cả hai lớp đều ĐẶC nên icon không lộ vệt nối.\n */}\n <span className=\"relative flex shrink-0\">\n <IconAvatar\n name={app.name}\n seed={app.id}\n icon={Icon}\n tone={app.tone}\n size={variant === \"grid\" ? \"md\" : \"sm\"}\n className={cn(isLocked && \"bg-muted text-muted-foreground\")}\n />\n {isLocked ? (\n <span className=\"absolute -bottom-1 -right-1 flex size-4 items-center justify-center rounded-full bg-muted text-muted-foreground ring-2 ring-popover\">\n <Lock className=\"size-2.5\" />\n </span>\n ) : null}\n </span>\n\n {/*\n * `w-full` ở ô lưới là thứ THẬT SỰ bó được chữ — không phải `min-w-0`.\n *\n * Ô lưới là `flex-col items-start` (xem `tileClass`), nên theo trục ngang cột này lấy\n * `fit-content`: nó RỘNG RA THEO TÊN thay vì bằng ô. Mọi phép cắt bên trong vì thế không\n * bao giờ có gì để bám vào. Đo ở app thật, ô 109px: tên một từ dài cho cột 272px và tràn\n * 174px, `scrollWidth === clientWidth` — `line-clamp` được đặt nhưng chưa từng chạy.\n * Thêm `min-w-0` vào hàng bên dưới KHÔNG đổi được số nào (đo: y hệt bản hỏng); `w-full`\n * đưa cột về đúng 89px và từ đó phép cắt mới có hiệu lực.\n *\n * Biến thể `list` không dính: ô đó là hàng ngang nên `flex-1` đã ghim bề rộng sẵn.\n */}\n <span className={cn(\"flex min-w-0 flex-col\", variant === \"grid\" ? \"w-full\" : \"flex-1\")}>\n {/*\n * `items-start` chứ không `items-center` khi tên có thể xuống hai dòng: căn giữa đẩy\n * badge xuống giữa khe hai dòng (đo được 10,6px), xa hẳn dòng chữ nó đang chú thích.\n * Tên một dòng thì hai cách lệch nhau 0,8px — không nhìn thấy.\n */}\n <span className={cn(\"flex min-w-0 gap-1.5\", variant === \"grid\" ? \"items-start\" : \"items-center\")}>\n <span\n className={cn(\n \"text-sm font-medium text-foreground\",\n /*\n * Lưới: tên ứng dụng dài hơn ô là chuyện thường (\"Tài khoản Comitor\" ở ô 109px),\n * nên cho hai dòng rồi mới cắt — `grid-cols-3` khai `items-stretch` mặc định nên\n * ô cao thêm một dòng không làm hàng bị so le.\n *\n * `break-words` đi KÈM `line-clamp-2`, không phải phòng xa: `-webkit-line-clamp`\n * chỉ đặt được dấu \"…\" khi chữ THẬT SỰ tràn sang dòng thứ ba. Một tên không có\n * khoảng trắng thì không xuống dòng được, nên nó nằm gọn một dòng và bị\n * `overflow: hidden` cắt PHẲNG giữa chữ — đo được: `soDong: 1`,\n * `scrollHeight === clientHeight`, không có dấu ba chấm nào. Cho phép ngắt trong\n * từ thì nó thành hai dòng và dấu \"…\" quay lại.\n *\n * Danh sách: mỗi mục là một hàng ngang trong popover hẹp; hai dòng ở đó làm chiều\n * cao các mục nhấp nhô. Giữ một dòng — `truncate` vốn đã chạy đúng ở đó vì `flex-1`\n * ghim sẵn bề rộng. Cùng lý do tác giả chỉ cho `description` xuống hai dòng ở lưới.\n */\n variant === \"grid\" ? \"line-clamp-2 break-words\" : \"truncate\"\n )}\n >\n {app.name}\n </span>\n {app.badge ? (\n /*\n * `shrink-0` KHÔNG phải để chống bóp méo — đo rồi: có hay không, badge đều 116,8px,\n * vì `whitespace-nowrap` trong `Badge` đã giữ sàn `min-width: auto` bằng trọn bề rộng\n * chữ, và phần co dồn hết sang ô tên (`overflow: hidden` → sàn 0).\n *\n * Nó ở đây vì hai lý do khác: khớp quy ước của mọi chỗ gọi `Badge` khác trong shell\n * (`workspace-switcher`, `sidebar-menu-item` — chỗ này là chỗ DUY NHẤT thiếu), và để\n * lá chắn thôi phụ thuộc vào một lớp ngầm trong `Badge`. Ai đó thêm `truncate` vào\n * base cva của `Badge` là sàn tụt về 0 và badge méo, im lặng.\n */\n <Badge variant=\"destructive\" size=\"sm\" className=\"shrink-0\">\n {app.badge}\n </Badge>\n ) : null}\n </span>\n {app.description ? (\n <span\n className={cn(\"text-[11px] leading-snug text-muted-foreground\", variant === \"grid\" && \"line-clamp-2\")}\n >\n {app.description}\n </span>\n ) : null}\n </span>\n </>\n );\n\n // Chưa mở khoá → không điều hướng, chỉ báo cho app biết để mời nâng gói.\n if (isLocked) {\n return (\n <button\n key={app.id}\n type=\"button\"\n data-app-tile=\"\"\n title={labels.lockedHint}\n aria-label={`${app.name} — ${labels.locked}`}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => {\n onUpsell?.(app.id);\n onSelected?.();\n }}\n >\n {body}\n </button>\n );\n }\n\n // Có `onAppSelect` → app tự lo điều hướng (đổi subdomain, nạp trước dữ liệu…);\n // không có → dùng link thật để giữ chuột-giữa / mở tab mới / trình đọc màn hình.\n if (onAppSelect) {\n return (\n <button\n key={app.id}\n type=\"button\"\n data-app-tile=\"\"\n aria-current={isCurrent ? \"page\" : undefined}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => {\n onAppSelect(app);\n onSelected?.();\n }}\n >\n {body}\n </button>\n );\n }\n\n return (\n <ShellNavLink\n key={app.id}\n href={app.href}\n data-app-tile=\"\"\n aria-current={isCurrent ? \"page\" : undefined}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => onSelected?.()}\n >\n {body}\n </ShellNavLink>\n );\n };\n\n if (total === 0) return null;\n\n /*\n * `auto-rows-fr` vá thứ duy nhất mà việc cho tên xuống hai dòng làm xấu đi: TRONG một hàng thì\n * các ô đã bằng nhau sẵn (`align-items: stretch` là mặc định của Grid), nhưng GIỮA các hàng thì\n * không — hàng có một tên hai dòng cao 116px trong khi hàng kia 97px. `fr` cho mọi hàng chung\n * một chiều cao.\n */\n const groupClass = variant === \"grid\" ? \"grid auto-rows-fr grid-cols-3 gap-1\" : \"flex flex-col gap-0.5\";\n\n return (\n <div ref={rootRef} className={cn(\"flex flex-col gap-3\", className)}>\n {entitled.length > 0 ? (\n <section>\n <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\">\n {labels.myApps}\n </p>\n <div className={groupClass}>{entitled.map(renderTile)}</div>\n </section>\n ) : null}\n\n {locked.length > 0 ? (\n <section>\n <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\">\n {labels.discoverApps}\n </p>\n <div className={groupClass}>{locked.map(renderTile)}</div>\n </section>\n ) : null}\n </div>\n );\n}\n\nexport interface AppLauncherProps extends AppLauncherPanelProps {\n /** Bỏ nút bấm + Popover, render thẳng panel (MobileMenu dùng cách này). */\n inline?: boolean;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 2 — chuyển sản phẩm trong cùng workspace.**\n *\n * Nút lưới 3×3 ở header mở popover lưới ô app.\n * Trạng thái mở nằm ở `ShellProvider` để phím tắt `Cmd/Ctrl + Shift + K` bật được.\n * App chưa có trong gói hiện ô khoá và gọi `onUpsell(appId)` thay vì điều hướng.\n */\nexport function AppLauncher({\n className,\n variant = \"grid\",\n inline = false,\n align = \"end\",\n onSelected\n}: AppLauncherProps) {\n const { apps, appLauncherOpen, setAppLauncherOpen, labels } = useShell();\n\n if (apps.length === 0) return null;\n\n if (inline) {\n return <AppLauncherPanel variant={variant} className={className} onSelected={onSelected} />;\n }\n\n return (\n <Popover open={appLauncherOpen} onOpenChange={setAppLauncherOpen}>\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.appLauncherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n appLauncherOpen && \"bg-muted text-foreground\",\n className\n )}\n >\n <LayoutGrid className=\"size-4\" />\n </button>\n </PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">{labels.apps}</TooltipContent>\n </Tooltip>\n\n <PopoverContent align={align} sideOffset={8} className=\"w-[22rem] p-2\">\n <AppLauncherPanel\n variant={variant}\n onSelected={() => {\n setAppLauncherOpen(false);\n onSelected?.();\n }}\n />\n </PopoverContent>\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/app-launcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { LayoutGrid, Lock } from \"lucide-react\";\nimport { type KeyboardEvent, useRef } from \"react\";\nimport { IconAvatar } from \"../components/composite/icon-avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ShellNavLink } from \"./shell-link.js\";\nimport type { AppDescriptor } from \"./types.js\";\nimport { getEntitledApps, getLockedApps } from \"./utils.js\";\n\n/** Đánh dấu ô app để điều hướng bàn phím tìm được, kể cả ô render bằng LinkComponent của app. */\nconst TILE_ATTR = \"data-app-tile\";\n\nexport interface AppLauncherPanelProps {\n /** \"grid\" = lưới 3 cột; \"list\" = danh sách dọc (menu mobile). */\n variant?: \"grid\" | \"list\";\n className?: string;\n /** Gọi sau khi người dùng chọn một app — dùng để đóng popover/menu mobile. */\n onSelected?: () => void;\n}\n\n/**\n * Nội dung của AppLauncher, tách riêng để `MobileMenu` nhúng thẳng (không popover).\n *\n * Điều hướng bàn phím: ←/→/↑/↓ chạy giữa các ô (xuyên cả hai nhóm như một lưới liền),\n * Home/End nhảy đầu-cuối; Enter/Space để chính thẻ button/link xử lý.\n * Vị trí ô đọc từ DOM chứ không giữ mảng ref: ô \"đã mở khoá\" có thể là `LinkComponent`\n * do app cung cấp — không chắc component đó nhận `ref`.\n */\nexport function AppLauncherPanel({ variant = \"grid\", className, onSelected }: AppLauncherPanelProps) {\n const { apps, currentApp, onAppSelect, onUpsell, labels } = useShell();\n\n const entitled = getEntitledApps(apps);\n const locked = getLockedApps(apps);\n const total = entitled.length + locked.length;\n\n const rootRef = useRef<HTMLDivElement>(null);\n const columns = variant === \"grid\" ? 3 : 1;\n\n const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n const root = rootRef.current;\n if (!root) return;\n const tiles = Array.from(root.querySelectorAll<HTMLElement>(`[${TILE_ATTR}]`));\n const index = tiles.indexOf(event.currentTarget);\n if (index < 0) return;\n\n const moves: Record<string, number | undefined> = {\n ArrowRight: index + 1,\n ArrowLeft: index - 1,\n ArrowDown: index + columns,\n ArrowUp: index - columns,\n Home: 0,\n End: tiles.length - 1\n };\n const next = moves[event.key];\n if (next === undefined) return;\n\n event.preventDefault();\n tiles[Math.max(0, Math.min(tiles.length - 1, next))]?.focus();\n };\n\n const renderTile = (app: AppDescriptor) => {\n const isLocked = app.entitled === false;\n const isCurrent = app.id === currentApp?.id;\n const Icon = app.icon;\n\n // Ô KHOÁ không tô `opacity-60` nữa. Nó KHÔNG phải control vô hiệu — bấm được và gọi\n // `onUpsell(appId)` — nên không hưởng miễn trừ \"thành phần vô hiệu\" của WCAG 1.4.3/1.4.11.\n // Với 60% độ mờ thì mô tả app tụt còn 2,41:1 (light) / 2,74:1 (dark) ✗ và icon trong ô\n // `bg-muted` còn 2,31:1 / 2,60:1 ✗, chỉ phục hồi khi hover — tức người không rê chuột thì\n // không đọc được. Bỏ độ mờ: mô tả `text-muted-foreground` 5,23:1 (light) / 5,35:1 (dark) ✓.\n // Tín hiệu \"chưa mở khoá\" không mất gì: huy hiệu ổ khoá, `title`/`aria-label` nói rõ (ô icon\n // giữ màu app — xem chú thích ở `IconAvatar` bên dưới).\n //\n // Ô LƯỚI là hình VUÔNG, icon + tên căn GIỮA. Bản trước là `items-start` + `text-left`: ô kéo\n // theo bề rộng cột (~109px ở bản 22rem) nhưng chỉ cao bằng nội dung, nên ra hình chữ nhật nằm ngang với\n // khoảng trống thừa bên phải icon và tên — mắt đọc ra một ô \"chưa điền hết\". `aspect-square`\n // là SÀN chứ không phải trần: `min-height: auto` mặc định để ô cao thêm khi app truyền\n // `description` dài, và `auto-rows-fr` của lưới giữ cả hàng cao đều nhau.\n // Cạnh ô do bề rộng popover quyết (`w-[21rem] px-4` ở `AppLauncher`, ba cột, `gap-2`): 336 − 32\n // padding − 16 khe = 288 → ô 96px; `p-1.5` để lại 84px cho tên — \"Khách hàng\" (79,7px ở `text-sm`)\n // nằm một dòng. Bản 22rem `p-2` `gap-1` cho ô 109px mà nội dung (icon 36px + một dòng tên) chỉ cao\n // ~62px — gần nửa ô trống; bản 19rem kế đó vừa ô nhưng menu hẹp, khe 4px làm các ô dính nhau và\n // mép menu sát ô. Ba số (rộng · padding · khe) đi CÙNG nhau: nới một số là ô co lại, tên xuống dòng.\n const tileClass = cn(\n \"group relative flex rounded-md transition-colors duration-150 ease-out\",\n variant === \"grid\"\n ? \"aspect-square flex-col items-center justify-center gap-1.5 p-1.5 text-center\"\n : \"items-center gap-2.5 p-2 text-left\",\n isLocked ? \"hover:bg-muted\" : \"hover:bg-accent\",\n isCurrent && \"bg-accent\"\n );\n\n const body = (\n <>\n {/*\n * ── Ô icon: `IconAvatar`, màu TỰ SUY theo `id` ────────────────────────────────────\n * Từ 1.10.0 ô này thôi tô bằng `--app-accent`. Lý do không phải thẩm mỹ: bảng accent cũ\n * bắt mỗi app phải được ai đó gán màu tay, nên sản phẩm thứ sáu hoặc trùng màu với sản\n * phẩm khác, hoặc phải chờ một lần sửa mã. `IconAvatar` suy màu từ `id` bằng bảng tone\n * dùng chung với `LetterAvatar`, nên thêm app là nó tự có màu — và cùng màu ấy ở email,\n * canvas, PDF vì `getAvatarToneColors` đọc cùng bảng.\n *\n * ⚠ `seed={app.id}`, KHÔNG phải `app.name`: tên đổi theo ngôn ngữ giao diện. Đo được\n * 4/6 app đổi màu khi chuyển vi ⇄ en nếu băm theo tên.\n *\n * ⚠ Nhánh KHOÁ giữ ĐÚNG màu của app (từ 3.10.0), tín hiệu duy nhất là huy hiệu ổ khoá.\n * Bản trước tô xám cả ô: ở mục \"Khám phá thêm\" mọi app chưa mở trông y hệt nhau, tức người\n * dùng mất đúng thứ giúp nhận ra sản phẩm ngay ở chỗ cần mời họ dùng thử. \"Chưa mở khoá\"\n * vẫn được nói đủ: ổ khoá, nhãn nhóm, `title`/`aria-label` của ô.\n */}\n <span className=\"relative flex shrink-0\">\n <IconAvatar\n name={app.name}\n seed={app.id}\n icon={Icon}\n tone={app.tone}\n size={variant === \"grid\" ? \"md\" : \"sm\"}\n />\n {isLocked ? (\n <span className=\"absolute -bottom-1 -right-1 flex size-4 items-center justify-center rounded-full bg-muted text-muted-foreground ring-2 ring-popover\">\n <Lock className=\"size-2.5\" />\n </span>\n ) : null}\n </span>\n\n {/*\n * `w-full` ở ô lưới là thứ THẬT SỰ bó được chữ — không phải `min-w-0`.\n *\n * Ô lưới là `flex-col` (xem `tileClass`), nên theo trục ngang cột này lấy\n * `fit-content`: nó RỘNG RA THEO TÊN thay vì bằng ô. Mọi phép cắt bên trong vì thế không\n * bao giờ có gì để bám vào. Đo ở app thật, ô 109px: tên một từ dài cho cột 272px và tràn\n * 174px, `scrollWidth === clientWidth` — `line-clamp` được đặt nhưng chưa từng chạy.\n * Thêm `min-w-0` vào hàng bên dưới KHÔNG đổi được số nào (đo: y hệt bản hỏng); `w-full`\n * đưa cột về đúng 89px và từ đó phép cắt mới có hiệu lực.\n *\n * Biến thể `list` không dính: ô đó là hàng ngang nên `flex-1` đã ghim bề rộng sẵn.\n */}\n <span className={cn(\"flex min-w-0 flex-col\", variant === \"grid\" ? \"w-full\" : \"flex-1\")}>\n {/*\n * `items-start` chứ không `items-center` khi tên có thể xuống hai dòng: căn giữa đẩy\n * badge xuống giữa khe hai dòng (đo được 10,6px), xa hẳn dòng chữ nó đang chú thích.\n * Tên một dòng thì hai cách lệch nhau 0,8px — không nhìn thấy.\n */}\n <span\n className={cn(\"flex min-w-0 gap-1.5\", variant === \"grid\" ? \"items-start justify-center\" : \"items-center\")}\n >\n <span\n className={cn(\n \"text-sm font-medium text-foreground\",\n /*\n * Lưới: tên ứng dụng dài hơn ô là chuyện thường (\"Tài khoản Comitor\" ở ô 109px),\n * nên cho hai dòng rồi mới cắt — `grid-cols-3` khai `items-stretch` mặc định nên\n * ô cao thêm một dòng không làm hàng bị so le.\n *\n * `break-words` đi KÈM `line-clamp-2`, không phải phòng xa: `-webkit-line-clamp`\n * chỉ đặt được dấu \"…\" khi chữ THẬT SỰ tràn sang dòng thứ ba. Một tên không có\n * khoảng trắng thì không xuống dòng được, nên nó nằm gọn một dòng và bị\n * `overflow: hidden` cắt PHẲNG giữa chữ — đo được: `soDong: 1`,\n * `scrollHeight === clientHeight`, không có dấu ba chấm nào. Cho phép ngắt trong\n * từ thì nó thành hai dòng và dấu \"…\" quay lại.\n *\n * Danh sách: mỗi mục là một hàng ngang trong popover hẹp; hai dòng ở đó làm chiều\n * cao các mục nhấp nhô. Giữ một dòng — `truncate` vốn đã chạy đúng ở đó vì `flex-1`\n * ghim sẵn bề rộng. Cùng lý do tác giả chỉ cho `description` xuống hai dòng ở lưới.\n */\n variant === \"grid\" ? \"line-clamp-2 break-words\" : \"truncate\"\n )}\n >\n {app.name}\n </span>\n {app.badge ? (\n /*\n * `shrink-0` KHÔNG phải để chống bóp méo — đo rồi: có hay không, badge đều 116,8px,\n * vì `whitespace-nowrap` trong `Badge` đã giữ sàn `min-width: auto` bằng trọn bề rộng\n * chữ, và phần co dồn hết sang ô tên (`overflow: hidden` → sàn 0).\n *\n * Nó ở đây vì hai lý do khác: khớp quy ước của mọi chỗ gọi `Badge` khác trong shell\n * (`workspace-switcher`, `sidebar-menu-item` — chỗ này là chỗ DUY NHẤT thiếu), và để\n * lá chắn thôi phụ thuộc vào một lớp ngầm trong `Badge`. Ai đó thêm `truncate` vào\n * base cva của `Badge` là sàn tụt về 0 và badge méo, im lặng.\n */\n <Badge variant=\"destructive\" size=\"sm\" className=\"shrink-0\">\n {app.badge}\n </Badge>\n ) : null}\n </span>\n {app.description ? (\n <span\n className={cn(\"text-[11px] leading-snug text-muted-foreground\", variant === \"grid\" && \"line-clamp-2\")}\n >\n {app.description}\n </span>\n ) : null}\n </span>\n </>\n );\n\n // Chưa mở khoá → không điều hướng, chỉ báo cho app biết để mời nâng gói.\n if (isLocked) {\n return (\n <button\n key={app.id}\n type=\"button\"\n data-app-tile=\"\"\n title={labels.lockedHint}\n aria-label={`${app.name} — ${labels.locked}`}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => {\n onUpsell?.(app.id);\n onSelected?.();\n }}\n >\n {body}\n </button>\n );\n }\n\n // Có `onAppSelect` → app tự lo điều hướng (đổi subdomain, nạp trước dữ liệu…);\n // không có → dùng link thật để giữ chuột-giữa / mở tab mới / trình đọc màn hình.\n if (onAppSelect) {\n return (\n <button\n key={app.id}\n type=\"button\"\n data-app-tile=\"\"\n aria-current={isCurrent ? \"page\" : undefined}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => {\n onAppSelect(app);\n onSelected?.();\n }}\n >\n {body}\n </button>\n );\n }\n\n return (\n <ShellNavLink\n key={app.id}\n href={app.href}\n data-app-tile=\"\"\n aria-current={isCurrent ? \"page\" : undefined}\n className={tileClass}\n onKeyDown={handleKeyDown}\n onClick={() => onSelected?.()}\n >\n {body}\n </ShellNavLink>\n );\n };\n\n if (total === 0) return null;\n\n /*\n * `auto-rows-fr` vá thứ duy nhất mà việc cho tên xuống hai dòng làm xấu đi: TRONG một hàng thì\n * các ô đã bằng nhau sẵn (`align-items: stretch` là mặc định của Grid), nhưng GIỮA các hàng thì\n * không — hàng có một tên hai dòng cao 116px trong khi hàng kia 97px. `fr` cho mọi hàng chung\n * một chiều cao.\n */\n const groupClass = variant === \"grid\" ? \"grid auto-rows-fr grid-cols-3 gap-2\" : \"flex flex-col gap-0.5\";\n\n return (\n <div ref={rootRef} className={cn(\"flex flex-col\", variant === \"grid\" ? \"gap-4\" : \"gap-3\", className)}>\n {entitled.length > 0 ? (\n <section>\n <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\">\n {labels.myApps}\n </p>\n <div className={groupClass}>{entitled.map(renderTile)}</div>\n </section>\n ) : null}\n\n {locked.length > 0 ? (\n <section>\n <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground\">\n {labels.discoverApps}\n </p>\n <div className={groupClass}>{locked.map(renderTile)}</div>\n </section>\n ) : null}\n </div>\n );\n}\n\nexport interface AppLauncherProps extends AppLauncherPanelProps {\n /** Bỏ nút bấm + Popover, render thẳng panel (MobileMenu dùng cách này). */\n inline?: boolean;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 2 — chuyển sản phẩm trong cùng workspace.**\n *\n * Nút lưới 3×3 ở header mở popover lưới ô app.\n * Trạng thái mở nằm ở `ShellProvider` để phím tắt `Cmd/Ctrl + Shift + K` bật được.\n * App chưa có trong gói hiện ô khoá và gọi `onUpsell(appId)` thay vì điều hướng.\n */\nexport function AppLauncher({\n className,\n variant = \"grid\",\n inline = false,\n align = \"end\",\n onSelected\n}: AppLauncherProps) {\n const { apps, appLauncherOpen, setAppLauncherOpen, labels } = useShell();\n\n if (apps.length === 0) return null;\n\n if (inline) {\n return <AppLauncherPanel variant={variant} className={className} onSelected={onSelected} />;\n }\n\n return (\n <Popover open={appLauncherOpen} onOpenChange={setAppLauncherOpen}>\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label={labels.appLauncherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\",\n appLauncherOpen && \"bg-muted text-foreground\",\n className\n )}\n >\n <LayoutGrid className=\"size-4\" />\n </button>\n </PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">{labels.apps}</TooltipContent>\n </Tooltip>\n\n <PopoverContent align={align} sideOffset={8} className=\"w-[21rem] px-4 py-3\">\n <AppLauncherPanel\n variant={variant}\n onSelected={() => {\n setAppLauncherOpen(false);\n onSelected?.();\n }}\n />\n </PopoverContent>\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
package/dist/tokens.d.ts CHANGED
@@ -71,6 +71,10 @@ declare const accent: {
71
71
  /** Gốc của tone `severe`. Giữ 3:1 trên mọi nền trang (3,56:1 trắng · 3,32:1 `muted`) — vì nó
72
72
  * TÔ chấm của `StatusPill variant="dot"`; bậc sáng hơn (#F97316) chỉ 2,80:1. */
73
73
  readonly orange: "#EA580C";
74
+ /** Ba sắc ĐỊNH DANH — chỉ bảng tone avatar/icon app đọc (`lib/avatar-tone.ts`). */
75
+ readonly blue: "#4169E1";
76
+ readonly purple: "#8A4FD8";
77
+ readonly pink: "#FF1493";
74
78
  };
75
79
  /** Bảng MẶC ĐỊNH (comitor-ds) không khai lại màu phụ ở dark — cùng hex với light. Bản sáng
76
80
  * dành cho nền tối nằm ở `highContrast.dark`. Trừ `orange`: comitor-ds không có cam, và cùng
@@ -83,6 +87,10 @@ declare const accent: {
83
87
  readonly green: "#2EA56A";
84
88
  readonly yellow: "#F2C94C";
85
89
  readonly orange: "#FB923C";
90
+ /** Ba sắc định danh có bản dark RIÊNG như cam — bản light trên tint /15 hợp lên `card` chỉ 3,1–3,9:1. */
91
+ readonly blue: "#82A8F5";
92
+ readonly purple: "#A78BFA";
93
+ readonly pink: "#FF69B4";
86
94
  };
87
95
  };
88
96
  /**
@@ -112,6 +120,10 @@ declare const accentInk: {
112
120
  /** = color-mix(srgb, orange 72%, ink 28%) — bậc SÁNG nhất còn giữ trần tint /20: 5,85:1 trắng ·
113
121
  * 4,87:1 trên orange/15 · 4,58:1 trên orange/20. Bảng tương phản cao tối hơn: `highContrast`. */
114
122
  readonly orange: "#AC440F";
123
+ /** Ba sắc định danh — trên tint /15 của chính nó: blue 5,67 · purple 5,72 · pink 5,33:1. */
124
+ readonly blue: "#3053B8";
125
+ readonly purple: "#6E3FB0";
126
+ readonly pink: "#B0126A";
115
127
  };
116
128
  readonly dark: {
117
129
  /** navy nguyên bản chìm vào nền dark (1,15:1) → bậc sáng riêng; cũng là `--chart-3` ở dark. */
@@ -121,6 +133,9 @@ declare const accentInk: {
121
133
  readonly green: "#2EA56A";
122
134
  readonly yellow: "#F2C94C";
123
135
  readonly orange: "#FB923C";
136
+ readonly blue: "#82A8F5";
137
+ readonly purple: "#A78BFA";
138
+ readonly pink: "#FF69B4";
124
139
  };
125
140
  };
126
141
  /**
@@ -144,6 +159,10 @@ declare const accentForeground: {
144
159
  readonly green: "#0E1116";
145
160
  readonly yellow: "#0E1116";
146
161
  readonly orange: "#0E1116";
162
+ /** Ba sắc định danh (3.11.0): blue/purple chữ TRẮNG ở light (4,85 · 5,01:1), pink chữ mực (5,20:1). */
163
+ readonly blue: "#FFFFFF";
164
+ readonly purple: "#FFFFFF";
165
+ readonly pink: "#0E1116";
147
166
  };
148
167
  readonly dark: {
149
168
  readonly navy: "#FFFFFF";
@@ -151,6 +170,10 @@ declare const accentForeground: {
151
170
  readonly green: "#0E1116";
152
171
  readonly yellow: "#0E1116";
153
172
  readonly orange: "#0E1116";
173
+ /** Bản dark của ba sắc đều sáng → mực: blue 7,99 · purple 6,95 · pink 7,14:1. */
174
+ readonly blue: "#0E1116";
175
+ readonly purple: "#0E1116";
176
+ readonly pink: "#0E1116";
154
177
  };
155
178
  };
156
179
  /** Semantic token của shadcn — mirror khối `:root` / `.dark` trong styles.css. */
@@ -326,6 +349,10 @@ declare const highContrast: {
326
349
  /** Gốc của tone `severe`. Giữ 3:1 trên mọi nền trang (3,56:1 trắng · 3,32:1 `muted`) — vì nó
327
350
  * TÔ chấm của `StatusPill variant="dot"`; bậc sáng hơn (#F97316) chỉ 2,80:1. */
328
351
  readonly orange: "#EA580C";
352
+ /** Ba sắc ĐỊNH DANH — chỉ bảng tone avatar/icon app đọc (`lib/avatar-tone.ts`). */
353
+ readonly blue: "#4169E1";
354
+ readonly purple: "#8A4FD8";
355
+ readonly pink: "#FF1493";
329
356
  };
330
357
  };
331
358
  readonly dark: {
package/dist/tokens.js CHANGED
@@ -32,7 +32,11 @@ const accent = {
32
32
  yellow: "#F2C94C",
33
33
  /** Gốc của tone `severe`. Giữ 3:1 trên mọi nền trang (3,56:1 trắng · 3,32:1 `muted`) — vì nó
34
34
  * TÔ chấm của `StatusPill variant="dot"`; bậc sáng hơn (#F97316) chỉ 2,80:1. */
35
- orange: "#EA580C"
35
+ orange: "#EA580C",
36
+ /** Ba sắc ĐỊNH DANH — chỉ bảng tone avatar/icon app đọc (`lib/avatar-tone.ts`). */
37
+ blue: "#4169E1",
38
+ purple: "#8A4FD8",
39
+ pink: "#FF1493"
36
40
  },
37
41
  /** Bảng MẶC ĐỊNH (comitor-ds) không khai lại màu phụ ở dark — cùng hex với light. Bản sáng
38
42
  * dành cho nền tối nằm ở `highContrast.dark`. Trừ `orange`: comitor-ds không có cam, và cùng
@@ -44,7 +48,11 @@ const accent = {
44
48
  red: "#D64545",
45
49
  green: "#2EA56A",
46
50
  yellow: "#F2C94C",
47
- orange: "#FB923C"
51
+ orange: "#FB923C",
52
+ /** Ba sắc định danh có bản dark RIÊNG như cam — bản light trên tint /15 hợp lên `card` chỉ 3,1–3,9:1. */
53
+ blue: "#82A8F5",
54
+ purple: "#A78BFA",
55
+ pink: "#FF69B4"
48
56
  }
49
57
  };
50
58
  const accentInk = {
@@ -57,7 +65,11 @@ const accentInk = {
57
65
  yellow: gold[500],
58
66
  /** = color-mix(srgb, orange 72%, ink 28%) — bậc SÁNG nhất còn giữ trần tint /20: 5,85:1 trắng ·
59
67
  * 4,87:1 trên orange/15 · 4,58:1 trên orange/20. Bảng tương phản cao tối hơn: `highContrast`. */
60
- orange: "#AC440F"
68
+ orange: "#AC440F",
69
+ /** Ba sắc định danh — trên tint /15 của chính nó: blue 5,67 · purple 5,72 · pink 5,33:1. */
70
+ blue: "#3053B8",
71
+ purple: "#6E3FB0",
72
+ pink: "#B0126A"
61
73
  },
62
74
  dark: {
63
75
  /** navy nguyên bản chìm vào nền dark (1,15:1) → bậc sáng riêng; cũng là `--chart-3` ở dark. */
@@ -66,7 +78,10 @@ const accentInk = {
66
78
  red: accent.dark.red,
67
79
  green: accent.dark.green,
68
80
  yellow: accent.dark.yellow,
69
- orange: accent.dark.orange
81
+ orange: accent.dark.orange,
82
+ blue: accent.dark.blue,
83
+ purple: accent.dark.purple,
84
+ pink: accent.dark.pink
70
85
  }
71
86
  };
72
87
  const accentForeground = {
@@ -75,14 +90,22 @@ const accentForeground = {
75
90
  teal: "#0E1116",
76
91
  green: "#0E1116",
77
92
  yellow: "#0E1116",
78
- orange: "#0E1116"
93
+ orange: "#0E1116",
94
+ /** Ba sắc định danh (3.11.0): blue/purple chữ TRẮNG ở light (4,85 · 5,01:1), pink chữ mực (5,20:1). */
95
+ blue: "#FFFFFF",
96
+ purple: "#FFFFFF",
97
+ pink: "#0E1116"
79
98
  },
80
99
  dark: {
81
100
  navy: "#FFFFFF",
82
101
  teal: "#0E1116",
83
102
  green: "#0E1116",
84
103
  yellow: "#0E1116",
85
- orange: "#0E1116"
104
+ orange: "#0E1116",
105
+ /** Bản dark của ba sắc đều sáng → mực: blue 7,99 · purple 6,95 · pink 7,14:1. */
106
+ blue: "#0E1116",
107
+ purple: "#0E1116",
108
+ pink: "#0E1116"
86
109
  }
87
110
  };
88
111
  const semantic = {
@@ -315,16 +338,12 @@ const chart = {
315
338
  const avatarTonePalette = {
316
339
  light: {
317
340
  gold,
318
- ash,
319
- brandNeutral,
320
341
  accent: accent.light,
321
342
  accentInk: accentInk.light,
322
343
  card: semantic.light.card
323
344
  },
324
345
  dark: {
325
346
  gold,
326
- ash,
327
- brandNeutral,
328
347
  accent: accent.dark,
329
348
  accentInk: accentInk.dark,
330
349
  card: semantic.dark.card
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tokens.ts"],"sourcesContent":["/**\n * Design token dạng TS object — dành cho nơi KHÔNG có Tailwind: canvas, biểu đồ tuỳ biến,\n * email HTML, xuất PDF, (sau này) React Native.\n *\n * ⚠ Nguồn sự thật của giá trị là `styles.css` (CSS variable). File này chỉ MIRROR hằng số.\n * Sửa màu ở styles.css thì phải sửa kèm ở đây — và ngược lại.\n *\n * Trong môi trường có Tailwind, LUÔN dùng class token (`bg-primary`, `text-ash-500`),\n * đừng đọc hex từ đây rồi nhét vào style inline: làm vậy là mất dark mode.\n *\n * Hợp đồng ba vai trò của màu (giống hệt `styles.css`; bảng đầy đủ ở docs/hop-dong-mau.md),\n * đọc tên là biết dùng ở đâu:\n * `x` → tô NỀN (fill đặc hoặc tint /5–/20 — trần chung an toàn cho mọi tone)\n * `xForeground` → chữ/icon NẰM TRÊN nền `x` ĐẶC — đo với `x`, ngưỡng 4,5:1\n * `xInk` → chính màu đó khi ĐI LÊN nền trang hoặc lên tint của chính nó (chữ, icon\n * rời, viền, vạch active) — đo với nền đó\n *\n * Cố ý KHÔNG mirror (xem docs/quyet-dinh-tich-hop.md §21): `--elevation-*` (chuỗi box-shadow chỉ\n * có nghĩa trong CSS), `--app-accent*` (giá trị thuộc về APP, không phải gói), `--font-*`.\n */\n\nimport { AVATAR_TONES, type AvatarToneColors, type AvatarTonePalette, getAvatarToneIndex } from \"./lib/avatar-tone.js\";\n\n/** Thang GOLD 7 bậc — trục màu chính của brand Comitor. */\nexport const gold = {\n 50: \"#FBF5DC\",\n 100: \"#F8E89D\",\n 200: \"#F5D060\",\n /** primary ở light mode */\n 300: \"#E8B824\",\n 400: \"#B88A00\",\n 500: \"#7A5800\",\n 600: \"#5C4006\"\n} as const;\n\n/** Trung tính brand (khác thang ash — đây là màu \"giấy/mực\" của brand book). */\nexport const brandNeutral = {\n ink: \"#0E1116\",\n inkSoft: \"#3A3F4A\",\n paper: \"#FBF7EE\",\n warm: \"#EDE8D8\"\n} as const;\n\n/**\n * Thang xám Comitor — namespace `ash`, KHÔNG phải `gray`.\n * Chỉ có 6 bậc; các bậc khuyết (200/400/600/800) cố ý không khai để không lẫn với\n * thang gray dựng sẵn của Tailwind (xem chú thích trong styles.css).\n */\nexport const ash = {\n 50: \"#F7F7F8\",\n 100: \"#EDEEF1\",\n 300: \"#C8CBD3\",\n 500: \"#7E8290\",\n 700: \"#3A3F4A\",\n 900: \"#16181D\"\n} as const;\n\n/**\n * Màu phụ — vai trò TÔ NỀN (fill đặc hoặc tint). Bản `dark` được nâng sáng vì trên nền #0E1116\n * bản `light` chìm nghỉm.\n *\n * ⚠ ĐỪNG dùng bảng này làm màu CHỮ ở light: nó được chọn để tô nền nên quá nhạt — dùng\n * `accentInk` ngay dưới. `accent.dark.navy` giữ nguyên #0F1E3D chỉ để mirror đúng `styles.css`\n * (khối `.dark` không override `--navy`); vai trò navy ở dark do `accentInk.dark.navy` gánh.\n */\nexport const accent = {\n light: {\n navy: \"#0F1E3D\",\n teal: \"#1F8A8B\",\n red: \"#D64545\",\n green: \"#2EA56A\",\n yellow: \"#F2C94C\",\n /** Gốc của tone `severe`. Giữ 3:1 trên mọi nền trang (3,56:1 trắng · 3,32:1 `muted`) — vì nó\n * TÔ chấm của `StatusPill variant=\"dot\"`; bậc sáng hơn (#F97316) chỉ 2,80:1. */\n orange: \"#EA580C\"\n },\n /** Bảng MẶC ĐỊNH (comitor-ds) không khai lại màu phụ ở dark — cùng hex với light. Bản sáng\n * dành cho nền tối nằm ở `highContrast.dark`. Trừ `orange`: comitor-ds không có cam, và cùng\n * hex thì chữ cam trên tint /15 của chính nó ở dark chỉ 4,21:1 — nên bảng mặc định có bản\n * dark riêng. */\n dark: {\n navy: \"#0F1E3D\",\n teal: \"#1F8A8B\",\n red: \"#D64545\",\n green: \"#2EA56A\",\n yellow: \"#F2C94C\",\n orange: \"#FB923C\"\n }\n} as const;\n\n/**\n * Bậc INK của màu phụ — mirror `--teal-ink` / `--green-ink` / `--red-ink` / `--yellow-ink` /\n * `--orange-ink` / `--navy-ink`. Đây là màu phụ khi nó làm **CHỮ/ICON/VIỀN trên nền trang**\n * (hoặc trên tint /5–/20 của chính nó), tức vai trò NGƯỢC với `accent`.\n *\n * Hợp đồng đặt tên của cả gói: `--x` tô nền · `--x-foreground` là chữ NẰM TRÊN nền `--x` ·\n * `--x-ink` là chính màu đó ĐI LÊN nền trang.\n *\n * Bản `light` là bậc tối đã đóng băng (kết quả của `color-mix(in srgb, --<màu>, --ink 30/40%)`\n * mà trước đây mỗi component tự tính lấy); bản `dark` chính là `accent.dark`. Đo trên tint 15%\n * của chính nó — nền thật mà chữ initials avatar và badge tonal đứng lên: teal 5,54 · green 5,60\n * · red 5,83:1 ở light (bản chưa có ink: 3,44 / 2,69 / 3,57:1 ✗).\n *\n * ⚠ Ở bảng MẶC ĐỊNH, `accent.dark` bằng hex của light nên bậc ink ở dark cũng vậy — đúng thứ\n * comitor-ds render. Bậc sáng cho nền tối nằm ở `highContrast.dark`.\n */\nexport const accentInk = {\n light: {\n navy: accent.light.navy,\n teal: \"#1A6668\",\n red: \"#9A3537\",\n green: \"#216A48\",\n /** = gold[500]: vàng không có bậc tối riêng, mượn thang gold cùng sắc. */\n yellow: gold[500],\n /** = color-mix(srgb, orange 72%, ink 28%) — bậc SÁNG nhất còn giữ trần tint /20: 5,85:1 trắng ·\n * 4,87:1 trên orange/15 · 4,58:1 trên orange/20. Bảng tương phản cao tối hơn: `highContrast`. */\n orange: \"#AC440F\"\n },\n dark: {\n /** navy nguyên bản chìm vào nền dark (1,15:1) → bậc sáng riêng; cũng là `--chart-3` ở dark. */\n navy: \"#7EA2E8\",\n teal: accent.dark.teal,\n red: accent.dark.red,\n green: accent.dark.green,\n yellow: accent.dark.yellow,\n orange: accent.dark.orange\n }\n} as const;\n\n/**\n * Bậc FOREGROUND của màu phụ — mirror `--teal-foreground` / `--green-foreground` /\n * `--yellow-foreground` / `--orange-foreground` / `--navy-foreground`. Đây là chữ/icon nằm\n * **TRÊN nền `accent.*` ĐẶC**, tức vai trò thứ ba, khác hẳn `accentInk` (màu đó đi lên nền TRANG).\n *\n * Cả bốn sắc teal/green/yellow/orange đều sáng ở light và còn sáng hơn ở dark → chữ là MỰC ở cả\n * hai theme, một giá trị đủ dùng: teal 4,56:1 (light) / 10,16:1 (dark) · green 6,04 / 10,85 ·\n * yellow 11,92 / 12,35 · orange 5,31 / 8,36. Navy thì ngược lại — chữ TRẮNG, 16,49:1 ở cả hai theme.\n *\n * ⚠ CỐ Ý KHÔNG có `red`: nền đỏ đặc không mang được chữ tốt ở light — trắng trên #D64545 được\n * 4,38:1 và mực 4,32:1, cả hai đều dưới AA. Nền đỏ đặc có chữ phải dùng `status.*.destructive`;\n * ở bảng tương phản cao token đó là #C93B3B và đạt 5,03:1. `accent.*.red` chỉ để tô TINT và làm\n * màu chart. (Cam thì mực cứu được — 5,31:1 — nên `orange` có mặt.)\n */\nexport const accentForeground = {\n light: {\n navy: \"#FFFFFF\",\n teal: \"#0E1116\",\n green: \"#0E1116\",\n yellow: \"#0E1116\",\n orange: \"#0E1116\"\n },\n dark: {\n navy: \"#FFFFFF\",\n teal: \"#0E1116\",\n green: \"#0E1116\",\n yellow: \"#0E1116\",\n orange: \"#0E1116\"\n }\n} as const;\n\n/** Semantic token của shadcn — mirror khối `:root` / `.dark` trong styles.css. */\nexport const semantic = {\n light: {\n background: \"#FFFFFF\",\n foreground: \"#0E1116\",\n card: \"#FFFFFF\",\n cardForeground: \"#0E1116\",\n popover: \"#FFFFFF\",\n popoverForeground: \"#0E1116\",\n primary: \"#E8B824\",\n primaryForeground: \"#0E1116\",\n /** Bậc INK của brand. Ở bảng MẶC ĐỊNH nó BẰNG `primary`: comitor-ds tô gold cho cả những vai\n * trò mà 1.4.11 đòi 3:1 (rãnh Switch BẬT, gạch active của Tabs) — 1,86:1, đã biết và chấp\n * nhận. Bậc tối #7A5800 nằm ở `highContrast.light.primaryInk`. */\n primaryInk: \"#E8B824\",\n secondary: \"#EDE8D8\",\n secondaryForeground: \"#0E1116\",\n muted: \"#F7F7F8\",\n /** Bậc chữ phụ — giá trị của comitor-ds, bằng đúng `ash[500]`. 3,83:1 trên nền trắng và\n * 3,58:1 trên `muted`, tức dưới AA 4,5:1: đã biết và chấp nhận, bản đạt chuẩn nằm ở\n * `highContrast.light.mutedForeground`. */\n mutedForeground: ash[500],\n accent: \"#FBF5DC\",\n accentForeground: \"#0E1116\",\n /** Nền nút xoá. Chữ trắng trên nó được 4,38:1 — thiếu 0,12 so với AA, đã biết và chấp nhận. */\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n /** Nền nút xoá lúc HOVER = `destructive` ở 90% trên nền trắng, đúng thứ `hover:bg-red/90`\n * của comitor-ds render. Ghim hex vì `/90` phụ thuộc nền phía sau. */\n destructiveHover: \"#DA5A5A\",\n /** Chữ báo lỗi trên nền trang. comitor-ds viết `text-destructive` cho vai trò này, tức cùng\n * hex với nền — 4,38:1 trên nền trắng. */\n destructiveInk: \"#D64545\",\n /** RANH GIỚI TRANG TRÍ (vạch chia, viền thẻ nghỉ) — 1,16:1, cố ý: 1.4.11 không đòi tương\n * phản cho thứ không mang thông tin. Ranh giới của CONTROL đi qua `controlEdge`. */\n border: \"#EDEEF1\",\n /** Viền ô nhập — comitor-ds cho nó cùng bậc với `border`. 1,16:1, đã biết và chấp nhận. */\n input: \"#EDEEF1\",\n ring: \"#E8B824\",\n /** Ranh giới của control lúc NGHỈ — ở bảng mặc định bằng đúng `input`. */\n controlEdge: \"#EDEEF1\",\n /** Cùng vai trò nhưng ở HOVER/ACTIVE. comitor-ds dùng `hover:border-gray-300` = `ash[300]`. */\n controlEdgeStrong: ash[300],\n /** Nền control khi BẬT/ĐÃ CHỌN (rãnh Switch bật, ô Checkbox đã chọn, chấm Radio). comitor-ds\n * tô gold: 1,86:1 với nền trang và với núm — đã biết và chấp nhận. */\n controlOn: \"#E8B824\",\n /** Núm Switch / chấm Radio — TRẮNG ở cả hai theme, đúng `bg-white` của comitor-ds. */\n controlKnob: \"#FFFFFF\",\n /** DẤU CHECK của Checkbox và chữ trên ngày ĐANG CHỌN của Calendar — tách khỏi `controlKnob`\n * vì trên nền gold SÁNG thì dấu phải là MỰC (10,19:1), còn ở bảng tương phản cao nền là\n * gold-500 TỐI nên dấu lật thành màu nền trang. */\n controlCheck: brandNeutral.ink,\n /** Rãnh Switch khi TẮT — comitor-ds dùng `bg-gray-300` / `dark:bg-gray-700`. */\n switchTrack: ash[300],\n /** Rãnh Slider — comitor-ds dùng `bg-muted`, khác bậc với rãnh Switch. */\n sliderTrack: \"#F7F7F8\",\n /** Button variant `link`. comitor-ds dùng `gold[400]` — 3,15:1, dưới AA, đã biết và chấp nhận. */\n linkInk: gold[400],\n linkInkHover: gold[300],\n sidebar: \"#FAFAF9\",\n sidebarForeground: \"#0E1116\",\n sidebarBorder: \"#EDEEF1\",\n sidebarAccent: \"#FBF5DC\",\n sidebarAccentForeground: \"#0E1116\"\n },\n dark: {\n background: \"#0E1116\",\n foreground: \"#F7F7F8\",\n card: \"#16181D\",\n cardForeground: \"#F7F7F8\",\n popover: \"#16181D\",\n popoverForeground: \"#F7F7F8\",\n primary: \"#F5D060\",\n primaryForeground: \"#0E1116\",\n /** Ở dark `primary` vốn đã là bậc sáng (12,67:1 trên nền `background`) nên ink = chính nó. */\n primaryInk: \"#F5D060\",\n secondary: \"#1E2128\",\n secondaryForeground: \"#F7F7F8\",\n muted: \"#1E2128\",\n /** comitor-ds không khai lại token này ở dark — cùng hex với light. 4,21:1 trên `muted`,\n * dưới AA: đã biết và chấp nhận. */\n mutedForeground: ash[500],\n accent: \"#1E2128\",\n accentForeground: \"#F7F7F8\",\n /** Ghim đúng giá trị của light — comitor-ds dùng chung một hex cho cả hai theme. */\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n destructiveHover: \"#DA5A5A\",\n /** 4,06:1 trên `card` — cùng hex với nền, đúng `text-destructive` của comitor-ds. */\n destructiveInk: \"#D64545\",\n /** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */\n border: \"#1E2128\",\n /** Viền ô nhập bản dark — KHÔNG phải #1E2128 của comitor-ds (chỉ 1,10:1 trên `card`, ô nhập tan\n * vào nền). #434A5A = 2,00:1, mirror `.dark { --input }` từ `0.5.0`; trước `3.3.3` mirror này còn\n * ghi #1E2128. */\n input: \"#434A5A\",\n ring: \"#F5D060\",\n /** = `input` (`--control-edge: var(--input)`). */\n controlEdge: \"#434A5A\",\n /** comitor-ds dùng `dark:hover:border-gray-500` = `ash[500]` (lật chiều so với light). */\n controlEdgeStrong: ash[500],\n /** comitor-ds tô `dark:data-[state=checked]:bg-gold-300`, nhưng token `primary` của chính nó\n * ở dark là #F5D060; gói theo token, nên control BẬT ở dark là bậc sáng. */\n controlOn: \"#F5D060\",\n /** TRẮNG ở cả hai theme, đúng `bg-white` của comitor-ds. */\n controlKnob: \"#FFFFFF\",\n /** comitor-ds vẽ dấu check bằng `text-ink` ở CẢ hai theme. */\n controlCheck: brandNeutral.ink,\n switchTrack: ash[700],\n sliderTrack: \"#1E2128\",\n linkInk: gold[400],\n linkInkHover: gold[300],\n sidebar: \"#16181D\",\n sidebarForeground: \"#F7F7F8\",\n sidebarBorder: \"#1E2128\",\n sidebarAccent: \"#1E2128\",\n sidebarAccentForeground: \"#F7F7F8\"\n }\n} as const;\n\n/**\n * BẢNG MÀU THỨ HAI — TƯƠNG PHẢN CAO. Mirror khối `[data-contrast=\"high\"]` /\n * `.dark[data-contrast=\"high\"]` trong styles.css.\n *\n * Chỉ chứa những token KHÁC bảng mặc định, đúng như CSS: hợp lên trên để có bảng đầy đủ.\n *\n * ```ts\n * const mau = { ...semantic.light, ...highContrast.light };\n * ```\n *\n * Bảng mặc định là comitor-ds (bản duyệt, có những cặp dưới ngưỡng — đã liệt kê ở từng token).\n * Bảng này là bảng đã đo, do NGƯỜI DÙNG bật qua `<ContrastProvider>` + `<ContrastToggle>`.\n * `accent` là màu phụ bản sáng dành cho nền tối — bảng mặc định không có, xem `accent.dark`.\n */\nexport const highContrast = {\n light: {\n /** 5,23:1 trên `background` · 4,89:1 trên `muted` (bảng mặc định: 3,83 / 3,58) */\n mutedForeground: \"#686C7A\",\n /** Chữ trắng trên nền này được 5,03:1 (bảng mặc định #D64545 chỉ 4,38:1) */\n destructive: \"#C93B3B\",\n /** = color-mix(destructive, ink 15%) — ĐẬM thêm khi hover, ngược chiều bảng mặc định. */\n destructiveHover: \"#AD3535\",\n /** 7,15:1 trên nền trắng (bảng mặc định dùng chính #D64545, 4,38:1) */\n destructiveInk: accentInk.light.red,\n /** 3,83:1 trên nền trắng (bảng mặc định #EDEEF1 chỉ 1,16:1) */\n input: \"#7E8290\",\n controlEdge: \"#7E8290\",\n controlEdgeStrong: ash[700],\n /** #B48700 = gold-400 tối đi 2%: 3,28:1 trên `background`/`card` · 3,06:1 trên `muted` · ⇄ núm\n * trắng 3,28:1 (bảng mặc định: 1,86 / 1,86). Trước `3.3.3` mirror này còn ghi gold-500 — giá trị\n * `styles.css` đã bỏ. */\n controlOn: \"#B48700\",\n /** = `background`, nên luôn tương phản đúng chiều với cả rãnh BẬT lẫn rãnh TẮT. */\n controlKnob: \"#FFFFFF\",\n /** MỰC trên nền gold (5,77:1) — cùng cách vẽ với bảng mặc định; trước `3.3.3` mirror này ghi trắng. */\n controlCheck: brandNeutral.ink,\n switchTrack: \"#7E8290\",\n sliderTrack: \"#7E8290\",\n primaryInk: gold[500],\n linkInk: gold[500],\n linkInkHover: gold[600],\n /** = `--orange-ink` của khối này — bậc ink màu phụ DUY NHẤT mà bảng này đổi ở light:\n * color-mix(srgb, orange 65%, ink 35%), 6,66:1 trắng · 5,55:1 trên orange/15 (bảng mặc định\n * #AC440F: 5,85 / 4,87:1). */\n severeInk: \"#9D3F10\",\n accent: accent.light\n },\n dark: {\n /** 5,70:1 trên `background` · 5,35:1 trên `card` · 4,86:1 trên `muted` */\n mutedForeground: \"#888D9B\",\n destructive: \"#C93B3B\",\n destructiveHover: \"#AD3535\",\n destructiveInk: \"#F87171\",\n /** Ghim một giá trị cho cả hai theme — 4,94:1 trên `background` */\n input: \"#7E8290\",\n controlEdge: \"#7E8290\",\n controlEdgeStrong: ash[300],\n /** 12,67:1 với nền trang VÀ với `controlKnob` (#0E1116) */\n controlOn: \"#F5D060\",\n controlKnob: \"#0E1116\",\n controlCheck: \"#0E1116\",\n switchTrack: \"#7E8290\",\n sliderTrack: \"#7E8290\",\n primaryInk: \"#F5D060\",\n linkInk: gold[200],\n linkInkHover: gold[100],\n /** Bằng bảng mặc định (8,36:1 trên `background` · 6,09:1 trên orange/15) — khai cho đủ cặp\n * với `light`, vì CSS phải huỷ bậc tối của khối light ở đây. */\n severeInk: accent.dark.orange,\n /** Màu phụ bản SÁNG cho nền tối — bảng mặc định không có bản dark cho nhóm này (trừ `orange`,\n * vốn đã sáng ở `accent.dark`). */\n accent: {\n navy: \"#0F1E3D\",\n teal: \"#2DD4BF\",\n red: \"#F87171\",\n green: \"#4ADE80\",\n yellow: \"#FACC15\",\n orange: accent.dark.orange\n }\n }\n} as const;\n\n/**\n * Alias trạng thái — success/warning/severe/info trỏ vào màu phụ, destructive giữ nguyên 2 chế độ.\n * `severe` (cam) là nấc giữa warning và destructive: ưu tiên cao, SLA sắp trễ.\n *\n * Ba lớp KHÁC nhau, đừng gộp:\n * • `destructive` — màu NỀN (nút xoá): #D64545 ở bảng mặc định (chữ trắng 4,38:1, dưới AA và đã\n * chấp nhận), #C93B3B ở bảng tương phản cao (5,03:1 ✓);\n * • `destructiveForeground` (#FFFFFF) — chữ NẰM TRÊN nền đó;\n * • `destructiveInk` — màu CHỮ đỏ trên nền trang (dòng lỗi). Bảng mặc định cho nó cùng hex với\n * nền (đúng `text-destructive` của comitor-ds); bảng tương phản cao tách thành bậc tối riêng.\n *\n * `successForeground` / `warningForeground` / `infoForeground` là MỰC ở CẢ hai theme (không phải\n * trắng ở light rồi lật ở dark như bản 0.1.0): ba nền trạng thái đều là sắc sáng.\n * success mực 6,04:1 (light) · 10,85:1 (dark) [trắng chỉ 3,13 / 1,74:1 ✗]\n * info mực 4,56:1 (light) · 10,16:1 (dark) [trắng chỉ 4,14 / 1,86:1 ✗]\n * warning mực 11,92:1 (light) · 12,35:1 (dark) [trắng chỉ 1,59 / 1,53:1 ✗]\n * severe mực 5,31:1 (light) · 8,36:1 (dark) [trắng chỉ 3,56 / 2,26:1 ✗]\n * ⚠ `info` ở light là cặp chặt nhất gói (4,56:1, dư 0,06) — đổi `accent.light.teal` là phải đo lại.\n */\nexport const status = {\n light: {\n success: accent.light.green,\n successForeground: \"#0E1116\",\n warning: accent.light.yellow,\n warningForeground: \"#0E1116\",\n severe: accent.light.orange,\n severeForeground: \"#0E1116\",\n info: accent.light.teal,\n infoForeground: \"#0E1116\",\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n successInk: accentInk.light.green,\n warningInk: accentInk.light.yellow,\n severeInk: accentInk.light.orange,\n infoInk: accentInk.light.teal,\n /** = `semantic.light.destructiveInk` (#D64545 — `--destructive-ink: var(--destructive)` ở bảng mặc\n * định). Trước `3.3.3` trỏ nhầm `accentInk.light.red` (#9A3537 — bậc của bảng TƯƠNG PHẢN CAO). */\n destructiveInk: semantic.light.destructiveInk\n },\n dark: {\n success: accent.dark.green,\n successForeground: \"#0E1116\",\n warning: accent.dark.yellow,\n warningForeground: \"#0E1116\",\n severe: accent.dark.orange,\n severeForeground: \"#0E1116\",\n info: accent.dark.teal,\n infoForeground: \"#0E1116\",\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n successInk: accentInk.dark.green,\n warningInk: accentInk.dark.yellow,\n severeInk: accentInk.dark.orange,\n infoInk: accentInk.dark.teal,\n destructiveInk: semantic.dark.destructiveInk\n }\n} as const;\n\n/**\n * Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định.\n *\n * ⚠ `chart-1` ở dark là `gold[300]` như light: `styles.css` KHÔNG khai lại `--chart-1` trong `.dark`\n * (chỉ `--chart-3` tự sáng lên qua `--navy-ink`). Trước `3.3.3` mirror này ghi `gold[200]` — biểu\n * đồ canvas/email/PDF dựng từ `tokens` ra màu khác biểu đồ trên trang.\n */\nexport const chart = {\n light: [gold[300], accent.light.teal, accent.light.navy, accent.light.green, accent.light.red],\n dark: [gold[300], accent.dark.teal, accentInk.dark.navy, accent.dark.green, accent.dark.red]\n} as const;\n\n/**\n * Màu avatar chữ cái (hex đục) cho nơi KHÔNG có Tailwind: canvas, email HTML, PDF.\n *\n * ⚠ KHÔNG phải bảng màu thứ hai. Bảng tone DUY NHẤT của cả gói nằm ở `lib/avatar-tone.ts`, mỗi\n * tone mang cả chuỗi class (cho DOM) lẫn công thức hex tương ứng; ở đây chỉ nạp bảng màu vào công\n * thức đó. Web và email vì vậy dùng chung một thứ tự tone, một hàm băm, một cách chuẩn hoá tên —\n * không còn hai bảng để lệch nhau.\n */\nconst avatarTonePalette = {\n light: {\n gold,\n ash,\n brandNeutral,\n accent: accent.light,\n accentInk: accentInk.light,\n card: semantic.light.card\n },\n dark: {\n gold,\n ash,\n brandNeutral,\n accent: accent.dark,\n accentInk: accentInk.dark,\n card: semantic.dark.card\n }\n} satisfies Record<ThemeMode, AvatarTonePalette>;\n\n/**\n * Cặp nền/chữ (hex) của avatar cho một tên, theo theme.\n *\n * Tên hàm nói đúng thứ nó trả về: `...Colors` = hex (canvas/email/PDF), còn bản trả về chuỗi\n * class Tailwind cho DOM là `getAvatarToneClasses` ở `@comitor/ui`. Cùng một tên người luôn ra\n * cùng một tone ở cả hai đường vì chỉ số do MỘT hàm tính (`getAvatarToneIndex`).\n */\nexport function getAvatarToneColors(name: string, mode: ThemeMode = \"light\"): AvatarToneColors {\n // `noUncheckedIndexedAccess` bật → phải có nhánh lùi, dù chỉ số luôn nằm trong [0, 8).\n const tone = AVATAR_TONES[getAvatarToneIndex(name)] ?? AVATAR_TONES[0];\n return tone[mode](avatarTonePalette[mode]);\n}\n\nexport type { AvatarToneColors };\nexport { getAvatarToneIndex };\n\n/** Số đo khung ứng dụng — mirror --sidebar-width / --sidebar-width-icon / --header-height. */\nexport const layout = {\n /** px — --header-height 3.5rem */\n headerHeight: 56,\n /** px — --sidebar-width 16rem */\n sidebarWidth: 256,\n /** px — --sidebar-width-icon 3.5rem */\n sidebarCollapsedWidth: 56,\n /** px — --radius 0.375rem */\n radius: 6,\n breakpoints: { mobile: 768, tablet: 1024, desktop: 1280 },\n /** px — kích thước tối thiểu của vùng chạm trên mobile (WCAG 2.5.5) */\n minTapTarget: 44\n} as const;\n\n/** Font stack — app phải tự nạp Inter (subsets latin + vietnamese) và gán --font-inter. */\nexport const typography = {\n fontSans: '\"Inter\", system-ui, sans-serif',\n fontMono: '\"Geist Mono\", ui-monospace, monospace'\n} as const;\n\nexport type ThemeMode = \"light\" | \"dark\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/tokens.ts"],"sourcesContent":["/**\n * Design token dạng TS object — dành cho nơi KHÔNG có Tailwind: canvas, biểu đồ tuỳ biến,\n * email HTML, xuất PDF, (sau này) React Native.\n *\n * ⚠ Nguồn sự thật của giá trị là `styles.css` (CSS variable). File này chỉ MIRROR hằng số.\n * Sửa màu ở styles.css thì phải sửa kèm ở đây — và ngược lại.\n *\n * Trong môi trường có Tailwind, LUÔN dùng class token (`bg-primary`, `text-ash-500`),\n * đừng đọc hex từ đây rồi nhét vào style inline: làm vậy là mất dark mode.\n *\n * Hợp đồng ba vai trò của màu (giống hệt `styles.css`; bảng đầy đủ ở docs/hop-dong-mau.md),\n * đọc tên là biết dùng ở đâu:\n * `x` → tô NỀN (fill đặc hoặc tint /5–/20 — trần chung an toàn cho mọi tone)\n * `xForeground` → chữ/icon NẰM TRÊN nền `x` ĐẶC — đo với `x`, ngưỡng 4,5:1\n * `xInk` → chính màu đó khi ĐI LÊN nền trang hoặc lên tint của chính nó (chữ, icon\n * rời, viền, vạch active) — đo với nền đó\n *\n * Cố ý KHÔNG mirror (xem docs/quyet-dinh-tich-hop.md §21): `--elevation-*` (chuỗi box-shadow chỉ\n * có nghĩa trong CSS), `--app-accent*` (giá trị thuộc về APP, không phải gói), `--font-*`.\n */\n\nimport { AVATAR_TONES, type AvatarToneColors, type AvatarTonePalette, getAvatarToneIndex } from \"./lib/avatar-tone.js\";\n\n/** Thang GOLD 7 bậc — trục màu chính của brand Comitor. */\nexport const gold = {\n 50: \"#FBF5DC\",\n 100: \"#F8E89D\",\n 200: \"#F5D060\",\n /** primary ở light mode */\n 300: \"#E8B824\",\n 400: \"#B88A00\",\n 500: \"#7A5800\",\n 600: \"#5C4006\"\n} as const;\n\n/** Trung tính brand (khác thang ash — đây là màu \"giấy/mực\" của brand book). */\nexport const brandNeutral = {\n ink: \"#0E1116\",\n inkSoft: \"#3A3F4A\",\n paper: \"#FBF7EE\",\n warm: \"#EDE8D8\"\n} as const;\n\n/**\n * Thang xám Comitor — namespace `ash`, KHÔNG phải `gray`.\n * Chỉ có 6 bậc; các bậc khuyết (200/400/600/800) cố ý không khai để không lẫn với\n * thang gray dựng sẵn của Tailwind (xem chú thích trong styles.css).\n */\nexport const ash = {\n 50: \"#F7F7F8\",\n 100: \"#EDEEF1\",\n 300: \"#C8CBD3\",\n 500: \"#7E8290\",\n 700: \"#3A3F4A\",\n 900: \"#16181D\"\n} as const;\n\n/**\n * Màu phụ — vai trò TÔ NỀN (fill đặc hoặc tint). Bản `dark` được nâng sáng vì trên nền #0E1116\n * bản `light` chìm nghỉm.\n *\n * ⚠ ĐỪNG dùng bảng này làm màu CHỮ ở light: nó được chọn để tô nền nên quá nhạt — dùng\n * `accentInk` ngay dưới. `accent.dark.navy` giữ nguyên #0F1E3D chỉ để mirror đúng `styles.css`\n * (khối `.dark` không override `--navy`); vai trò navy ở dark do `accentInk.dark.navy` gánh.\n */\nexport const accent = {\n light: {\n navy: \"#0F1E3D\",\n teal: \"#1F8A8B\",\n red: \"#D64545\",\n green: \"#2EA56A\",\n yellow: \"#F2C94C\",\n /** Gốc của tone `severe`. Giữ 3:1 trên mọi nền trang (3,56:1 trắng · 3,32:1 `muted`) — vì nó\n * TÔ chấm của `StatusPill variant=\"dot\"`; bậc sáng hơn (#F97316) chỉ 2,80:1. */\n orange: \"#EA580C\",\n /** Ba sắc ĐỊNH DANH — chỉ bảng tone avatar/icon app đọc (`lib/avatar-tone.ts`). */\n blue: \"#4169E1\",\n purple: \"#8A4FD8\",\n pink: \"#FF1493\"\n },\n /** Bảng MẶC ĐỊNH (comitor-ds) không khai lại màu phụ ở dark — cùng hex với light. Bản sáng\n * dành cho nền tối nằm ở `highContrast.dark`. Trừ `orange`: comitor-ds không có cam, và cùng\n * hex thì chữ cam trên tint /15 của chính nó ở dark chỉ 4,21:1 — nên bảng mặc định có bản\n * dark riêng. */\n dark: {\n navy: \"#0F1E3D\",\n teal: \"#1F8A8B\",\n red: \"#D64545\",\n green: \"#2EA56A\",\n yellow: \"#F2C94C\",\n orange: \"#FB923C\",\n /** Ba sắc định danh có bản dark RIÊNG như cam — bản light trên tint /15 hợp lên `card` chỉ 3,1–3,9:1. */\n blue: \"#82A8F5\",\n purple: \"#A78BFA\",\n pink: \"#FF69B4\"\n }\n} as const;\n\n/**\n * Bậc INK của màu phụ — mirror `--teal-ink` / `--green-ink` / `--red-ink` / `--yellow-ink` /\n * `--orange-ink` / `--navy-ink`. Đây là màu phụ khi nó làm **CHỮ/ICON/VIỀN trên nền trang**\n * (hoặc trên tint /5–/20 của chính nó), tức vai trò NGƯỢC với `accent`.\n *\n * Hợp đồng đặt tên của cả gói: `--x` tô nền · `--x-foreground` là chữ NẰM TRÊN nền `--x` ·\n * `--x-ink` là chính màu đó ĐI LÊN nền trang.\n *\n * Bản `light` là bậc tối đã đóng băng (kết quả của `color-mix(in srgb, --<màu>, --ink 30/40%)`\n * mà trước đây mỗi component tự tính lấy); bản `dark` chính là `accent.dark`. Đo trên tint 15%\n * của chính nó — nền thật mà chữ initials avatar và badge tonal đứng lên: teal 5,54 · green 5,60\n * · red 5,83:1 ở light (bản chưa có ink: 3,44 / 2,69 / 3,57:1 ✗).\n *\n * ⚠ Ở bảng MẶC ĐỊNH, `accent.dark` bằng hex của light nên bậc ink ở dark cũng vậy — đúng thứ\n * comitor-ds render. Bậc sáng cho nền tối nằm ở `highContrast.dark`.\n */\nexport const accentInk = {\n light: {\n navy: accent.light.navy,\n teal: \"#1A6668\",\n red: \"#9A3537\",\n green: \"#216A48\",\n /** = gold[500]: vàng không có bậc tối riêng, mượn thang gold cùng sắc. */\n yellow: gold[500],\n /** = color-mix(srgb, orange 72%, ink 28%) — bậc SÁNG nhất còn giữ trần tint /20: 5,85:1 trắng ·\n * 4,87:1 trên orange/15 · 4,58:1 trên orange/20. Bảng tương phản cao tối hơn: `highContrast`. */\n orange: \"#AC440F\",\n /** Ba sắc định danh — trên tint /15 của chính nó: blue 5,67 · purple 5,72 · pink 5,33:1. */\n blue: \"#3053B8\",\n purple: \"#6E3FB0\",\n pink: \"#B0126A\"\n },\n dark: {\n /** navy nguyên bản chìm vào nền dark (1,15:1) → bậc sáng riêng; cũng là `--chart-3` ở dark. */\n navy: \"#7EA2E8\",\n teal: accent.dark.teal,\n red: accent.dark.red,\n green: accent.dark.green,\n yellow: accent.dark.yellow,\n orange: accent.dark.orange,\n blue: accent.dark.blue,\n purple: accent.dark.purple,\n pink: accent.dark.pink\n }\n} as const;\n\n/**\n * Bậc FOREGROUND của màu phụ — mirror `--teal-foreground` / `--green-foreground` /\n * `--yellow-foreground` / `--orange-foreground` / `--navy-foreground`. Đây là chữ/icon nằm\n * **TRÊN nền `accent.*` ĐẶC**, tức vai trò thứ ba, khác hẳn `accentInk` (màu đó đi lên nền TRANG).\n *\n * Cả bốn sắc teal/green/yellow/orange đều sáng ở light và còn sáng hơn ở dark → chữ là MỰC ở cả\n * hai theme, một giá trị đủ dùng: teal 4,56:1 (light) / 10,16:1 (dark) · green 6,04 / 10,85 ·\n * yellow 11,92 / 12,35 · orange 5,31 / 8,36. Navy thì ngược lại — chữ TRẮNG, 16,49:1 ở cả hai theme.\n *\n * ⚠ CỐ Ý KHÔNG có `red`: nền đỏ đặc không mang được chữ tốt ở light — trắng trên #D64545 được\n * 4,38:1 và mực 4,32:1, cả hai đều dưới AA. Nền đỏ đặc có chữ phải dùng `status.*.destructive`;\n * ở bảng tương phản cao token đó là #C93B3B và đạt 5,03:1. `accent.*.red` chỉ để tô TINT và làm\n * màu chart. (Cam thì mực cứu được — 5,31:1 — nên `orange` có mặt.)\n */\nexport const accentForeground = {\n light: {\n navy: \"#FFFFFF\",\n teal: \"#0E1116\",\n green: \"#0E1116\",\n yellow: \"#0E1116\",\n orange: \"#0E1116\",\n /** Ba sắc định danh (3.11.0): blue/purple chữ TRẮNG ở light (4,85 · 5,01:1), pink chữ mực (5,20:1). */\n blue: \"#FFFFFF\",\n purple: \"#FFFFFF\",\n pink: \"#0E1116\"\n },\n dark: {\n navy: \"#FFFFFF\",\n teal: \"#0E1116\",\n green: \"#0E1116\",\n yellow: \"#0E1116\",\n orange: \"#0E1116\",\n /** Bản dark của ba sắc đều sáng → mực: blue 7,99 · purple 6,95 · pink 7,14:1. */\n blue: \"#0E1116\",\n purple: \"#0E1116\",\n pink: \"#0E1116\"\n }\n} as const;\n\n/** Semantic token của shadcn — mirror khối `:root` / `.dark` trong styles.css. */\nexport const semantic = {\n light: {\n background: \"#FFFFFF\",\n foreground: \"#0E1116\",\n card: \"#FFFFFF\",\n cardForeground: \"#0E1116\",\n popover: \"#FFFFFF\",\n popoverForeground: \"#0E1116\",\n primary: \"#E8B824\",\n primaryForeground: \"#0E1116\",\n /** Bậc INK của brand. Ở bảng MẶC ĐỊNH nó BẰNG `primary`: comitor-ds tô gold cho cả những vai\n * trò mà 1.4.11 đòi 3:1 (rãnh Switch BẬT, gạch active của Tabs) — 1,86:1, đã biết và chấp\n * nhận. Bậc tối #7A5800 nằm ở `highContrast.light.primaryInk`. */\n primaryInk: \"#E8B824\",\n secondary: \"#EDE8D8\",\n secondaryForeground: \"#0E1116\",\n muted: \"#F7F7F8\",\n /** Bậc chữ phụ — giá trị của comitor-ds, bằng đúng `ash[500]`. 3,83:1 trên nền trắng và\n * 3,58:1 trên `muted`, tức dưới AA 4,5:1: đã biết và chấp nhận, bản đạt chuẩn nằm ở\n * `highContrast.light.mutedForeground`. */\n mutedForeground: ash[500],\n accent: \"#FBF5DC\",\n accentForeground: \"#0E1116\",\n /** Nền nút xoá. Chữ trắng trên nó được 4,38:1 — thiếu 0,12 so với AA, đã biết và chấp nhận. */\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n /** Nền nút xoá lúc HOVER = `destructive` ở 90% trên nền trắng, đúng thứ `hover:bg-red/90`\n * của comitor-ds render. Ghim hex vì `/90` phụ thuộc nền phía sau. */\n destructiveHover: \"#DA5A5A\",\n /** Chữ báo lỗi trên nền trang. comitor-ds viết `text-destructive` cho vai trò này, tức cùng\n * hex với nền — 4,38:1 trên nền trắng. */\n destructiveInk: \"#D64545\",\n /** RANH GIỚI TRANG TRÍ (vạch chia, viền thẻ nghỉ) — 1,16:1, cố ý: 1.4.11 không đòi tương\n * phản cho thứ không mang thông tin. Ranh giới của CONTROL đi qua `controlEdge`. */\n border: \"#EDEEF1\",\n /** Viền ô nhập — comitor-ds cho nó cùng bậc với `border`. 1,16:1, đã biết và chấp nhận. */\n input: \"#EDEEF1\",\n ring: \"#E8B824\",\n /** Ranh giới của control lúc NGHỈ — ở bảng mặc định bằng đúng `input`. */\n controlEdge: \"#EDEEF1\",\n /** Cùng vai trò nhưng ở HOVER/ACTIVE. comitor-ds dùng `hover:border-gray-300` = `ash[300]`. */\n controlEdgeStrong: ash[300],\n /** Nền control khi BẬT/ĐÃ CHỌN (rãnh Switch bật, ô Checkbox đã chọn, chấm Radio). comitor-ds\n * tô gold: 1,86:1 với nền trang và với núm — đã biết và chấp nhận. */\n controlOn: \"#E8B824\",\n /** Núm Switch / chấm Radio — TRẮNG ở cả hai theme, đúng `bg-white` của comitor-ds. */\n controlKnob: \"#FFFFFF\",\n /** DẤU CHECK của Checkbox và chữ trên ngày ĐANG CHỌN của Calendar — tách khỏi `controlKnob`\n * vì trên nền gold SÁNG thì dấu phải là MỰC (10,19:1), còn ở bảng tương phản cao nền là\n * gold-500 TỐI nên dấu lật thành màu nền trang. */\n controlCheck: brandNeutral.ink,\n /** Rãnh Switch khi TẮT — comitor-ds dùng `bg-gray-300` / `dark:bg-gray-700`. */\n switchTrack: ash[300],\n /** Rãnh Slider — comitor-ds dùng `bg-muted`, khác bậc với rãnh Switch. */\n sliderTrack: \"#F7F7F8\",\n /** Button variant `link`. comitor-ds dùng `gold[400]` — 3,15:1, dưới AA, đã biết và chấp nhận. */\n linkInk: gold[400],\n linkInkHover: gold[300],\n sidebar: \"#FAFAF9\",\n sidebarForeground: \"#0E1116\",\n sidebarBorder: \"#EDEEF1\",\n sidebarAccent: \"#FBF5DC\",\n sidebarAccentForeground: \"#0E1116\"\n },\n dark: {\n background: \"#0E1116\",\n foreground: \"#F7F7F8\",\n card: \"#16181D\",\n cardForeground: \"#F7F7F8\",\n popover: \"#16181D\",\n popoverForeground: \"#F7F7F8\",\n primary: \"#F5D060\",\n primaryForeground: \"#0E1116\",\n /** Ở dark `primary` vốn đã là bậc sáng (12,67:1 trên nền `background`) nên ink = chính nó. */\n primaryInk: \"#F5D060\",\n secondary: \"#1E2128\",\n secondaryForeground: \"#F7F7F8\",\n muted: \"#1E2128\",\n /** comitor-ds không khai lại token này ở dark — cùng hex với light. 4,21:1 trên `muted`,\n * dưới AA: đã biết và chấp nhận. */\n mutedForeground: ash[500],\n accent: \"#1E2128\",\n accentForeground: \"#F7F7F8\",\n /** Ghim đúng giá trị của light — comitor-ds dùng chung một hex cho cả hai theme. */\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n destructiveHover: \"#DA5A5A\",\n /** 4,06:1 trên `card` — cùng hex với nền, đúng `text-destructive` của comitor-ds. */\n destructiveInk: \"#D64545\",\n /** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */\n border: \"#1E2128\",\n /** Viền ô nhập bản dark — KHÔNG phải #1E2128 của comitor-ds (chỉ 1,10:1 trên `card`, ô nhập tan\n * vào nền). #434A5A = 2,00:1, mirror `.dark { --input }` từ `0.5.0`; trước `3.3.3` mirror này còn\n * ghi #1E2128. */\n input: \"#434A5A\",\n ring: \"#F5D060\",\n /** = `input` (`--control-edge: var(--input)`). */\n controlEdge: \"#434A5A\",\n /** comitor-ds dùng `dark:hover:border-gray-500` = `ash[500]` (lật chiều so với light). */\n controlEdgeStrong: ash[500],\n /** comitor-ds tô `dark:data-[state=checked]:bg-gold-300`, nhưng token `primary` của chính nó\n * ở dark là #F5D060; gói theo token, nên control BẬT ở dark là bậc sáng. */\n controlOn: \"#F5D060\",\n /** TRẮNG ở cả hai theme, đúng `bg-white` của comitor-ds. */\n controlKnob: \"#FFFFFF\",\n /** comitor-ds vẽ dấu check bằng `text-ink` ở CẢ hai theme. */\n controlCheck: brandNeutral.ink,\n switchTrack: ash[700],\n sliderTrack: \"#1E2128\",\n linkInk: gold[400],\n linkInkHover: gold[300],\n sidebar: \"#16181D\",\n sidebarForeground: \"#F7F7F8\",\n sidebarBorder: \"#1E2128\",\n sidebarAccent: \"#1E2128\",\n sidebarAccentForeground: \"#F7F7F8\"\n }\n} as const;\n\n/**\n * BẢNG MÀU THỨ HAI — TƯƠNG PHẢN CAO. Mirror khối `[data-contrast=\"high\"]` /\n * `.dark[data-contrast=\"high\"]` trong styles.css.\n *\n * Chỉ chứa những token KHÁC bảng mặc định, đúng như CSS: hợp lên trên để có bảng đầy đủ.\n *\n * ```ts\n * const mau = { ...semantic.light, ...highContrast.light };\n * ```\n *\n * Bảng mặc định là comitor-ds (bản duyệt, có những cặp dưới ngưỡng — đã liệt kê ở từng token).\n * Bảng này là bảng đã đo, do NGƯỜI DÙNG bật qua `<ContrastProvider>` + `<ContrastToggle>`.\n * `accent` là màu phụ bản sáng dành cho nền tối — bảng mặc định không có, xem `accent.dark`.\n */\nexport const highContrast = {\n light: {\n /** 5,23:1 trên `background` · 4,89:1 trên `muted` (bảng mặc định: 3,83 / 3,58) */\n mutedForeground: \"#686C7A\",\n /** Chữ trắng trên nền này được 5,03:1 (bảng mặc định #D64545 chỉ 4,38:1) */\n destructive: \"#C93B3B\",\n /** = color-mix(destructive, ink 15%) — ĐẬM thêm khi hover, ngược chiều bảng mặc định. */\n destructiveHover: \"#AD3535\",\n /** 7,15:1 trên nền trắng (bảng mặc định dùng chính #D64545, 4,38:1) */\n destructiveInk: accentInk.light.red,\n /** 3,83:1 trên nền trắng (bảng mặc định #EDEEF1 chỉ 1,16:1) */\n input: \"#7E8290\",\n controlEdge: \"#7E8290\",\n controlEdgeStrong: ash[700],\n /** #B48700 = gold-400 tối đi 2%: 3,28:1 trên `background`/`card` · 3,06:1 trên `muted` · ⇄ núm\n * trắng 3,28:1 (bảng mặc định: 1,86 / 1,86). Trước `3.3.3` mirror này còn ghi gold-500 — giá trị\n * `styles.css` đã bỏ. */\n controlOn: \"#B48700\",\n /** = `background`, nên luôn tương phản đúng chiều với cả rãnh BẬT lẫn rãnh TẮT. */\n controlKnob: \"#FFFFFF\",\n /** MỰC trên nền gold (5,77:1) — cùng cách vẽ với bảng mặc định; trước `3.3.3` mirror này ghi trắng. */\n controlCheck: brandNeutral.ink,\n switchTrack: \"#7E8290\",\n sliderTrack: \"#7E8290\",\n primaryInk: gold[500],\n linkInk: gold[500],\n linkInkHover: gold[600],\n /** = `--orange-ink` của khối này — bậc ink màu phụ DUY NHẤT mà bảng này đổi ở light:\n * color-mix(srgb, orange 65%, ink 35%), 6,66:1 trắng · 5,55:1 trên orange/15 (bảng mặc định\n * #AC440F: 5,85 / 4,87:1). */\n severeInk: \"#9D3F10\",\n accent: accent.light\n },\n dark: {\n /** 5,70:1 trên `background` · 5,35:1 trên `card` · 4,86:1 trên `muted` */\n mutedForeground: \"#888D9B\",\n destructive: \"#C93B3B\",\n destructiveHover: \"#AD3535\",\n destructiveInk: \"#F87171\",\n /** Ghim một giá trị cho cả hai theme — 4,94:1 trên `background` */\n input: \"#7E8290\",\n controlEdge: \"#7E8290\",\n controlEdgeStrong: ash[300],\n /** 12,67:1 với nền trang VÀ với `controlKnob` (#0E1116) */\n controlOn: \"#F5D060\",\n controlKnob: \"#0E1116\",\n controlCheck: \"#0E1116\",\n switchTrack: \"#7E8290\",\n sliderTrack: \"#7E8290\",\n primaryInk: \"#F5D060\",\n linkInk: gold[200],\n linkInkHover: gold[100],\n /** Bằng bảng mặc định (8,36:1 trên `background` · 6,09:1 trên orange/15) — khai cho đủ cặp\n * với `light`, vì CSS phải huỷ bậc tối của khối light ở đây. */\n severeInk: accent.dark.orange,\n /** Màu phụ bản SÁNG cho nền tối — bảng mặc định không có bản dark cho nhóm này (trừ `orange`,\n * vốn đã sáng ở `accent.dark`). */\n accent: {\n navy: \"#0F1E3D\",\n teal: \"#2DD4BF\",\n red: \"#F87171\",\n green: \"#4ADE80\",\n yellow: \"#FACC15\",\n orange: accent.dark.orange\n }\n }\n} as const;\n\n/**\n * Alias trạng thái — success/warning/severe/info trỏ vào màu phụ, destructive giữ nguyên 2 chế độ.\n * `severe` (cam) là nấc giữa warning và destructive: ưu tiên cao, SLA sắp trễ.\n *\n * Ba lớp KHÁC nhau, đừng gộp:\n * • `destructive` — màu NỀN (nút xoá): #D64545 ở bảng mặc định (chữ trắng 4,38:1, dưới AA và đã\n * chấp nhận), #C93B3B ở bảng tương phản cao (5,03:1 ✓);\n * • `destructiveForeground` (#FFFFFF) — chữ NẰM TRÊN nền đó;\n * • `destructiveInk` — màu CHỮ đỏ trên nền trang (dòng lỗi). Bảng mặc định cho nó cùng hex với\n * nền (đúng `text-destructive` của comitor-ds); bảng tương phản cao tách thành bậc tối riêng.\n *\n * `successForeground` / `warningForeground` / `infoForeground` là MỰC ở CẢ hai theme (không phải\n * trắng ở light rồi lật ở dark như bản 0.1.0): ba nền trạng thái đều là sắc sáng.\n * success mực 6,04:1 (light) · 10,85:1 (dark) [trắng chỉ 3,13 / 1,74:1 ✗]\n * info mực 4,56:1 (light) · 10,16:1 (dark) [trắng chỉ 4,14 / 1,86:1 ✗]\n * warning mực 11,92:1 (light) · 12,35:1 (dark) [trắng chỉ 1,59 / 1,53:1 ✗]\n * severe mực 5,31:1 (light) · 8,36:1 (dark) [trắng chỉ 3,56 / 2,26:1 ✗]\n * ⚠ `info` ở light là cặp chặt nhất gói (4,56:1, dư 0,06) — đổi `accent.light.teal` là phải đo lại.\n */\nexport const status = {\n light: {\n success: accent.light.green,\n successForeground: \"#0E1116\",\n warning: accent.light.yellow,\n warningForeground: \"#0E1116\",\n severe: accent.light.orange,\n severeForeground: \"#0E1116\",\n info: accent.light.teal,\n infoForeground: \"#0E1116\",\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n successInk: accentInk.light.green,\n warningInk: accentInk.light.yellow,\n severeInk: accentInk.light.orange,\n infoInk: accentInk.light.teal,\n /** = `semantic.light.destructiveInk` (#D64545 — `--destructive-ink: var(--destructive)` ở bảng mặc\n * định). Trước `3.3.3` trỏ nhầm `accentInk.light.red` (#9A3537 — bậc của bảng TƯƠNG PHẢN CAO). */\n destructiveInk: semantic.light.destructiveInk\n },\n dark: {\n success: accent.dark.green,\n successForeground: \"#0E1116\",\n warning: accent.dark.yellow,\n warningForeground: \"#0E1116\",\n severe: accent.dark.orange,\n severeForeground: \"#0E1116\",\n info: accent.dark.teal,\n infoForeground: \"#0E1116\",\n destructive: \"#D64545\",\n destructiveForeground: \"#FFFFFF\",\n successInk: accentInk.dark.green,\n warningInk: accentInk.dark.yellow,\n severeInk: accentInk.dark.orange,\n infoInk: accentInk.dark.teal,\n destructiveInk: semantic.dark.destructiveInk\n }\n} as const;\n\n/**\n * Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định.\n *\n * ⚠ `chart-1` ở dark là `gold[300]` như light: `styles.css` KHÔNG khai lại `--chart-1` trong `.dark`\n * (chỉ `--chart-3` tự sáng lên qua `--navy-ink`). Trước `3.3.3` mirror này ghi `gold[200]` — biểu\n * đồ canvas/email/PDF dựng từ `tokens` ra màu khác biểu đồ trên trang.\n */\nexport const chart = {\n light: [gold[300], accent.light.teal, accent.light.navy, accent.light.green, accent.light.red],\n dark: [gold[300], accent.dark.teal, accentInk.dark.navy, accent.dark.green, accent.dark.red]\n} as const;\n\n/**\n * Màu avatar chữ cái (hex đục) cho nơi KHÔNG có Tailwind: canvas, email HTML, PDF.\n *\n * ⚠ KHÔNG phải bảng màu thứ hai. Bảng tone DUY NHẤT của cả gói nằm ở `lib/avatar-tone.ts`, mỗi\n * tone mang cả chuỗi class (cho DOM) lẫn công thức hex tương ứng; ở đây chỉ nạp bảng màu vào công\n * thức đó. Web và email vì vậy dùng chung một thứ tự tone, một hàm băm, một cách chuẩn hoá tên —\n * không còn hai bảng để lệch nhau.\n */\nconst avatarTonePalette = {\n light: {\n gold,\n accent: accent.light,\n accentInk: accentInk.light,\n card: semantic.light.card\n },\n dark: {\n gold,\n accent: accent.dark,\n accentInk: accentInk.dark,\n card: semantic.dark.card\n }\n} satisfies Record<ThemeMode, AvatarTonePalette>;\n\n/**\n * Cặp nền/chữ (hex) của avatar cho một tên, theo theme.\n *\n * Tên hàm nói đúng thứ nó trả về: `...Colors` = hex (canvas/email/PDF), còn bản trả về chuỗi\n * class Tailwind cho DOM là `getAvatarToneClasses` ở `@comitor/ui`. Cùng một tên người luôn ra\n * cùng một tone ở cả hai đường vì chỉ số do MỘT hàm tính (`getAvatarToneIndex`).\n */\nexport function getAvatarToneColors(name: string, mode: ThemeMode = \"light\"): AvatarToneColors {\n // `noUncheckedIndexedAccess` bật → phải có nhánh lùi, dù chỉ số luôn nằm trong [0, 8).\n const tone = AVATAR_TONES[getAvatarToneIndex(name)] ?? AVATAR_TONES[0];\n return tone[mode](avatarTonePalette[mode]);\n}\n\nexport type { AvatarToneColors };\nexport { getAvatarToneIndex };\n\n/** Số đo khung ứng dụng — mirror --sidebar-width / --sidebar-width-icon / --header-height. */\nexport const layout = {\n /** px — --header-height 3.5rem */\n headerHeight: 56,\n /** px — --sidebar-width 16rem */\n sidebarWidth: 256,\n /** px — --sidebar-width-icon 3.5rem */\n sidebarCollapsedWidth: 56,\n /** px — --radius 0.375rem */\n radius: 6,\n breakpoints: { mobile: 768, tablet: 1024, desktop: 1280 },\n /** px — kích thước tối thiểu của vùng chạm trên mobile (WCAG 2.5.5) */\n minTapTarget: 44\n} as const;\n\n/** Font stack — app phải tự nạp Inter (subsets latin + vietnamese) và gán --font-inter. */\nexport const typography = {\n fontSans: '\"Inter\", system-ui, sans-serif',\n fontMono: '\"Geist Mono\", ui-monospace, monospace'\n} as const;\n\nexport type ThemeMode = \"light\" | \"dark\";\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.9.0",
3
+ "version": "3.11.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",
package/styles.css CHANGED
@@ -131,6 +131,17 @@
131
131
  status-config.ts): --background 3,56 · --sidebar 3,41 · --muted 3,32 · --sidebar-accent
132
132
  3,25:1 ✓. Sáng hơn một nấc của cùng thang (#F97316) chỉ còn 2,80 / 2,62:1 ✗. */
133
133
  --orange: #ea580c;
134
+ /* Ba sắc ĐỊNH DANH — chỉ cho bảng tone avatar/icon app (`src/lib/avatar-tone.ts`), không có vai trò
135
+ trạng thái nào. Chúng thay navy · ash · neutral trong bảng tone: ba tone cũ đều là xám-xanh ở
136
+ các độ đậm khác nhau (navy #0F1E3D tint /15 ra nền xám nhạt + chữ gần đen), nên ô icon của ba
137
+ app khác nhau trông như cùng một màu. Gốc lấy từ bảng màu web thông dụng: royal blue #4169E1 ·
138
+ một nấc giữa medium purple và blue violet · deep pink #FF1493. comitor-ds không có ba sắc này
139
+ → màu ĐO của gói (như cam). Số đo bậc ink ở khối "Bậc INK" ngay dưới.
140
+ Bậc -foreground (chữ trên nền ĐẶC — accent khung app của module tô nền này) ở khối "Bậc
141
+ FOREGROUND" ngay dưới, từ 3.11.0. */
142
+ --blue: #4169e1;
143
+ --purple: #8a4fd8;
144
+ --pink: #ff1493;
134
145
 
135
146
  /* ─ HỢP ĐỒNG BA VAI TRÒ CỦA MỘT TONE MÀU — đọc tên là biết dùng ở đâu ─
136
147
 
@@ -193,6 +204,17 @@
193
204
  Sáng hơn một nấc (75% = #B3460F) vẫn qua /15 (4,60:1) nhưng tụt ở /20 (4,33:1 ✗) — app viết
194
205
  `bg-severe/20` theo khuôn của warning là hỏng. Bảng tương phản cao kéo tối thêm (khối 5b). */
195
206
  --orange-ink: #ac440f;
207
+ /* Ba sắc định danh: đóng băng color-mix(in srgb, --<màu>, --ink ~30%) rồi chỉnh tay cho ĐỀU sàn
208
+ (≥ 6,7:1 trên nền trắng · ≥ 5,3:1 trên tint /15 · ≥ 4,9:1 trên tint /20) mà vẫn đọc ra SẮC —
209
+ ink tối hơn nữa thì ba tone lại trôi về cùng một màu mực, đúng lỗi vừa sửa:
210
+ blue #3053B8 — trắng 6,87 · --muted 6,25 · --sidebar-accent 6,28 · /15 5,67 · /20 5,27:1 ✓
211
+ purple #6E3FB0 — trắng 7,00 · --muted 6,37 · --sidebar-accent 6,40 · /15 5,72 · /20 5,34:1 ✓
212
+ pink #B0126A — trắng 6,69 · --muted 6,08 · --sidebar-accent 6,11 · /15 5,33 · /20 4,92:1 ✓
213
+ (medium violet red #C71585 của bảng màu web chỉ 4,32:1 trên pink/15 ✗ — nên tối thêm một nấc.)
214
+ Bảng tương phản cao KHÔNG khai lại: ba bậc này đã đứng ngang sàn mà bảng đó kéo cam xuống. */
215
+ --blue-ink: #3053b8;
216
+ --purple-ink: #6e3fb0;
217
+ --pink-ink: #b0126a;
196
218
 
197
219
  /* ─ Bậc FOREGROUND của màu phụ — chữ/icon nằm TRÊN nền --x ĐẶC ─
198
220
  teal/green/yellow/orange đều là sắc SÁNG-VỪA ở light và còn sáng hơn ở dark, nên chữ phải là
@@ -215,6 +237,15 @@
215
237
  --yellow-foreground: #0e1116;
216
238
  --orange-foreground: #0e1116;
217
239
  --navy-foreground: #ffffff;
240
+ /* Ba sắc định danh (3.11.0). Khác teal/green/yellow: blue và purple ở light là sắc VỪA-TỐI nên chữ
241
+ là TRẮNG; bản dark của chúng sáng nên .dark lật sang MỰC. Pink thì mực ở cả hai theme:
242
+ blue trắng 4,85:1 (light) [mực chỉ 3,90 ✗] · mực 7,99:1 (dark #82A8F5) [trắng 2,37 ✗]
243
+ purple trắng 5,01:1 (light) [mực chỉ 3,77 ✗] · mực 6,95:1 (dark #A78BFA) [trắng 2,72 ✗]
244
+ pink mực 5,20:1 (light) [trắng chỉ 3,64 ✗] · mực 7,14:1 (dark #FF69B4) [trắng 2,65 ✗]
245
+ ⚠ Đổi --blue/--purple/--pink (ở :root hoặc .dark) là phải đo lại ba cặp này. */
246
+ --blue-foreground: #ffffff;
247
+ --purple-foreground: #ffffff;
248
+ --pink-foreground: #0e1116;
218
249
 
219
250
  /* ─ Semantic shadcn ─ */
220
251
  --background: #ffffff;
@@ -605,6 +636,23 @@
605
636
  ink gánh vai trò đó (7,40:1 trên --background · 6,95:1 trên --card), và --chart-3 vì trỏ vào
606
637
  --navy-ink nên tự đúng, không cần khai lại ở đây nữa. */
607
638
  --navy-ink: #7ea2e8;
639
+ /* Ba sắc định danh có bản dark RIÊNG (như cam): bản light làm chữ trên tint /15 hợp lên --card
640
+ chỉ 3,1–3,9:1 ✗. Bậc ink trỏ về fill, đúng lối của khối này. Số đo (--background · --card ·
641
+ --muted · /15 · /20 trên --card):
642
+ blue #82A8F5 — 7,99 · 7,50 · 6,80 · 5,76 · 5,19:1 ✓
643
+ purple #A78BFA — 6,95 · 6,53 · 5,92 · 5,17 · 4,69:1 ✓ (Tailwind violet-400)
644
+ pink #FF69B4 — 7,14 · 6,71 · 6,09 · 5,37 · 4,90:1 ✓ (hot pink)
645
+ .dark[data-contrast="high"] không khai lại: khối light của bảng đó không đụng ba bậc này. */
646
+ --blue: #82a8f5;
647
+ --purple: #a78bfa;
648
+ --pink: #ff69b4;
649
+ --blue-ink: var(--blue);
650
+ --purple-ink: var(--purple);
651
+ --pink-ink: var(--pink);
652
+ /* Bản dark của blue/purple sáng → chữ trên nền đặc lật sang MỰC (7,99 · 6,95:1; trắng chỉ 2,37 ·
653
+ 2,72 ✗). Pink vốn là mực ở :root nên không khai lại. */
654
+ --blue-foreground: #0e1116;
655
+ --purple-foreground: #0e1116;
608
656
  /* Accent app tách khỏi --primary-ink (xem :root): ở dark gold-500 chìm vào nền, nên bậc sáng
609
657
  của brand gánh vai trò này. */
610
658
  --app-accent-ink: var(--primary);
@@ -899,12 +947,18 @@
899
947
  --color-green: var(--green);
900
948
  --color-yellow: var(--yellow);
901
949
  --color-orange: var(--orange);
950
+ --color-blue: var(--blue);
951
+ --color-purple: var(--purple);
952
+ --color-pink: var(--pink);
902
953
  --color-navy-ink: var(--navy-ink);
903
954
  --color-teal-ink: var(--teal-ink);
904
955
  --color-red-ink: var(--red-ink);
905
956
  --color-green-ink: var(--green-ink);
906
957
  --color-yellow-ink: var(--yellow-ink);
907
958
  --color-orange-ink: var(--orange-ink);
959
+ --color-blue-ink: var(--blue-ink);
960
+ --color-purple-ink: var(--purple-ink);
961
+ --color-pink-ink: var(--pink-ink);
908
962
  /* Chữ trên nền màu phụ ĐẶC. Không có `red-foreground`: nền --red đặc không mang được chữ ở
909
963
  light (4,38 / 4,32:1) → dùng `bg-destructive text-destructive-foreground`. */
910
964
  --color-navy-foreground: var(--navy-foreground);
@@ -912,6 +966,9 @@
912
966
  --color-green-foreground: var(--green-foreground);
913
967
  --color-yellow-foreground: var(--yellow-foreground);
914
968
  --color-orange-foreground: var(--orange-foreground);
969
+ --color-blue-foreground: var(--blue-foreground);
970
+ --color-purple-foreground: var(--purple-foreground);
971
+ --color-pink-foreground: var(--pink-foreground);
915
972
 
916
973
  /* Trạng thái */
917
974
  --color-success: var(--success);