@comitor/ui 1.7.1 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/components/composite/avatar-group.js +1 -1
- package/dist/components/composite/avatar-group.js.map +1 -1
- package/dist/components/ui/switch.d.ts +12 -2
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/shell/display-axis-controls.d.ts +19 -4
- package/dist/shell/display-axis-controls.js +13 -4
- package/dist/shell/display-axis-controls.js.map +1 -1
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/theme-toggle.d.ts +23 -2
- package/dist/shell/theme-toggle.js +9 -14
- package/dist/shell/theme-toggle.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ component ghép sẵn và khung ứng dụng (shell) kiểu workspace × app.
|
|
|
8
8
|
- Token brand nằm ở **`styles.css`** — đổi token, bump version, mọi app nhận cùng một thay đổi.
|
|
9
9
|
- Server-component-friendly: `"use client"` chỉ nằm ở file thật sự cần.
|
|
10
10
|
|
|
11
|
-
> Trạng thái: `1.
|
|
11
|
+
> Trạng thái: `1.8.0` — 52 file primitive (Tier 1) · 31 file composite (Tier 2) · 25 file shell
|
|
12
12
|
> (Tier 3) · 4 file chart · 5 file uploader. API chốt từ `1.0.0`.
|
|
13
13
|
>
|
|
14
14
|
> Tài liệu đầy đủ: **https://design-system.comitor.ai** —
|
|
@@ -167,7 +167,8 @@ riêng kèm demo, bảng prop và cạm bẫy của nó:
|
|
|
167
167
|
| **Tier 1 — primitive** · 52 file · `@comitor/ui` | shadcn/ui style `new-york`, đã tùy biến token theo brand Comitor | https://design-system.comitor.ai/components |
|
|
168
168
|
| **Tier 2 — composite** · 31 file · `@comitor/ui` | Giá trị thật của gói: pattern lặp ở mọi app, ghép từ nhiều primitive (`DataTable`, `PageHeader`, `Combobox`, `DatePicker`, `StatCard`…) | https://design-system.comitor.ai/composites |
|
|
169
169
|
| **Tier 3 — shell** · 25 file · `@comitor/ui/shell` | Khung ứng dụng workspace × app: sidebar, header, theme, ba trục hiển thị (tương phản · mật độ · cỡ chữ) | https://design-system.comitor.ai/shell |
|
|
170
|
-
| **chart** · 4 file · `@comitor/ui/chart` | Lớp shadcn bọc recharts, cùng ba bản dựng sẵn theo brand | https://design-system.comitor.ai/components/chart |
|
|
170
|
+
| **chart** · 4 file · `@comitor/ui/chart` | Lớp shadcn bọc recharts, cùng ba bản dựng sẵn theo brand. ⚠ Ba wrapper `Comitor*Chart` **mặc định TẮT** hiệu ứng vẽ dần (`isAnimationActive = false`) — bật lại là biểu đồ kẹt ở khung đầu: có trục, có lưới, KHÔNG có dữ liệu. Mặc định đó KHÔNG áp cho `<ChartContainer>` + recharts do app tự lắp | https://design-system.comitor.ai/components/chart |
|
|
171
|
+
| **uploader** · 5 file · `@comitor/ui/uploader` | `FileUpload` (tệp) · `ImageUploadField` (ảnh) · `useFileUpload` — kéo thả, xem trước, tiến độ. Peer optional: `@uppy/core` + `@uppy/xhr-upload` | https://design-system.comitor.ai/components |
|
|
171
172
|
| **form** · `@comitor/ui/form` | Binding shadcn ⇄ react-hook-form | https://design-system.comitor.ai/components/form |
|
|
172
173
|
|
|
173
174
|
**Cơ chế `labels` — đổi ngôn ngữ mà không fork component.** Chuỗi hiển thị mặc định của gói là **tiếng
|
|
@@ -234,8 +235,10 @@ Mỗi bản publish đi qua cùng một chuỗi cổng (`prepublishOnly` nối c
|
|
|
234
235
|
không làm đỏ — nó là bản duyệt và đã được chấp nhận với đúng những con số dưới ngưỡng ở §6; bắt
|
|
235
236
|
nó là bắt một quyết định, và một cổng lúc nào cũng đỏ thì lần sau không ai chạy nữa.
|
|
236
237
|
⚠ **Cổng xanh vì thế KHÔNG có nghĩa bảng mặc định đạt** — đọc khối "BẢNG MÀU MẶC ĐỊNH: N cặp dưới
|
|
237
|
-
ngưỡng" mà nó in ra
|
|
238
|
-
|
|
238
|
+
ngưỡng" mà nó in ra, cùng khối "Bốn lớp nhiều nhất" ngay dưới — **đọc output, đừng đọc con số ở
|
|
239
|
+
đây**: nó đổi theo mỗi lần chạm token. Hình dạng của khoản nợ thì ổn định, và đó mới là thứ đáng
|
|
240
|
+
nhớ: gần ba phần tư nằm ở đúng `text-muted-foreground`, nên "chặn được chúng" nghĩa là đổi MỘT
|
|
241
|
+
token và đổi vẻ ngoài mọi app. Trước `1.6.0` cổng còn không ĐO bảng mặc định lần nào,
|
|
239
242
|
tức nó in ✓ cho một bảng nó chưa từng nhìn.
|
|
240
243
|
Ở bảng tương phản cao thì mọi loại phát hiện đều `exit 1`, kể cả lớp dùng bảng màu mặc định của
|
|
241
244
|
Tailwind và miễn trừ đã thừa; miễn trừ mỗi mục bắt buộc kèm lý do bằng chữ.
|
|
@@ -45,7 +45,7 @@ function AvatarGroup({
|
|
|
45
45
|
{
|
|
46
46
|
type: "button",
|
|
47
47
|
className: cn(
|
|
48
|
-
"rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground",
|
|
48
|
+
"inline-flex rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground",
|
|
49
49
|
surfaceClassName
|
|
50
50
|
),
|
|
51
51
|
children: /* @__PURE__ */ jsx(LetterAvatar, { name: item.name, src: item.src, size, title: void 0 })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/avatar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip.js\";\nimport { LetterAvatar, type LetterAvatarSize } from \"./letter-avatar.js\";\n\nexport interface AvatarGroupItem {\n id: string;\n name: string;\n src?: string | null;\n /** Dòng phụ trong tooltip (vd email, vai trò) */\n caption?: string;\n}\n\nexport interface AvatarGroupProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n items: AvatarGroupItem[];\n /** Số avatar hiện tối đa; phần dư gộp vào ô \"+k\" (mặc định 4) */\n max?: number;\n size?: LetterAvatarSize;\n /** Bấm vào ô \"+k\" — vd mở danh sách đầy đủ */\n onOverflowClick?: () => void;\n /**\n * Nhãn `sr-only` của ô \"+k\" khi ô đó bấm được — mặc định\n * `(count) => \"Xem tất cả \" + count + \" thành viên\"`.\n *\n * Là HÀM chứ không phải chuỗi mẫu vì có số chèn vào giữa câu: trật tự từ quanh con số khác nhau\n * theo ngôn ngữ, nên mọi cách để gói tự ghép chuỗi đều là chỗ chắc chắn dịch sai.\n *\n * Nhận TỔNG số thành viên (`items.length`), không phải phần dư đang bị giấu: mặt nút đã in sẵn\n * \"+k\" là phần dư rồi, còn câu này nói ra HÀNH ĐỘNG — bấm vào là xem hết cả nhóm.\n *\n * Khác `label` ở tầm phủ: `label` đặt tên cho CẢ NHÓM (`aria-label` của `role=\"group\"`), đọc lên\n * khi con trỏ bước vào nhóm; `overflowLabel` đặt tên cho MỘT nút bên trong, đọc lên khi Tab tới\n * đúng ô \"+k\". App đa ngôn ngữ phải dịch cả hai — dịch mỗi `label` thì nhóm mang tên tiếng Anh\n * mà nút cuối vẫn xưng tiếng Việt.\n */\n overflowLabel?: (count: number) => string;\n /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định \"Thành viên\") — xem `overflowLabel` để phân biệt */\n label?: string;\n /**\n * BỀ MẶT mà nhóm đang đứng lên — cho cả lớp nền đục dưới mỗi avatar lẫn màu vòng ngăn cách.\n *\n * Vì sao phải khai: tone avatar là các tint CÓ ALPHA (`bg-teal/15`, `bg-yellow/25`…), mà nhóm\n * thì cố tình cho chúng chồng lên nhau `-space-x-2`. Không có lớp đục ở dưới thì tint của avatar\n * trên hợp thành với MÀU CỦA AVATAR DƯỚI chứ không phải với nền trang — ba khuôn mặt lẫn vào\n * nhau đúng ở chỗ chồng. Đây cũng chính là bề mặt mà `getAvatarToneColors` giả định khi tính hex\n * đục cho canvas/email (`tintOver(…, p.card, …)`), nên mặc định là `--card`.\n *\n * Đặt nhóm lên bề mặt khác thì khai lại cả hai class, vd `\"bg-sidebar ring-sidebar\"`.\n */\n surfaceClassName?: string;\n}\n\n/**\n * Độ chồng theo CỠ, không phải một hằng số cho mọi cỡ.\n *\n * Mẫu Avatar stack ở comitor-ds là `-space-x-2` trên avatar 40px — tức chồng **20%**. Áp thẳng\n * `-space-x-2` cho mọi cỡ thì ở `xs` (24px) thành chồng 33%, và cộng thêm vòng 2px nữa là phần bị\n * che ăn vào đúng chữ cái đầu của initials: \"ĐH\" ở 10px bắt đầu cách mép ~5,5px, trong khi mép che\n * đã tới 10px. Giữ tỉ lệ ~20% thì mọi cỡ đọc được mà mật độ vẫn y như bản duyệt.\n *\n * xs 24px → 4px (17%) · sm 28px → 6px (21%) · md 36px → 8px (22%)\n * lg 44px → 10px (23%) · xl 56px → 12px (21%)\n */\nconst overlapClasses: Record<LetterAvatarSize, string> = {\n xs: \"-space-x-1\",\n sm: \"-space-x-1.5\",\n md: \"-space-x-2\",\n lg: \"-space-x-2.5\",\n xl: \"-space-x-3\"\n};\n\nconst overflowSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-7 text-xs\",\n md: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\"\n};\n\n/**\n * Chồng avatar + ô \"+k\" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).\n *\n * Tooltip hiện tên đầy đủ vì avatar chồng nhau thì initials bị che một phần; không có tooltip\n * thì nhóm avatar chỉ là trang trí, người dùng không biết ai đang ở trong việc.\n *\n * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình\n * — xem chú thích ở trigger bên dưới.\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"sm\",\n onOverflowClick,\n overflowLabel = (count) => `Xem tất cả ${count} thành viên`,\n label = \"Thành viên\",\n surfaceClassName = \"bg-card ring-card\",\n className,\n ...props\n}: AvatarGroupProps) {\n const visible = items.slice(0, Math.max(0, max));\n const overflow = items.length - visible.length;\n\n return (\n <TooltipProvider delayDuration={200}>\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center\", overlapClasses[size], className)}\n {...props}\n >\n {visible.map((item) => (\n <Tooltip key={item.id}>\n <TooltipTrigger asChild>\n {/* Trigger phải FOCUSABLE: `<span>` thường chỉ nhận sự kiện chuột nên tooltip\n (và cả tên thành viên) biến mất với người dùng bàn phím. Nút không có\n onClick là chủ ý — nó chỉ mở tooltip; tên accessible lấy từ `sr-only`\n bên trong LetterAvatar nên không cần aria-label lặp lại. */}\n {/* Viền focus dùng `foreground` chứ không `ring`: `--ring` là gold — #E8B824 chỉ\n 1,86:1 trên nền trang ở light, dưới 3:1 mà WCAG 1.4.11/2.4.11 đòi cho chỉ báo\n focus (ở dark #F5D060 mới đạt 12,67:1, tức hỏng đúng một nửa số người dùng).\n `outline-foreground` đạt 18,91:1 (light) / 17,66:1 (dark) — cùng lối với viền\n focus của dòng trong `DataTable`. */}\n {/* Nền đục nằm DƯỚI tint có alpha của avatar — xem `surfaceClassName`. Vòng\n `ring-2` cùng màu bề mặt chỉ tách hai avatar ra, nó KHÔNG che được phần tint\n chồng lên nhau bên trong vòng. */}\n <button\n type=\"button\"\n className={cn(\n \"rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n surfaceClassName\n )}\n >\n <LetterAvatar name={item.name} src={item.src} size={size} title={undefined} />\n </button>\n </TooltipTrigger>\n <TooltipContent>\n <span className=\"font-medium\">{item.name}</span>\n {/* `opacity-80` ở đây ĐẠT chuẩn và đã đo, không phải chỗ sót: Tooltip là\n `bg-foreground text-background`, nên chữ mờ 80% ra 12,15:1 (light) / 9,85:1\n (dark) — trên xa ngưỡng 4,5:1. Tooltip cũng không tương tác được. */}\n {item.caption && <span className=\"block text-xs opacity-80\">{item.caption}</span>}\n </TooltipContent>\n </Tooltip>\n ))}\n\n {overflow > 0 &&\n (onOverflowClick ? (\n <button\n type=\"button\"\n onClick={onOverflowClick}\n className={cn(\n // `surfaceClassName` đứng TRƯỚC để `bg-muted` ngay sau đè lại phần nền của nó:\n // ô \"+k\" vốn đã đục, ở đây chỉ mượn màu VÒNG của bề mặt.\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 transition-colors hover:bg-accent hover:text-accent-foreground\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n <span className=\"sr-only\">{overflowLabel(items.length)}</span>\n </button>\n ) : (\n <span\n className={cn(\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n </span>\n ))}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";AAuIgB,cAGJ,YAHI;AApIhB,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,iBAAiB,sBAAsB;AACzE,SAAS,oBAA2C;AA4DpD,MAAM,iBAAmD;AAAA,EACvD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,sBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA,gBAAgB,CAAC,UAAU,wBAAc,KAAK;AAAA,EAC9C,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AAC/C,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,SACE,oBAAC,mBAAgB,eAAe,KAC9B;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,eAAe,IAAI,GAAG,SAAS;AAAA,MACjE,GAAG;AAAA,MAEH;AAAA,gBAAQ,IAAI,CAAC,SACZ,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MAarB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,OAAO,QAAW;AAAA;AAAA,UAC9E,GACF;AAAA,UACA,qBAAC,kBACC;AAAA,gCAAC,UAAK,WAAU,eAAe,eAAK,MAAK;AAAA,YAIxC,KAAK,WAAW,oBAAC,UAAK,WAAU,4BAA4B,eAAK,SAAQ;AAAA,aAC5E;AAAA,aA9BY,KAAK,EA+BnB,CACD;AAAA,QAEA,WAAW,MACT,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA,cACF,oBAAC,UAAK,WAAU,WAAW,wBAAc,MAAM,MAAM,GAAE;AAAA;AAAA;AAAA,QACzD,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA;AAAA;AAAA,QACJ;AAAA;AAAA;AAAA,EAEN,GACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/avatar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip.js\";\nimport { LetterAvatar, type LetterAvatarSize } from \"./letter-avatar.js\";\n\nexport interface AvatarGroupItem {\n id: string;\n name: string;\n src?: string | null;\n /** Dòng phụ trong tooltip (vd email, vai trò) */\n caption?: string;\n}\n\nexport interface AvatarGroupProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n items: AvatarGroupItem[];\n /** Số avatar hiện tối đa; phần dư gộp vào ô \"+k\" (mặc định 4) */\n max?: number;\n size?: LetterAvatarSize;\n /** Bấm vào ô \"+k\" — vd mở danh sách đầy đủ */\n onOverflowClick?: () => void;\n /**\n * Nhãn `sr-only` của ô \"+k\" khi ô đó bấm được — mặc định\n * `(count) => \"Xem tất cả \" + count + \" thành viên\"`.\n *\n * Là HÀM chứ không phải chuỗi mẫu vì có số chèn vào giữa câu: trật tự từ quanh con số khác nhau\n * theo ngôn ngữ, nên mọi cách để gói tự ghép chuỗi đều là chỗ chắc chắn dịch sai.\n *\n * Nhận TỔNG số thành viên (`items.length`), không phải phần dư đang bị giấu: mặt nút đã in sẵn\n * \"+k\" là phần dư rồi, còn câu này nói ra HÀNH ĐỘNG — bấm vào là xem hết cả nhóm.\n *\n * Khác `label` ở tầm phủ: `label` đặt tên cho CẢ NHÓM (`aria-label` của `role=\"group\"`), đọc lên\n * khi con trỏ bước vào nhóm; `overflowLabel` đặt tên cho MỘT nút bên trong, đọc lên khi Tab tới\n * đúng ô \"+k\". App đa ngôn ngữ phải dịch cả hai — dịch mỗi `label` thì nhóm mang tên tiếng Anh\n * mà nút cuối vẫn xưng tiếng Việt.\n */\n overflowLabel?: (count: number) => string;\n /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định \"Thành viên\") — xem `overflowLabel` để phân biệt */\n label?: string;\n /**\n * BỀ MẶT mà nhóm đang đứng lên — cho cả lớp nền đục dưới mỗi avatar lẫn màu vòng ngăn cách.\n *\n * Vì sao phải khai: tone avatar là các tint CÓ ALPHA (`bg-teal/15`, `bg-yellow/25`…), mà nhóm\n * thì cố tình cho chúng chồng lên nhau `-space-x-2`. Không có lớp đục ở dưới thì tint của avatar\n * trên hợp thành với MÀU CỦA AVATAR DƯỚI chứ không phải với nền trang — ba khuôn mặt lẫn vào\n * nhau đúng ở chỗ chồng. Đây cũng chính là bề mặt mà `getAvatarToneColors` giả định khi tính hex\n * đục cho canvas/email (`tintOver(…, p.card, …)`), nên mặc định là `--card`.\n *\n * Đặt nhóm lên bề mặt khác thì khai lại cả hai class, vd `\"bg-sidebar ring-sidebar\"`.\n */\n surfaceClassName?: string;\n}\n\n/**\n * Độ chồng theo CỠ, không phải một hằng số cho mọi cỡ.\n *\n * Mẫu Avatar stack ở comitor-ds là `-space-x-2` trên avatar 40px — tức chồng **20%**. Áp thẳng\n * `-space-x-2` cho mọi cỡ thì ở `xs` (24px) thành chồng 33%, và cộng thêm vòng 2px nữa là phần bị\n * che ăn vào đúng chữ cái đầu của initials: \"ĐH\" ở 10px bắt đầu cách mép ~5,5px, trong khi mép che\n * đã tới 10px. Giữ tỉ lệ ~20% thì mọi cỡ đọc được mà mật độ vẫn y như bản duyệt.\n *\n * xs 24px → 4px (17%) · sm 28px → 6px (21%) · md 36px → 8px (22%)\n * lg 44px → 10px (23%) · xl 56px → 12px (21%)\n */\nconst overlapClasses: Record<LetterAvatarSize, string> = {\n xs: \"-space-x-1\",\n sm: \"-space-x-1.5\",\n md: \"-space-x-2\",\n lg: \"-space-x-2.5\",\n xl: \"-space-x-3\"\n};\n\nconst overflowSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-7 text-xs\",\n md: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\"\n};\n\n/**\n * Chồng avatar + ô \"+k\" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).\n *\n * Tooltip hiện tên đầy đủ vì avatar chồng nhau thì initials bị che một phần; không có tooltip\n * thì nhóm avatar chỉ là trang trí, người dùng không biết ai đang ở trong việc.\n *\n * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình\n * — xem chú thích ở trigger bên dưới.\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"sm\",\n onOverflowClick,\n overflowLabel = (count) => `Xem tất cả ${count} thành viên`,\n label = \"Thành viên\",\n surfaceClassName = \"bg-card ring-card\",\n className,\n ...props\n}: AvatarGroupProps) {\n const visible = items.slice(0, Math.max(0, max));\n const overflow = items.length - visible.length;\n\n return (\n <TooltipProvider delayDuration={200}>\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center\", overlapClasses[size], className)}\n {...props}\n >\n {visible.map((item) => (\n <Tooltip key={item.id}>\n <TooltipTrigger asChild>\n {/* Trigger phải FOCUSABLE: `<span>` thường chỉ nhận sự kiện chuột nên tooltip\n (và cả tên thành viên) biến mất với người dùng bàn phím. Nút không có\n onClick là chủ ý — nó chỉ mở tooltip; tên accessible lấy từ `sr-only`\n bên trong LetterAvatar nên không cần aria-label lặp lại. */}\n {/* Viền focus dùng `foreground` chứ không `ring`: `--ring` là gold — #E8B824 chỉ\n 1,86:1 trên nền trang ở light, dưới 3:1 mà WCAG 1.4.11/2.4.11 đòi cho chỉ báo\n focus (ở dark #F5D060 mới đạt 12,67:1, tức hỏng đúng một nửa số người dùng).\n `outline-foreground` đạt 18,91:1 (light) / 17,66:1 (dark) — cùng lối với viền\n focus của dòng trong `DataTable`. */}\n {/* Nền đục nằm DƯỚI tint có alpha của avatar — xem `surfaceClassName`. Vòng\n `ring-2` cùng màu bề mặt chỉ tách hai avatar ra, nó KHÔNG che được phần tint\n chồng lên nhau bên trong vòng. */}\n {/* ⚠ `inline-flex` KHÔNG phải để bố trí — nó vá một khoảng trắng có thật.\n `LetterAvatar` là `inline-flex`, nên trong một nút KHỐI nó thành nội dung\n inline và bị canh theo BASELINE: line box chừa thêm khoảng chân chữ BÊN DƯỚI.\n Đã đo ở `xs` trên `/`: nút cao **30,5px** ôm một avatar **24px** — dư 6,5px,\n toàn bộ ở đáy. Nút lại có `bg-card` ĐỤC và `rounded-full`, nên chỗ dư ấy là\n một mảng trắng hình lưỡi liềm dưới mỗi khuôn mặt. Lúc nghỉ nó lẫn vào nền thẻ;\n `hover:z-10` + `hover:-translate-y-0.5` kéo nút lên trước và phơi nó ra.\n ⚠ `leading-none` KHÔNG đủ — đã đo: còn dư 2,5px. Phải bỏ hẳn line box.\n Ô \"+k\" bên dưới vốn đã `inline-flex`; chỗ này chỉ là sót. */}\n <button\n type=\"button\"\n className={cn(\n \"inline-flex rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n surfaceClassName\n )}\n >\n <LetterAvatar name={item.name} src={item.src} size={size} title={undefined} />\n </button>\n </TooltipTrigger>\n <TooltipContent>\n <span className=\"font-medium\">{item.name}</span>\n {/* `opacity-80` ở đây ĐẠT chuẩn và đã đo, không phải chỗ sót: Tooltip là\n `bg-foreground text-background`, nên chữ mờ 80% ra 12,15:1 (light) / 9,85:1\n (dark) — trên xa ngưỡng 4,5:1. Tooltip cũng không tương tác được. */}\n {item.caption && <span className=\"block text-xs opacity-80\">{item.caption}</span>}\n </TooltipContent>\n </Tooltip>\n ))}\n\n {overflow > 0 &&\n (onOverflowClick ? (\n <button\n type=\"button\"\n onClick={onOverflowClick}\n className={cn(\n // `surfaceClassName` đứng TRƯỚC để `bg-muted` ngay sau đè lại phần nền của nó:\n // ô \"+k\" vốn đã đục, ở đây chỉ mượn màu VÒNG của bề mặt.\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 transition-colors hover:bg-accent hover:text-accent-foreground\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n <span className=\"sr-only\">{overflowLabel(items.length)}</span>\n </button>\n ) : (\n <span\n className={cn(\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n </span>\n ))}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";AAgJgB,cAGJ,YAHI;AA7IhB,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,iBAAiB,sBAAsB;AACzE,SAAS,oBAA2C;AA4DpD,MAAM,iBAAmD;AAAA,EACvD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,sBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA,gBAAgB,CAAC,UAAU,wBAAc,KAAK;AAAA,EAC9C,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AAC/C,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,SACE,oBAAC,mBAAgB,eAAe,KAC9B;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,eAAe,IAAI,GAAG,SAAS;AAAA,MACjE,GAAG;AAAA,MAEH;AAAA,gBAAQ,IAAI,CAAC,SACZ,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MAsBrB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,OAAO,QAAW;AAAA;AAAA,UAC9E,GACF;AAAA,UACA,qBAAC,kBACC;AAAA,gCAAC,UAAK,WAAU,eAAe,eAAK,MAAK;AAAA,YAIxC,KAAK,WAAW,oBAAC,UAAK,WAAU,4BAA4B,eAAK,SAAQ;AAAA,aAC5E;AAAA,aAvCY,KAAK,EAwCnB,CACD;AAAA,QAEA,WAAW,MACT,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA,cACF,oBAAC,UAAK,WAAU,WAAW,wBAAc,MAAM,MAAM,GAAE;AAAA;AAAA;AAAA,QACzD,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA;AAAA;AAAA,QACJ;AAAA;AAAA;AAAA,EAEN,GACF;AAEJ;","names":[]}
|
|
@@ -6,8 +6,18 @@ interface SwitchProps extends React.ComponentProps<typeof SwitchPrimitive.Root>
|
|
|
6
6
|
size?: "sm" | "md";
|
|
7
7
|
/** Nhãn đặt cạnh công tắc; có nhãn thì component tự bọc thêm lớp bố cục. */
|
|
8
8
|
label?: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Mô tả phụ dưới nhãn.
|
|
11
|
+
*
|
|
12
|
+
* `ReactNode` chứ không phải `string`, và đó là chủ ý: mô tả hay phải mang theo một mảnh giao
|
|
13
|
+
* diện — một dấu "chưa hoạt động", một `<strong>`, một liên kết. Khi kiểu còn là `string`, app
|
|
14
|
+
* buộc phải kéo mô tả RA NGOÀI bố cục của component rồi tự căn lề bằng số đo lấy từ nội thất của
|
|
15
|
+
* gói (`w-9` + `gap-2.5`) — tức chép hằng số riêng của gói vào app, và một bản nâng gói sẽ phá
|
|
16
|
+
* chỗ đó trong im lặng. Đã xảy ra thật ở `comitor-starter`.
|
|
17
|
+
*
|
|
18
|
+
* Không đổi gì ở runtime: giá trị vẫn được render làm children của cùng một `<p>`.
|
|
19
|
+
*/
|
|
20
|
+
description?: React.ReactNode;
|
|
11
21
|
}
|
|
12
22
|
declare function Switch({ className, size, label, description, id, ...props }: SwitchProps): React.JSX.Element;
|
|
13
23
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/switch.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/* Bảng cỡ tách ra ngoài component để không dựng lại object mỗi lần render. */\nconst trackSizes = {\n sm: \"h-4 w-7\",\n md: \"h-5 w-9\"\n} as const;\n\n/*\n * Quãng dịch của núm phải để hở HAI BÊN BẰNG NHAU: núm nghỉ ở `translate-x-0.5` (2px), nên khi\n * BẬT nó phải dừng ở `rãnh − núm − 2px`, không phải ở một số tròn.\n *\n * md 36 − 16 − 2 = 18px → translate-x-4.5 (trước: translate-x-4 = 16px → hở phải 4px)\n * sm 28 − 12 − 2 = 14px → translate-x-3.5 (trước: translate-x-3 = 12px → hở phải 4px)\n *\n * Bản trước dùng số tròn nên công tắc hở 2px lúc TẮT mà 4px lúc BẬT — mắt bắt được ngay khi hai\n * công tắc khác trạng thái đứng cạnh nhau trong cùng một danh sách.\n */\nconst thumbSizes = {\n sm: \"size-3 data-[state=checked]:translate-x-3.5\",\n md: \"size-4 data-[state=checked]:translate-x-4.5\"\n} as const;\n\nexport interface SwitchProps extends React.ComponentProps<typeof SwitchPrimitive.Root> {\n /** Cỡ công tắc. Mặc định `md`. */\n size?: \"sm\" | \"md\";\n /** Nhãn đặt cạnh công tắc; có nhãn thì component tự bọc thêm lớp bố cục. */\n label?: string;\n
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/switch.tsx"],"sourcesContent":["\"use client\";\n\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/* Bảng cỡ tách ra ngoài component để không dựng lại object mỗi lần render. */\nconst trackSizes = {\n sm: \"h-4 w-7\",\n md: \"h-5 w-9\"\n} as const;\n\n/*\n * Quãng dịch của núm phải để hở HAI BÊN BẰNG NHAU: núm nghỉ ở `translate-x-0.5` (2px), nên khi\n * BẬT nó phải dừng ở `rãnh − núm − 2px`, không phải ở một số tròn.\n *\n * md 36 − 16 − 2 = 18px → translate-x-4.5 (trước: translate-x-4 = 16px → hở phải 4px)\n * sm 28 − 12 − 2 = 14px → translate-x-3.5 (trước: translate-x-3 = 12px → hở phải 4px)\n *\n * Bản trước dùng số tròn nên công tắc hở 2px lúc TẮT mà 4px lúc BẬT — mắt bắt được ngay khi hai\n * công tắc khác trạng thái đứng cạnh nhau trong cùng một danh sách.\n */\nconst thumbSizes = {\n sm: \"size-3 data-[state=checked]:translate-x-3.5\",\n md: \"size-4 data-[state=checked]:translate-x-4.5\"\n} as const;\n\nexport interface SwitchProps extends React.ComponentProps<typeof SwitchPrimitive.Root> {\n /** Cỡ công tắc. Mặc định `md`. */\n size?: \"sm\" | \"md\";\n /** Nhãn đặt cạnh công tắc; có nhãn thì component tự bọc thêm lớp bố cục. */\n label?: string;\n /**\n * Mô tả phụ dưới nhãn.\n *\n * `ReactNode` chứ không phải `string`, và đó là chủ ý: mô tả hay phải mang theo một mảnh giao\n * diện — một dấu \"chưa hoạt động\", một `<strong>`, một liên kết. Khi kiểu còn là `string`, app\n * buộc phải kéo mô tả RA NGOÀI bố cục của component rồi tự căn lề bằng số đo lấy từ nội thất của\n * gói (`w-9` + `gap-2.5`) — tức chép hằng số riêng của gói vào app, và một bản nâng gói sẽ phá\n * chỗ đó trong im lặng. Đã xảy ra thật ở `comitor-starter`.\n *\n * Không đổi gì ở runtime: giá trị vẫn được render làm children của cùng một `<p>`.\n */\n description?: React.ReactNode;\n}\n\n/*\n * Style port từ comitor-ds (đổi gray-* → ash-* theo namespace của package).\n *\n * ─ BA LỚP MÀU, BA TOKEN VAI TRÒ — KHÔNG viết thẳng thang màu ─\n *\n * rãnh TẮT `bg-switch-track` (comitor-ds: bg-gray-300 / dark:bg-gray-700)\n * rãnh BẬT `data-[state=checked]:bg-control-on` (comitor-ds: bg-gold-300)\n * núm `bg-control-knob` (comitor-ds: bg-white)\n *\n * Ba token này là chỗ DUY NHẤT phân biệt hai bảng màu của gói (styles.css khối 5b). Viết thẳng\n * `bg-ash-300` / `bg-gold-300` / `bg-white` là khoá cứng bảng mặc định và bảng tương phản cao\n * lặng lẽ không đổi được công tắc — không có gì báo đỏ.\n *\n * ─ CÔNG TẮC LÀ THÀNH PHẦN PHI VĂN BẢN: BA PHÉP ĐO, KHÔNG PHẢI MỘT ─\n *\n * WCAG 1.4.11 đòi 3:1 cho \"thông tin thị giác cần để nhận ra thành phần và TRẠNG THÁI của nó\".\n * Với công tắc, đó là ba cặp:\n *\n * (1) rãnh ⇄ nền trang → có nhìn thấy cái công tắc không\n * (2) núm ⇄ rãnh → núm đang nằm bên nào\n * (3) rãnh BẬT ⇄ rãnh TẮT → hai trạng thái có khác nhau không\n *\n * Số đo của hai bảng — bảng mặc định là bản duyệt comitor-ds, nó KHÔNG đạt (1) và (2), và đó là\n * quyết định đã chấp nhận (xem khối NGUỒN SỰ THẬT đầu styles.css):\n *\n * bảng MẶC ĐỊNH (light) bảng TƯƠNG PHẢN CAO (light · dark)\n * rãnh TẮT ⇄ nền 1,52:1 ✗ 3,83:1 · 4,94:1 ✓\n * rãnh BẬT ⇄ nền 1,86:1 ✗ 6,51:1 · 12,67:1 ✓\n * núm ⇄ rãnh BẬT 1,86:1 ✗ 6,51:1 · 12,67:1 ✓\n *\n * ⚠ Ở bảng tương phản cao, (3) rãnh BẬT ⇄ rãnh TẮT chỉ 1,70:1 (light) / 2,57:1 (dark) — cố ý:\n * hai trạng thái còn khác nhau bằng VỊ TRÍ núm, và cả hai rãnh đều tự đạt ≥3:1 với nền trang lẫn\n * với núm. Không có cặp màu nào vừa đạt (1) vừa đạt (3) khi nền trang là trắng.\n *\n * ⚠ ĐỪNG dùng `--muted-foreground` cho rãnh TẮT dù số có vẻ vừa: đó là bậc CHỮ, nó bị kéo\n * tối/sáng theo nhu cầu đọc chữ (đã đổi một lần #7E8290 → #686C7A / #888D9B), tức ràng buộc\n * \"núm còn nổi trên rãnh\" không có gì bảo vệ.\n */\nfunction Switch({ className, size = \"md\", label, description, id, ...props }: SwitchProps) {\n const generatedId = React.useId();\n const switchId = id ?? generatedId;\n\n const switchElement = (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n id={switchId}\n className={cn(\n \"peer inline-flex shrink-0 items-center rounded-full bg-switch-track data-[state=checked]:bg-control-on\",\n \"transition-colors duration-150 ease-out\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n trackSizes[size],\n className\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n \"pointer-events-none block translate-x-0.5 rounded-full bg-control-knob shadow-sm ring-0\",\n \"transition-transform duration-150 ease-out\",\n thumbSizes[size]\n )}\n />\n </SwitchPrimitive.Root>\n );\n\n if (!label && !description) {\n return switchElement;\n }\n\n return (\n <div className=\"flex items-start gap-2.5\">\n {switchElement}\n <div className=\"flex flex-col gap-0.5\">\n {label && (\n <label\n htmlFor={switchId}\n className=\"text-sm font-medium text-foreground cursor-pointer select-none leading-none\"\n >\n {label}\n </label>\n )}\n {description && <p className=\"text-xs text-muted-foreground leading-relaxed\">{description}</p>}\n </div>\n </div>\n );\n}\n\nexport { Switch };\n"],"mappings":";AAsGM,cAkBA,YAlBA;AApGN,YAAY,qBAAqB;AACjC,YAAY,WAAW;AACvB,SAAS,UAAU;AAGnB,MAAM,aAAa;AAAA,EACjB,IAAI;AAAA,EACJ,IAAI;AACN;AAYA,MAAM,aAAa;AAAA,EACjB,IAAI;AAAA,EACJ,IAAI;AACN;AA2DA,SAAS,OAAO,EAAE,WAAW,OAAO,MAAM,OAAO,aAAa,IAAI,GAAG,MAAM,GAAgB;AACzF,QAAM,cAAc,MAAM,MAAM;AAChC,QAAM,WAAW,MAAM;AAEvB,QAAM,gBACJ;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC,aAAU;AAAA,MACV,IAAI;AAAA,MACJ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,IAAI;AAAA,QACf;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,gBAAgB;AAAA,QAAhB;AAAA,UACC,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,WAAW,IAAI;AAAA,UACjB;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAGF,MAAI,CAAC,SAAS,CAAC,aAAa;AAC1B,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,SAAI,WAAU,4BACZ;AAAA;AAAA,IACD,qBAAC,SAAI,WAAU,yBACZ;AAAA,eACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS;AAAA,UACT,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MAED,eAAe,oBAAC,OAAE,WAAU,iDAAiD,uBAAY;AAAA,OAC5F;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { FontSizeMode } from './display-axis-constants.js';
|
|
2
|
+
import { DensityMode, FontSizeMode } from './display-axis-constants.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Hai control của trục MẬT ĐỘ và trục CỠ CHỮ.
|
|
@@ -16,8 +16,21 @@ import { FontSizeMode } from './display-axis-constants.js';
|
|
|
16
16
|
*/
|
|
17
17
|
interface DensityToggleProps {
|
|
18
18
|
label?: string;
|
|
19
|
-
description?:
|
|
19
|
+
description?: React.ReactNode;
|
|
20
20
|
className?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.
|
|
23
|
+
*
|
|
24
|
+
* ⚠ Đây là callback THÔNG BÁO, không phải `onChange` của một component controlled. Giá trị CÓ
|
|
25
|
+
* HIỆU LỰC vẫn nằm ở `localStorage` của gói và vẫn do provider quản; prop này chỉ cho app biết
|
|
26
|
+
* "vừa có thay đổi" để nó ghi một MẶC ĐỊNH theo tài khoản ở nơi khác (Comitor.Account), hiện một
|
|
27
|
+
* toast, hoặc ghi log.
|
|
28
|
+
*
|
|
29
|
+
* Vì sao KHÔNG làm controlled: trục hiển thị phải có hiệu lực trước lượt sơn đầu tiên, và điều đó
|
|
30
|
+
* chỉ làm được bằng script chống nháy đọc `localStorage` — một giá trị đi từ server xuống prop
|
|
31
|
+
* luôn tới SAU lượt sơn ấy. Xem `display-axis-provider.tsx`.
|
|
32
|
+
*/
|
|
33
|
+
onDensityChange?: (mode: DensityMode) => void;
|
|
21
34
|
}
|
|
22
35
|
/**
|
|
23
36
|
* Nút gạt "bố cục gọn".
|
|
@@ -27,13 +40,15 @@ interface DensityToggleProps {
|
|
|
27
40
|
* thanh "Có thay đổi chưa lưu" sáng lên cho một thay đổi đã có hiệu lực, và nút "Hoàn tác" trong
|
|
28
41
|
* toast sẽ lùi bản nháp mà KHÔNG lùi được trục — giao diện tự mâu thuẫn với chính nó.
|
|
29
42
|
*/
|
|
30
|
-
declare function DensityToggle({ label, description, className }: DensityToggleProps): React.JSX.Element;
|
|
43
|
+
declare function DensityToggle({ label, description, className, onDensityChange }: DensityToggleProps): React.JSX.Element;
|
|
31
44
|
interface FontSizeControlProps {
|
|
32
45
|
/** Nhãn của cả nhóm, đọc lên cho trình đọc màn hình. */
|
|
33
46
|
label?: string;
|
|
34
47
|
/** Nhãn từng mức — THỨ TỰ theo `FONT_SIZE_MODES` (`sm`, `md`, `lg`). */
|
|
35
48
|
optionLabels?: Record<FontSizeMode, string>;
|
|
36
49
|
className?: string;
|
|
50
|
+
/** Gọi SAU khi người dùng đổi — xem giải thích ở `DensityToggleProps.onDensityChange`. */
|
|
51
|
+
onFontSizeChange?: (mode: FontSizeMode) => void;
|
|
37
52
|
}
|
|
38
53
|
/**
|
|
39
54
|
* Chọn cỡ chữ — ba mức, một hàng nút.
|
|
@@ -46,6 +61,6 @@ interface FontSizeControlProps {
|
|
|
46
61
|
* Cỡ chữ của CHÍNH các nút này cố định `text-sm`: nếu chúng phóng theo lựa chọn thì bấm "Lớn" sẽ
|
|
47
62
|
* làm cả hàng nút nhảy chỗ ngay dưới ngón tay.
|
|
48
63
|
*/
|
|
49
|
-
declare function FontSizeControl({ label, optionLabels, className }: FontSizeControlProps): React.JSX.Element;
|
|
64
|
+
declare function FontSizeControl({ label, optionLabels, className, onFontSizeChange }: FontSizeControlProps): React.JSX.Element;
|
|
50
65
|
|
|
51
66
|
export { DensityToggle, type DensityToggleProps, FontSizeControl, type FontSizeControlProps };
|
|
@@ -7,7 +7,8 @@ import { useDensity, useFontSize } from "./display-axis-provider.js";
|
|
|
7
7
|
function DensityToggle({
|
|
8
8
|
label = "B\u1ED1 c\u1EE5c g\u1ECDn",
|
|
9
9
|
description = "Gi\u1EA3m kho\u1EA3ng c\xE1ch d\u1ECDc gi\u1EEFa c\xE1c d\xF2ng trong b\u1EA3ng, danh s\xE1ch v\xE0 menu.",
|
|
10
|
-
className
|
|
10
|
+
className,
|
|
11
|
+
onDensityChange
|
|
11
12
|
}) {
|
|
12
13
|
const { density, setDensity, mounted } = useDensity();
|
|
13
14
|
return /* @__PURE__ */ jsx(
|
|
@@ -15,7 +16,11 @@ function DensityToggle({
|
|
|
15
16
|
{
|
|
16
17
|
className,
|
|
17
18
|
checked: density === "compact",
|
|
18
|
-
onCheckedChange: (checked) =>
|
|
19
|
+
onCheckedChange: (checked) => {
|
|
20
|
+
const next = checked ? "compact" : "comfortable";
|
|
21
|
+
setDensity(next);
|
|
22
|
+
onDensityChange?.(next);
|
|
23
|
+
},
|
|
19
24
|
disabled: !mounted,
|
|
20
25
|
label,
|
|
21
26
|
description
|
|
@@ -30,7 +35,8 @@ const DEFAULT_FONT_SIZE_LABELS = {
|
|
|
30
35
|
function FontSizeControl({
|
|
31
36
|
label = "C\u1EE1 ch\u1EEF",
|
|
32
37
|
optionLabels = DEFAULT_FONT_SIZE_LABELS,
|
|
33
|
-
className
|
|
38
|
+
className,
|
|
39
|
+
onFontSizeChange
|
|
34
40
|
}) {
|
|
35
41
|
const { fontSize, setFontSize, mounted } = useFontSize();
|
|
36
42
|
return /* @__PURE__ */ jsx(
|
|
@@ -48,7 +54,10 @@ function FontSizeControl({
|
|
|
48
54
|
role: "radio",
|
|
49
55
|
"aria-checked": selected,
|
|
50
56
|
disabled: !mounted,
|
|
51
|
-
onClick: () =>
|
|
57
|
+
onClick: () => {
|
|
58
|
+
setFontSize(mode);
|
|
59
|
+
onFontSizeChange?.(mode);
|
|
60
|
+
},
|
|
52
61
|
className: cn(
|
|
53
62
|
"rounded-sm px-3 py-1 text-sm transition-colors duration-150 ease-out",
|
|
54
63
|
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/display-axis-controls.tsx"],"sourcesContent":["\"use client\";\n\nimport { Switch } from \"../components/ui/switch.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { FONT_SIZE_MODES, type FontSizeMode } from \"./display-axis-constants.js\";\nimport { useDensity, useFontSize } from \"./display-axis-provider.js\";\n\n/**\n * Hai control của trục MẬT ĐỘ và trục CỠ CHỮ.\n *\n * ─ VÌ SAO CẢ HAI ĐỀU CÓ `disabled={!mounted}` ─\n * Giá trị thật nằm ở localStorage, mà server không đọc được. Render lần đầu ở client PHẢI khớp\n * HTML server sinh ra, nếu không React báo lệch hydration — nên provider giữ giá trị mặc định cho\n * tới khi effect đầu chạy. Trong khoảnh khắc đó control hiện mặc định chứ không hiện lựa chọn\n * thật, và nếu người dùng kịp bấm thì họ ghi đè lên chính lựa chọn của mình. Khoá cho tới khi\n * `mounted` là cách `ContrastToggle` đã dùng.\n *\n * (DOM thật thì không nháy: script chống nháy của provider đã đặt thuộc tính lên `<html>` trước\n * lần sơn đầu tiên. Chỉ CONTROL là phải chờ.)\n */\n\nexport interface DensityToggleProps {\n label?: string;\n description?:
|
|
1
|
+
{"version":3,"sources":["../../src/shell/display-axis-controls.tsx"],"sourcesContent":["\"use client\";\n\nimport type * as React from \"react\";\nimport { Switch } from \"../components/ui/switch.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { type DensityMode, FONT_SIZE_MODES, type FontSizeMode } from \"./display-axis-constants.js\";\nimport { useDensity, useFontSize } from \"./display-axis-provider.js\";\n\n/**\n * Hai control của trục MẬT ĐỘ và trục CỠ CHỮ.\n *\n * ─ VÌ SAO CẢ HAI ĐỀU CÓ `disabled={!mounted}` ─\n * Giá trị thật nằm ở localStorage, mà server không đọc được. Render lần đầu ở client PHẢI khớp\n * HTML server sinh ra, nếu không React báo lệch hydration — nên provider giữ giá trị mặc định cho\n * tới khi effect đầu chạy. Trong khoảnh khắc đó control hiện mặc định chứ không hiện lựa chọn\n * thật, và nếu người dùng kịp bấm thì họ ghi đè lên chính lựa chọn của mình. Khoá cho tới khi\n * `mounted` là cách `ContrastToggle` đã dùng.\n *\n * (DOM thật thì không nháy: script chống nháy của provider đã đặt thuộc tính lên `<html>` trước\n * lần sơn đầu tiên. Chỉ CONTROL là phải chờ.)\n */\n\nexport interface DensityToggleProps {\n label?: string;\n description?: React.ReactNode;\n className?: string;\n /**\n * Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.\n *\n * ⚠ Đây là callback THÔNG BÁO, không phải `onChange` của một component controlled. Giá trị CÓ\n * HIỆU LỰC vẫn nằm ở `localStorage` của gói và vẫn do provider quản; prop này chỉ cho app biết\n * \"vừa có thay đổi\" để nó ghi một MẶC ĐỊNH theo tài khoản ở nơi khác (Comitor.Account), hiện một\n * toast, hoặc ghi log.\n *\n * Vì sao KHÔNG làm controlled: trục hiển thị phải có hiệu lực trước lượt sơn đầu tiên, và điều đó\n * chỉ làm được bằng script chống nháy đọc `localStorage` — một giá trị đi từ server xuống prop\n * luôn tới SAU lượt sơn ấy. Xem `display-axis-provider.tsx`.\n */\n onDensityChange?: (mode: DensityMode) => void;\n}\n\n/**\n * Nút gạt \"bố cục gọn\".\n *\n * ⚠ Đây KHÔNG phải một trường của biểu mẫu cài đặt. Nó có hiệu lực NGAY và tự lưu, giống\n * `ThemeToggle`/`ContrastToggle` — đừng đưa nó vào luồng nháp/\"Lưu thay đổi\". Trộn hai kiểu thì\n * thanh \"Có thay đổi chưa lưu\" sáng lên cho một thay đổi đã có hiệu lực, và nút \"Hoàn tác\" trong\n * toast sẽ lùi bản nháp mà KHÔNG lùi được trục — giao diện tự mâu thuẫn với chính nó.\n */\nexport function DensityToggle({\n label = \"Bố cục gọn\",\n description = \"Giảm khoảng cách dọc giữa các dòng trong bảng, danh sách và menu.\",\n className,\n onDensityChange\n}: DensityToggleProps) {\n const { density, setDensity, mounted } = useDensity();\n\n return (\n <Switch\n className={className}\n checked={density === \"compact\"}\n onCheckedChange={(checked) => {\n const next: DensityMode = checked ? \"compact\" : \"comfortable\";\n setDensity(next);\n onDensityChange?.(next);\n }}\n disabled={!mounted}\n label={label}\n description={description}\n />\n );\n}\n\nexport interface FontSizeControlProps {\n /** Nhãn của cả nhóm, đọc lên cho trình đọc màn hình. */\n label?: string;\n /** Nhãn từng mức — THỨ TỰ theo `FONT_SIZE_MODES` (`sm`, `md`, `lg`). */\n optionLabels?: Record<FontSizeMode, string>;\n className?: string;\n /** Gọi SAU khi người dùng đổi — xem giải thích ở `DensityToggleProps.onDensityChange`. */\n onFontSizeChange?: (mode: FontSizeMode) => void;\n}\n\nconst DEFAULT_FONT_SIZE_LABELS: Record<FontSizeMode, string> = {\n sm: \"Nhỏ\",\n md: \"Vừa\",\n lg: \"Lớn\"\n};\n\n/**\n * Chọn cỡ chữ — ba mức, một hàng nút.\n *\n * `role=\"radiogroup\"` + `aria-checked` chứ không phải ba nút thường: đây là chọn MỘT trong ba, và\n * trình đọc màn hình cần nghe \"2 trên 3\" chứ không nghe ba nút rời rạc. Dùng `<button>` thật (bàn\n * phím tới được bằng Tab) thay vì roving focus của Radix ToggleGroup — nhóm chỉ có ba mục nên\n * roving focus không đáng cái giá thêm một phụ thuộc.\n *\n * Cỡ chữ của CHÍNH các nút này cố định `text-sm`: nếu chúng phóng theo lựa chọn thì bấm \"Lớn\" sẽ\n * làm cả hàng nút nhảy chỗ ngay dưới ngón tay.\n */\nexport function FontSizeControl({\n label = \"Cỡ chữ\",\n optionLabels = DEFAULT_FONT_SIZE_LABELS,\n className,\n onFontSizeChange\n}: FontSizeControlProps) {\n const { fontSize, setFontSize, mounted } = useFontSize();\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={label}\n className={cn(\"inline-flex items-center gap-1 rounded-md border border-input p-1 text-sm\", className)}\n >\n {FONT_SIZE_MODES.map((mode) => {\n const selected = fontSize === mode;\n return (\n <button\n key={mode}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n disabled={!mounted}\n onClick={() => {\n setFontSize(mode);\n onFontSizeChange?.(mode);\n }}\n className={cn(\n \"rounded-sm px-3 py-1 text-sm transition-colors duration-150 ease-out\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n selected\n ? \"bg-control-on text-control-check font-medium\"\n : \"text-muted-foreground hover:bg-accent hover:text-foreground\"\n )}\n >\n {optionLabels[mode]}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AA0DI;AAvDJ,SAAS,cAAc;AACvB,SAAS,UAAU;AACnB,SAA2B,uBAA0C;AACrE,SAAS,YAAY,mBAAmB;AA2CjC,SAAS,cAAc;AAAA,EAC5B,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,EAAE,SAAS,YAAY,QAAQ,IAAI,WAAW;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,YAAY;AAAA,MACrB,iBAAiB,CAAC,YAAY;AAC5B,cAAM,OAAoB,UAAU,YAAY;AAChD,mBAAW,IAAI;AACf,0BAAkB,IAAI;AAAA,MACxB;AAAA,MACA,UAAU,CAAC;AAAA,MACX;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;AAYA,MAAM,2BAAyD;AAAA,EAC7D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAaO,SAAS,gBAAgB;AAAA,EAC9B,QAAQ;AAAA,EACR,eAAe;AAAA,EACf;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,EAAE,UAAU,aAAa,QAAQ,IAAI,YAAY;AAEvD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,6EAA6E,SAAS;AAAA,MAEnG,0BAAgB,IAAI,CAAC,SAAS;AAC7B,cAAM,WAAW,aAAa;AAC9B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,UAAU,CAAC;AAAA,YACX,SAAS,MAAM;AACb,0BAAY,IAAI;AAChB,iCAAmB,IAAI;AAAA,YACzB;AAAA,YACA,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA,WACI,iDACA;AAAA,YACN;AAAA,YAEC,uBAAa,IAAI;AAAA;AAAA,UAlBb;AAAA,QAmBP;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { Sidebar, SidebarProps } from './sidebar.js';
|
|
|
18
18
|
export { SidebarGroup, SidebarGroupProps } from './sidebar-group.js';
|
|
19
19
|
export { SidebarMenuItem, SidebarMenuItemProps } from './sidebar-menu-item.js';
|
|
20
20
|
export { ThemeProvider, ThemeProviderProps } from './theme-provider.js';
|
|
21
|
-
export { ThemeToggle, ThemeToggleLabels, ThemeToggleProps } from './theme-toggle.js';
|
|
21
|
+
export { ThemeChoice, ThemeToggle, ThemeToggleLabels, ThemeToggleProps } from './theme-toggle.js';
|
|
22
22
|
export { AppDescriptor, DEFAULT_SHELL_LABELS, NavBadgeVariant, NavGroup, NavItem, ShellIcon, ShellLabels, ShellLinkComponent, ShellLinkProps, ShellNav, ShellUser, UserMenuItem, Workspace } from './types.js';
|
|
23
23
|
export { UserAvatar, UserAvatarProps, UserMenu, UserMenuProps } from './user-menu.js';
|
|
24
24
|
export { NavSearchParams, appAccentStyle, flattenNavItems, getEntitledApps, getLockedApps, isNavBranchActive, isNavItemActive, resolveCurrentApp, toNavGroups, toSearchParams } from './utils.js';
|
|
@@ -4,6 +4,15 @@ import 'lucide-react';
|
|
|
4
4
|
|
|
5
5
|
/** Chỉ những nhãn ThemeToggle thực sự dùng — để nút này KHÔNG buộc phải nằm trong ShellProvider. */
|
|
6
6
|
type ThemeToggleLabels = Pick<ShellLabels, "themeLight" | "themeDark" | "themeSystem" | "toggleTheme">;
|
|
7
|
+
/**
|
|
8
|
+
* Ba lựa chọn mà `ThemeToggle` ghi ra — tập ĐÓNG, và `"system"` là một giá trị thật chứ không phải
|
|
9
|
+
* "chưa chọn".
|
|
10
|
+
*
|
|
11
|
+
* ⚠ App nào lưu giá trị này (ví dụ làm mặc định theo tài khoản) phải chấp nhận đủ CẢ BA. Ép nó về
|
|
12
|
+
* hai trạng thái sáng/tối là làm mất lựa chọn "đi theo hệ điều hành" — thứ mà người bật chế độ tối
|
|
13
|
+
* theo giờ trong ngày sẽ mất ngay khi họ đăng nhập ở một máy khác.
|
|
14
|
+
*/
|
|
15
|
+
type ThemeChoice = "light" | "dark" | "system";
|
|
7
16
|
interface ThemeToggleProps {
|
|
8
17
|
className?: string;
|
|
9
18
|
/**
|
|
@@ -13,6 +22,18 @@ interface ThemeToggleProps {
|
|
|
13
22
|
variant?: "icon" | "menu";
|
|
14
23
|
/** Override chuỗi hiển thị (mặc định tiếng Việt). `AppHeader` truyền labels của shell xuống. */
|
|
15
24
|
labels?: Partial<ThemeToggleLabels>;
|
|
25
|
+
/**
|
|
26
|
+
* Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.
|
|
27
|
+
*
|
|
28
|
+
* ⚠ Callback THÔNG BÁO, không phải `onChange` của component controlled: theme CÓ HIỆU LỰC vẫn do
|
|
29
|
+
* `next-themes` giữ trong `localStorage`. Prop này để app ghi một MẶC ĐỊNH theo tài khoản ở nơi
|
|
30
|
+
* khác. Xem `DensityToggleProps.onDensityChange` cho lý do đầy đủ.
|
|
31
|
+
*
|
|
32
|
+
* ⚠ Nhận đủ ba giá trị kể cả `"system"` — và ở `variant="icon"` thì nó KHÔNG BAO GIỜ phát
|
|
33
|
+
* `"system"`, vì nút ấy chỉ lật sáng ⇄ tối. Muốn người dùng chọn được "hệ thống" thì phải dùng
|
|
34
|
+
* `variant="menu"`.
|
|
35
|
+
*/
|
|
36
|
+
onThemeChange?: (theme: ThemeChoice) => void;
|
|
16
37
|
}
|
|
17
38
|
/**
|
|
18
39
|
* Nút đổi giao diện — dùng được ĐỘC LẬP (trang đăng nhập, trang marketing),
|
|
@@ -23,6 +44,6 @@ interface ThemeToggleProps {
|
|
|
23
44
|
* lần đầu sẽ lệch với HTML do server sinh ra. Trước khi mount hiện ô rỗng cùng
|
|
24
45
|
* kích thước để layout không nhảy.
|
|
25
46
|
*/
|
|
26
|
-
declare function ThemeToggle({ className, variant, labels: labelsProp }: ThemeToggleProps): React.JSX.Element;
|
|
47
|
+
declare function ThemeToggle({ className, variant, labels: labelsProp, onThemeChange }: ThemeToggleProps): React.JSX.Element;
|
|
27
48
|
|
|
28
|
-
export { ThemeToggle, type ThemeToggleLabels, type ThemeToggleProps };
|
|
49
|
+
export { type ThemeChoice, ThemeToggle, type ThemeToggleLabels, type ThemeToggleProps };
|
|
@@ -12,8 +12,12 @@ import {
|
|
|
12
12
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip.js";
|
|
13
13
|
import { cn } from "../lib/cn.js";
|
|
14
14
|
import { DEFAULT_SHELL_LABELS } from "./types.js";
|
|
15
|
-
function ThemeToggle({ className, variant = "icon", labels: labelsProp }) {
|
|
15
|
+
function ThemeToggle({ className, variant = "icon", labels: labelsProp, onThemeChange }) {
|
|
16
16
|
const { theme, resolvedTheme, setTheme } = useTheme();
|
|
17
|
+
const choose = (next) => {
|
|
18
|
+
setTheme(next);
|
|
19
|
+
onThemeChange?.(next);
|
|
20
|
+
};
|
|
17
21
|
const labels = { ...DEFAULT_SHELL_LABELS, ...labelsProp };
|
|
18
22
|
const [mounted, setMounted] = useState(false);
|
|
19
23
|
useEffect(() => {
|
|
@@ -31,15 +35,15 @@ function ThemeToggle({ className, variant = "icon", labels: labelsProp }) {
|
|
|
31
35
|
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
32
36
|
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", className: buttonClass, "aria-label": labels.toggleTheme, children: resolvedTheme === "dark" ? /* @__PURE__ */ jsx(Moon, { className: "size-4" }) : /* @__PURE__ */ jsx(Sun, { className: "size-4" }) }) }),
|
|
33
37
|
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
|
|
34
|
-
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () =>
|
|
38
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => choose("light"), className: cn(theme === "light" && "bg-accent"), children: [
|
|
35
39
|
/* @__PURE__ */ jsx(Sun, { className: "size-4" }),
|
|
36
40
|
labels.themeLight
|
|
37
41
|
] }),
|
|
38
|
-
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () =>
|
|
42
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => choose("dark"), className: cn(theme === "dark" && "bg-accent"), children: [
|
|
39
43
|
/* @__PURE__ */ jsx(Moon, { className: "size-4" }),
|
|
40
44
|
labels.themeDark
|
|
41
45
|
] }),
|
|
42
|
-
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () =>
|
|
46
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => choose("system"), className: cn(theme === "system" && "bg-accent"), children: [
|
|
43
47
|
/* @__PURE__ */ jsx(Monitor, { className: "size-4" }),
|
|
44
48
|
labels.themeSystem
|
|
45
49
|
] })
|
|
@@ -48,16 +52,7 @@ function ThemeToggle({ className, variant = "icon", labels: labelsProp }) {
|
|
|
48
52
|
}
|
|
49
53
|
const nextTheme = resolvedTheme === "dark" ? "light" : "dark";
|
|
50
54
|
return /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
51
|
-
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
52
|
-
"button",
|
|
53
|
-
{
|
|
54
|
-
type: "button",
|
|
55
|
-
onClick: () => setTheme(nextTheme),
|
|
56
|
-
className: buttonClass,
|
|
57
|
-
"aria-label": labels.toggleTheme,
|
|
58
|
-
children: resolvedTheme === "dark" ? /* @__PURE__ */ jsx(Sun, { className: "size-4" }) : /* @__PURE__ */ jsx(Moon, { className: "size-4" })
|
|
59
|
-
}
|
|
60
|
-
) }),
|
|
55
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", onClick: () => choose(nextTheme), className: buttonClass, "aria-label": labels.toggleTheme, children: resolvedTheme === "dark" ? /* @__PURE__ */ jsx(Sun, { className: "size-4" }) : /* @__PURE__ */ jsx(Moon, { className: "size-4" }) }) }),
|
|
61
56
|
/* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: nextTheme === "dark" ? labels.themeDark : labels.themeLight })
|
|
62
57
|
] });
|
|
63
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/theme-toggle.tsx"],"sourcesContent":["\"use client\";\n\nimport { Monitor, Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { DEFAULT_SHELL_LABELS, type ShellLabels } from \"./types.js\";\n\n/** Chỉ những nhãn ThemeToggle thực sự dùng — để nút này KHÔNG buộc phải nằm trong ShellProvider. */\nexport type ThemeToggleLabels = Pick<ShellLabels, \"themeLight\" | \"themeDark\" | \"themeSystem\" | \"toggleTheme\">;\n\nexport interface ThemeToggleProps {\n className?: string;\n /**\n * \"icon\" — một nút, bấm là lật Sáng ⇄ Tối (mặc định, dùng ở header).\n * \"menu\" — dropdown 3 lựa chọn Sáng / Tối / Hệ thống.\n */\n variant?: \"icon\" | \"menu\";\n /** Override chuỗi hiển thị (mặc định tiếng Việt). `AppHeader` truyền labels của shell xuống. */\n labels?: Partial<ThemeToggleLabels>;\n}\n\n/**\n * Nút đổi giao diện — dùng được ĐỘC LẬP (trang đăng nhập, trang marketing),\n * không bắt buộc nằm trong `ShellProvider`.\n *\n * Chống lệch hydration bằng cờ `mounted`: theme thật chỉ biết được ở client\n * (đọc localStorage/`prefers-color-scheme`), render thẳng icon theo theme ngay\n * lần đầu sẽ lệch với HTML do server sinh ra. Trước khi mount hiện ô rỗng cùng\n * kích thước để layout không nhảy.\n */\nexport function ThemeToggle({ className, variant = \"icon\", labels: labelsProp }: ThemeToggleProps) {\n const { theme, resolvedTheme, setTheme } = useTheme();\n const labels: ThemeToggleLabels = { ...DEFAULT_SHELL_LABELS, ...labelsProp };\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n }, []);\n\n const buttonClass = 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 className\n );\n\n if (!mounted) {\n return <span className={buttonClass} aria-hidden />;\n }\n\n if (variant === \"menu\") {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button type=\"button\" className={buttonClass} aria-label={labels.toggleTheme}>\n {resolvedTheme === \"dark\" ? <Moon className=\"size-4\" /> : <Sun className=\"size-4\" />}\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() =>
|
|
1
|
+
{"version":3,"sources":["../../src/shell/theme-toggle.tsx"],"sourcesContent":["\"use client\";\n\nimport { Monitor, Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger\n} from \"../components/ui/dropdown-menu.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { DEFAULT_SHELL_LABELS, type ShellLabels } from \"./types.js\";\n\n/** Chỉ những nhãn ThemeToggle thực sự dùng — để nút này KHÔNG buộc phải nằm trong ShellProvider. */\nexport type ThemeToggleLabels = Pick<ShellLabels, \"themeLight\" | \"themeDark\" | \"themeSystem\" | \"toggleTheme\">;\n\n/**\n * Ba lựa chọn mà `ThemeToggle` ghi ra — tập ĐÓNG, và `\"system\"` là một giá trị thật chứ không phải\n * \"chưa chọn\".\n *\n * ⚠ App nào lưu giá trị này (ví dụ làm mặc định theo tài khoản) phải chấp nhận đủ CẢ BA. Ép nó về\n * hai trạng thái sáng/tối là làm mất lựa chọn \"đi theo hệ điều hành\" — thứ mà người bật chế độ tối\n * theo giờ trong ngày sẽ mất ngay khi họ đăng nhập ở một máy khác.\n */\nexport type ThemeChoice = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeToggleProps {\n className?: string;\n /**\n * \"icon\" — một nút, bấm là lật Sáng ⇄ Tối (mặc định, dùng ở header).\n * \"menu\" — dropdown 3 lựa chọn Sáng / Tối / Hệ thống.\n */\n variant?: \"icon\" | \"menu\";\n /** Override chuỗi hiển thị (mặc định tiếng Việt). `AppHeader` truyền labels của shell xuống. */\n labels?: Partial<ThemeToggleLabels>;\n /**\n * Gọi SAU khi người dùng đổi — cùng khuôn với `onContrastChange` của `ContrastToggle`.\n *\n * ⚠ Callback THÔNG BÁO, không phải `onChange` của component controlled: theme CÓ HIỆU LỰC vẫn do\n * `next-themes` giữ trong `localStorage`. Prop này để app ghi một MẶC ĐỊNH theo tài khoản ở nơi\n * khác. Xem `DensityToggleProps.onDensityChange` cho lý do đầy đủ.\n *\n * ⚠ Nhận đủ ba giá trị kể cả `\"system\"` — và ở `variant=\"icon\"` thì nó KHÔNG BAO GIỜ phát\n * `\"system\"`, vì nút ấy chỉ lật sáng ⇄ tối. Muốn người dùng chọn được \"hệ thống\" thì phải dùng\n * `variant=\"menu\"`.\n */\n onThemeChange?: (theme: ThemeChoice) => void;\n}\n\n/**\n * Nút đổi giao diện — dùng được ĐỘC LẬP (trang đăng nhập, trang marketing),\n * không bắt buộc nằm trong `ShellProvider`.\n *\n * Chống lệch hydration bằng cờ `mounted`: theme thật chỉ biết được ở client\n * (đọc localStorage/`prefers-color-scheme`), render thẳng icon theo theme ngay\n * lần đầu sẽ lệch với HTML do server sinh ra. Trước khi mount hiện ô rỗng cùng\n * kích thước để layout không nhảy.\n */\nexport function ThemeToggle({ className, variant = \"icon\", labels: labelsProp, onThemeChange }: ThemeToggleProps) {\n const { theme, resolvedTheme, setTheme } = useTheme();\n\n /*\n * Gói hai lời gọi lại làm một: bốn chỗ đổi theme trong file này (ba mục menu + nút lật) đều phải\n * báo, và một chỗ quên báo là một đường mà app không bao giờ ghi được mặc định — thứ chỉ lộ ra\n * khi người dùng đăng nhập ở máy khác.\n */\n const choose = (next: ThemeChoice) => {\n setTheme(next);\n onThemeChange?.(next);\n };\n const labels: ThemeToggleLabels = { ...DEFAULT_SHELL_LABELS, ...labelsProp };\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n }, []);\n\n const buttonClass = 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 className\n );\n\n if (!mounted) {\n return <span className={buttonClass} aria-hidden />;\n }\n\n if (variant === \"menu\") {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button type=\"button\" className={buttonClass} aria-label={labels.toggleTheme}>\n {resolvedTheme === \"dark\" ? <Moon className=\"size-4\" /> : <Sun className=\"size-4\" />}\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => choose(\"light\")} className={cn(theme === \"light\" && \"bg-accent\")}>\n <Sun className=\"size-4\" />\n {labels.themeLight}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => choose(\"dark\")} className={cn(theme === \"dark\" && \"bg-accent\")}>\n <Moon className=\"size-4\" />\n {labels.themeDark}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => choose(\"system\")} className={cn(theme === \"system\" && \"bg-accent\")}>\n <Monitor className=\"size-4\" />\n {labels.themeSystem}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n const nextTheme: ThemeChoice = resolvedTheme === \"dark\" ? \"light\" : \"dark\";\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button type=\"button\" onClick={() => choose(nextTheme)} className={buttonClass} aria-label={labels.toggleTheme}>\n {resolvedTheme === \"dark\" ? <Sun className=\"size-4\" /> : <Moon className=\"size-4\" />}\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">{nextTheme === \"dark\" ? labels.themeDark : labels.themeLight}</TooltipContent>\n </Tooltip>\n );\n}\n"],"mappings":";AAsFW,cAYD,YAZC;AApFX,SAAS,SAAS,MAAM,WAAW;AACnC,SAAS,gBAAgB;AACzB,SAAS,WAAW,gBAAgB;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,4BAA8C;AA+ChD,SAAS,YAAY,EAAE,WAAW,UAAU,QAAQ,QAAQ,YAAY,cAAc,GAAqB;AAChH,QAAM,EAAE,OAAO,eAAe,SAAS,IAAI,SAAS;AAOpD,QAAM,SAAS,CAAC,SAAsB;AACpC,aAAS,IAAI;AACb,oBAAgB,IAAI;AAAA,EACtB;AACA,QAAM,SAA4B,EAAE,GAAG,sBAAsB,GAAG,WAAW;AAC3E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,CAAC,SAAS;AACZ,WAAO,oBAAC,UAAK,WAAW,aAAa,eAAW,MAAC;AAAA,EACnD;AAEA,MAAI,YAAY,QAAQ;AACtB,WACE,qBAAC,gBACC;AAAA,0BAAC,uBAAoB,SAAO,MAC1B,8BAAC,YAAO,MAAK,UAAS,WAAW,aAAa,cAAY,OAAO,aAC9D,4BAAkB,SAAS,oBAAC,QAAK,WAAU,UAAS,IAAK,oBAAC,OAAI,WAAU,UAAS,GACpF,GACF;AAAA,MACA,qBAAC,uBAAoB,OAAM,OACzB;AAAA,6BAAC,oBAAiB,SAAS,MAAM,OAAO,OAAO,GAAG,WAAW,GAAG,UAAU,WAAW,WAAW,GAC9F;AAAA,8BAAC,OAAI,WAAU,UAAS;AAAA,UACvB,OAAO;AAAA,WACV;AAAA,QACA,qBAAC,oBAAiB,SAAS,MAAM,OAAO,MAAM,GAAG,WAAW,GAAG,UAAU,UAAU,WAAW,GAC5F;AAAA,8BAAC,QAAK,WAAU,UAAS;AAAA,UACxB,OAAO;AAAA,WACV;AAAA,QACA,qBAAC,oBAAiB,SAAS,MAAM,OAAO,QAAQ,GAAG,WAAW,GAAG,UAAU,YAAY,WAAW,GAChG;AAAA,8BAAC,WAAQ,WAAU,UAAS;AAAA,UAC3B,OAAO;AAAA,WACV;AAAA,SACF;AAAA,OACF;AAAA,EAEJ;AAEA,QAAM,YAAyB,kBAAkB,SAAS,UAAU;AAEpE,SACE,qBAAC,WACC;AAAA,wBAAC,kBAAe,SAAO,MACrB,8BAAC,YAAO,MAAK,UAAS,SAAS,MAAM,OAAO,SAAS,GAAG,WAAW,aAAa,cAAY,OAAO,aAChG,4BAAkB,SAAS,oBAAC,OAAI,WAAU,UAAS,IAAK,oBAAC,QAAK,WAAU,UAAS,GACpF,GACF;AAAA,IACA,oBAAC,kBAAe,MAAK,UAAU,wBAAc,SAAS,OAAO,YAAY,OAAO,YAAW;AAAA,KAC7F;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comitor/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"class-variance-authority": "^0.7.1",
|
|
134
134
|
"clsx": "^2.1.1",
|
|
135
135
|
"cmdk": "1.1.1",
|
|
136
|
-
"date-fns": "4.
|
|
136
|
+
"date-fns": "4.4.0",
|
|
137
137
|
"embla-carousel-react": "8.6.0",
|
|
138
138
|
"input-otp": "1.4.2",
|
|
139
139
|
"react-day-picker": "9.13.2",
|
|
@@ -151,8 +151,8 @@
|
|
|
151
151
|
"@types/react-dom": "^19.0.0",
|
|
152
152
|
"@uppy/core": "^6.0.0",
|
|
153
153
|
"@uppy/xhr-upload": "^6.0.0",
|
|
154
|
-
"lucide-react": "^1.
|
|
155
|
-
"next": "^16.
|
|
154
|
+
"lucide-react": "^1.41.0",
|
|
155
|
+
"next": "^16.3.4",
|
|
156
156
|
"next-themes": "^0.4.6",
|
|
157
157
|
"publint": "^0.3.21",
|
|
158
158
|
"react": "^19.0.0",
|