@comitor/ui 0.2.1 → 0.3.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.
Files changed (90) hide show
  1. package/README.md +236 -30
  2. package/dist/chart/chart-colors.d.ts +56 -0
  3. package/dist/chart/chart-colors.js +21 -0
  4. package/dist/chart/chart-colors.js.map +1 -0
  5. package/dist/chart/chart.d.ts +40 -0
  6. package/dist/chart/chart.js.map +1 -1
  7. package/dist/chart/comitor-chart.d.ts +21 -43
  8. package/dist/chart/comitor-chart.js +13 -24
  9. package/dist/chart/comitor-chart.js.map +1 -1
  10. package/dist/chart/index.d.ts +2 -1
  11. package/dist/chart/index.js +1 -0
  12. package/dist/chart/index.js.map +1 -1
  13. package/dist/components/composite/data-table-constants.d.ts +40 -0
  14. package/dist/components/composite/data-table-constants.js +13 -0
  15. package/dist/components/composite/data-table-constants.js.map +1 -0
  16. package/dist/components/composite/data-table.d.ts +2 -27
  17. package/dist/components/composite/data-table.js +1 -10
  18. package/dist/components/composite/data-table.js.map +1 -1
  19. package/dist/components/composite/date-picker-constants.d.ts +21 -0
  20. package/dist/components/composite/date-picker-constants.js +39 -0
  21. package/dist/components/composite/date-picker-constants.js.map +1 -0
  22. package/dist/components/composite/date-picker.d.ts +2 -18
  23. package/dist/components/composite/date-picker.js +6 -48
  24. package/dist/components/composite/date-picker.js.map +1 -1
  25. package/dist/components/composite/form-field-constants.d.ts +26 -0
  26. package/dist/components/composite/form-field-constants.js +13 -0
  27. package/dist/components/composite/form-field-constants.js.map +1 -0
  28. package/dist/components/composite/form-field.d.ts +1 -13
  29. package/dist/components/composite/form-field.js +1 -11
  30. package/dist/components/composite/form-field.js.map +1 -1
  31. package/dist/components/composite/search-filter-bar-constants.d.ts +52 -0
  32. package/dist/components/composite/search-filter-bar-constants.js +18 -0
  33. package/dist/components/composite/search-filter-bar-constants.js.map +1 -0
  34. package/dist/components/composite/search-filter-bar.d.ts +3 -37
  35. package/dist/components/composite/search-filter-bar.js +2 -15
  36. package/dist/components/composite/search-filter-bar.js.map +1 -1
  37. package/dist/components/composite/table-pagination-constants.d.ts +56 -0
  38. package/dist/components/composite/table-pagination-constants.js +18 -0
  39. package/dist/components/composite/table-pagination-constants.js.map +1 -0
  40. package/dist/components/composite/table-pagination.d.ts +2 -43
  41. package/dist/components/composite/table-pagination.js +4 -15
  42. package/dist/components/composite/table-pagination.js.map +1 -1
  43. package/dist/components/ui/badge.js +3 -3
  44. package/dist/components/ui/badge.js.map +1 -1
  45. package/dist/components/ui/button.js +4 -4
  46. package/dist/components/ui/button.js.map +1 -1
  47. package/dist/components/ui/calendar.js +1 -1
  48. package/dist/components/ui/calendar.js.map +1 -1
  49. package/dist/components/ui/checkbox.js +2 -2
  50. package/dist/components/ui/checkbox.js.map +1 -1
  51. package/dist/components/ui/command.js +1 -1
  52. package/dist/components/ui/command.js.map +1 -1
  53. package/dist/components/ui/input-group.js +1 -1
  54. package/dist/components/ui/input-group.js.map +1 -1
  55. package/dist/components/ui/input-otp.js +1 -1
  56. package/dist/components/ui/input-otp.js.map +1 -1
  57. package/dist/components/ui/navigation-menu.js +1 -1
  58. package/dist/components/ui/navigation-menu.js.map +1 -1
  59. package/dist/components/ui/radio-group.js +1 -1
  60. package/dist/components/ui/radio-group.js.map +1 -1
  61. package/dist/components/ui/resizable.js +1 -1
  62. package/dist/components/ui/resizable.js.map +1 -1
  63. package/dist/components/ui/slider.js +1 -1
  64. package/dist/components/ui/slider.js.map +1 -1
  65. package/dist/components/ui/switch.js +1 -1
  66. package/dist/components/ui/switch.js.map +1 -1
  67. package/dist/components/ui/textarea.js +1 -1
  68. package/dist/components/ui/textarea.js.map +1 -1
  69. package/dist/index.d.ts +11 -6
  70. package/dist/index.js +5 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/shell/contrast-constants.d.ts +30 -0
  73. package/dist/shell/contrast-constants.js +16 -0
  74. package/dist/shell/contrast-constants.js.map +1 -0
  75. package/dist/shell/contrast-provider.d.ts +60 -0
  76. package/dist/shell/contrast-provider.js +61 -0
  77. package/dist/shell/contrast-provider.js.map +1 -0
  78. package/dist/shell/contrast-toggle.d.ts +24 -0
  79. package/dist/shell/contrast-toggle.js +29 -0
  80. package/dist/shell/contrast-toggle.js.map +1 -0
  81. package/dist/shell/index.d.ts +3 -0
  82. package/dist/shell/index.js +3 -0
  83. package/dist/shell/index.js.map +1 -1
  84. package/dist/shell/shell-context.js +3 -2
  85. package/dist/shell/shell-context.js.map +1 -1
  86. package/dist/tokens.d.ts +170 -70
  87. package/dist/tokens.js +124 -47
  88. package/dist/tokens.js.map +1 -1
  89. package/package.json +3 -2
  90. package/styles.css +255 -120
package/README.md CHANGED
@@ -53,6 +53,11 @@ pnpm add react-hook-form # cho @comitor/ui/form
53
53
  >
54
54
  > `lucide-react` cũng là `>=` vì cùng lý do: thang phiên bản của nó nhảy `0.x` → `1.x` mà API
55
55
  > icon không đổi. Đã kiểm typecheck + build với `1.33.0`.
56
+ >
57
+ > **Hiệu ứng vẽ dần của recharts 2.15.x hỏng với React 19.2**, nên `ComitorAreaChart` /
58
+ > `ComitorBarChart` / `ComitorLineChart` mặc định **tắt** hiệu ứng từ `0.2.2` (§4.4). Nếu tự lắp
59
+ > recharts trong `<ChartContainer>` thì gói không tắt hộ được — app phải tự ghi
60
+ > `isAnimationActive={false}` lên từng `<Bar>`/`<Line>`/`<Area>` (§4.4 mục 1b).
56
61
 
57
62
  ## 2. Nạp `styles.css` — ĐỌC KỸ, đây là chỗ dễ sai nhất
58
63
 
@@ -334,11 +339,13 @@ Luật khi đã có nó:
334
339
  > sang render phía client khi build tĩnh. Cái giá đó phải do app chọn, không phải hệ quả của việc
335
340
  > import shell.
336
341
 
337
- > ⚠ Entry `/shell` chỉ nạp được **qua bundler** (webpack/turbopack/vite). Nạp bằng Node ESM thuần sẽ
338
- > lỗi `Cannot find module 'next/link'` — vì `next@16` không khai `exports` map nên Node không resolve
339
- > được subpath không đuôi. Test runner chạy resolution kiểu Node thì alias `next/link` → `next/link.js`,
340
- > hoặc dùng `ShellProvider` với `LinkComponent`/`pathname` tự cấp. Các entry `.`, `/chart`, `/form`,
341
- > `/tokens` **không** dính vấn đề này.
342
+ > **Từ `0.2.2`, cả 5 entry — kể cả `/shell` — nạp được bằng Node ESM thuần.** Ở `0.2.1` thì không:
343
+ > `next@16` không khai `exports` map, nên với Node thì `next/link` chỉ là một đường dẫn file thiếu
344
+ > đuôi và `import("@comitor/ui/shell")` chết ngay lúc nạp
345
+ > (`ERR_MODULE_NOT_FOUND … Did you mean next/link.js?`), trong khi app Next không hề hấn vì bundler
346
+ > tự thêm đuôi. Nay shell ghi thẳng `next/link.js` / `next/navigation.js` — **cùng một file** mà
347
+ > bundler vẫn resolve tới, nên client-side navigation và `usePathname()` không đổi.
348
+ > `pnpm smoke:entries` canh lại điều này ở mỗi lần publish.
342
349
 
343
350
  ### 4.3 Token ngoài Tailwind
344
351
 
@@ -359,11 +366,130 @@ const { background, foreground } = getAvatarToneColors("Nguyễn Đức Thành",
359
366
  > Tailwind → trả hex đục đã trộn sẵn tint). Cùng bảng tone, cùng hàm băm, nên một người ra một màu ở cả
360
367
  > hai nơi. **Đừng** tự băm tên rồi bốc màu từ `accent` — đó là đường ngắn nhất để web và email lệch màu.
361
368
 
369
+ ### 4.4 Biểu đồ, và hằng của gói khi dùng ở Server Component
370
+
371
+ Hai điều phải biết trước khi vẽ biểu đồ đầu tiên. Cả hai đều do app mẫu `comitor-starter` phát
372
+ hiện bằng cách *dùng thật*, và cả hai đều IM LẶNG — không có lỗi, không có warning. Mục 1 có **hai
373
+ nửa**: một nửa gói tự lo (`Comitor*Chart`), một nửa app phải tự lo (`ChartContainer` + recharts tự
374
+ lắp) — đừng đọc nửa đầu rồi bỏ nửa sau.
375
+
376
+ **1. Hiệu ứng vẽ dần mặc định TẮT (từ `0.2.2`).** Với `recharts` 2.15.x + React 19.2, hiệu ứng
377
+ không bao giờ chạy tới khung hình cuối, nên mark đứng nguyên ở khung đầu — tức là vô hình. Đo trên
378
+ DOM thật, ba dạng cùng một lỗi:
379
+
380
+ | | biểu hiện trong SVG | thấy trên màn hình |
381
+ |---|---|---|
382
+ | `<Bar>` | `<g class="recharts-bar-rectangle">` **rỗng** (`Rectangle` trả `null` khi cao 0) | không có cột |
383
+ | `<Line>` | path có `d` đúng nhưng `stroke-dasharray="0px 634px"` | không có đường |
384
+ | `<Area>` | path có `d` đúng nhưng `<clipPath><rect width="0">` | không có vùng |
385
+
386
+ Kết quả giống hệt nhau: đủ trục, đủ lưới, đủ chú giải, KHÔNG có dữ liệu — và chờ bao lâu cũng
387
+ không tự hết. Nay ba component nhận prop **optional** `isAnimationActive`, mặc định `false`:
388
+
389
+ ```tsx
390
+ <ComitorBarChart data={data} xAxisKey="thang" series={series} /> // hiện ngay
391
+ <ComitorBarChart data={data} xAxisKey="thang" series={series} isAnimationActive /> // bật lại
392
+ ```
393
+
394
+ Bật lại chỉ nên làm khi app chạy **recharts 3.x**; trên 2.15.x nó dựng lại đúng cái biểu đồ trống
395
+ ở trên.
396
+
397
+ **1b. Tự lắp recharts (`ChartContainer`) thì PHẢI TỰ TẮT — gói không đặt hộ được.** Mặc định mới ở
398
+ trên chỉ có hiệu lực bên trong ba wrapper `Comitor*Chart`. Entry `/chart` còn một đường dùng thứ
399
+ hai, đúng lối shadcn quen thuộc: `<ChartContainer>` + recharts do app tự lắp. Ở đường đó
400
+ `<Bar>`/`<Line>`/`<Area>` là **children do app viết**, container không sờ vào được, nên trên
401
+ `recharts` 2.15.x app gặp lại y nguyên lỗi im lặng của `0.2.1` — đủ trục, đủ lưới, KHÔNG có dữ liệu:
402
+
403
+ ```tsx
404
+ "use client";
405
+ import { ChartContainer } from "@comitor/ui/chart";
406
+ import { Bar, BarChart, XAxis } from "recharts";
407
+
408
+ // ✗ recharts 2.15.x — <g class="recharts-bar-rectangle"> rỗng, không cột nào
409
+ <ChartContainer config={{ doanhThu: { label: "Doanh thu", color: "var(--chart-1)" } }}>
410
+ <BarChart data={data}>
411
+ <XAxis dataKey="thang" />
412
+ <Bar dataKey="doanhThu" fill="var(--color-doanhThu)" />
413
+ </BarChart>
414
+ </ChartContainer>
415
+
416
+ // ✓ tắt tay, trên TỪNG mark
417
+ <Bar dataKey="doanhThu" fill="var(--color-doanhThu)" isAnimationActive={false} />
418
+ <Line dataKey="doanhThu" stroke="var(--color-doanhThu)" isAnimationActive={false} />
419
+ <Area dataKey="doanhThu" fill="var(--color-doanhThu)" isAnimationActive={false} />
420
+ ```
421
+
422
+ Đo trên DOM thật (Next 16.3.2 · React 19.2.8 · recharts 2.15.4, `next build` + `next start`, chờ
423
+ 5000ms sau hydrate), cùng một trang chỉ khác một prop:
424
+
425
+ | mark trong `<ChartContainer>` | thiếu prop | có `isAnimationActive={false}` |
426
+ |---|---|---|
427
+ | `<Bar>` | 3 `<g.recharts-bar-rectangle>`, **0** `path.recharts-rectangle` | **3/3** path, cao 273/410/342px |
428
+ | `<Line>` | `stroke-dasharray="0px 818.84px"` | `stroke-dasharray` = `null` |
429
+ | `<Area>` | `<clipPath><rect width="0">` | không còn rect `width="0"` |
430
+
431
+ Trên **recharts 3.x** thì bỏ prop đi cũng được — lỗi không tồn tại ở đó. Nhưng dải peer của gói là
432
+ `>=2.15.0`, nên cứ ghi `isAnimationActive={false}`: trên 3.x nó chỉ còn nghĩa "không hoạt hình".
433
+
434
+ **2. `CHART_COLORS` (và mọi hằng khác của gói) đọc được từ Server Component.** Ở `0.2.1` thì
435
+ không: hai hằng đó export từ `chart/comitor-chart.tsx`, mà file đó có `"use client"`. Bundler RSC
436
+ cắt ranh giới theo FILE, nên mọi export của một module client tới phía server đều là
437
+ client-reference proxy — `CHART_COLORS.gold` ra `undefined`, **không throw, không warning**, trong
438
+ khi TypeScript vẫn khai kiểu literal. Kịch bản gãy đúng như JSDoc của gói đang khuyên viết:
439
+
440
+ ```tsx
441
+ // page.tsx — Server Component
442
+ const series = [{ key: "loi", color: CHART_COLORS.red }]; // 0.2.1: color = undefined (im lặng)
443
+ ```
444
+
445
+ `0.2.2` chuyển **10 hằng** sang module không có directive (`chart/chart-colors.ts`,
446
+ `components/composite/*-constants.ts`) và cho barrel xuất thẳng từ đó. Tên export không đổi, chỗ
447
+ import không đổi:
448
+
449
+ | Đọc từ Server Component | `0.2.1` | `0.2.2` |
450
+ |---|---|---|
451
+ | `CHART_COLORS.gold` | `undefined` | `"var(--chart-1)"` |
452
+ | `CHART_SERIES_COLORS[0]` | `undefined` | `"var(--chart-1)"` |
453
+ | `DEFAULT_DATA_TABLE_LABELS.emptyTitle` | `undefined` | `"Chưa có dữ liệu"` |
454
+ | `VI_DATE_FORMAT` | `undefined` | `"dd/MM/yyyy"` |
455
+ | `DEFAULT_DATE_PRESETS.length` | `0` | `3` |
456
+ | `DEFAULT_DATE_RANGE_PRESETS.length` | `0` | `5` |
457
+ | `DEFAULT_PAGE_SIZE_OPTIONS[0]` | `undefined` | `20` |
458
+ | `SELECT_EMPTY_VALUE` | `undefined` | `"__empty__"` |
459
+ | `DEFAULT_TABLE_PAGINATION_LABELS.region` | `undefined` | `"Phân trang"` |
460
+ | `DEFAULT_SEARCH_FILTER_BAR_LABELS.searchPlaceholder` | `undefined` | `"Tìm kiếm…"` |
461
+
462
+ `toSelectValue` / `fromSelectValue` đi cùng `SELECT_EMPTY_VALUE`, nên nay gọi được trong Server
463
+ Action. `pnpm verify:directives` canh không cho hằng thuần lọt ngược vào file `"use client"`.
464
+
465
+ > **Vẫn còn nằm sau ranh giới client — 13 tên, và tất cả đều BÁO LỖI TO, không im lặng.** Chúng là
466
+ > hàm export từ file `"use client"`; gọi ở Server Component ném `Attempted to call <tên>() from the
467
+ > server but <tên> is on the client`. Đã kiểm bằng trang Server Component prerender thật
468
+ > (Next 16.3.2, `next build`, đọc HTML sinh ra trên đĩa): **13/13 ném đúng lỗi đó, 10/10 đối chứng
469
+ > chạy bình thường**.
470
+ >
471
+ > | | tên | vì sao vẫn ở đó |
472
+ > |---|---|---|
473
+ > | dựng chuỗi class (`cva`) | `buttonVariants`, `toggleVariants`, `fieldVariants`, `inputGroupAddonVariants`, `inputGroupButtonVariants` | sống cùng file với component client tương ứng |
474
+ > | gọi lệnh toast | `toast`, `dismissToast`, `toastPromise` | vốn chỉ có nghĩa trong trình duyệt |
475
+ > | hook | `useIsMac`, `useCarousel`, `useShell` (`/shell`), `useChart` (`/chart`), `useFormField` (`/form`) | hook thì đương nhiên client |
476
+ >
477
+ > Nhóm đầu là nhóm dễ vấp nhất, vì chúng chỉ ghép chuỗi class — không có lý do trực giác nào để
478
+ > chúng không chạy ở trang server. Muốn dựng class nút cho một `<a>` ở trang server thì để phần đó
479
+ > trong một client component.
480
+ >
481
+ > Chín helper `cva` còn lại **KHÔNG** kẹt — `alertVariants`, `badgeVariants`, `buttonGroupVariants`,
482
+ > `cardVariants`, `emptyMediaVariants`, `itemVariants`, `itemMediaVariants`,
483
+ > `navigationMenuTriggerStyle`, `statCardIconVariants`, cùng `cn`, đều gọi được từ Server Component
484
+ > (đã đo cùng lượt, cùng trang). Danh sách trên là đầy đủ, không phải trích: nó dựng từ đồ thị
485
+ > re-export của cả 5 entry, lọc mọi tên có giá trị runtime mà khai báo nằm trong file `"use client"`.
486
+
362
487
  ## 5. Bảng component
363
488
 
364
- Số liệu thực tế của `0.2.1`: **51 file primitive · 24 file composite · 18 file shell · 2 file chart**
489
+ Số liệu thực tế của `0.2.2`: **51 file primitive · 29 file composite · 18 file shell · 3 file chart**
365
490
  (chưa kể barrel) — số tên xuất ra theo entry: `.` **637** · `/shell` **70** · `/chart` **29** · `/form`
366
- **15** · `/tokens` **15** (xem §3 cho các tên xuất hiện ở nhiều entry). Bảng dưới liệt kê component gốc;
491
+ **15** · `/tokens` **15** (xem §3 cho các tên xuất hiện ở nhiều entry). `0.2.2` **không thêm/bớt tên
492
+ nào**; số file tăng vì hằng dữ liệu được tách sang module riêng (§4.4). Bảng dưới liệt kê component gốc;
367
493
  các thành phần con (`CardHeader`, `DialogFooter`, `SelectItem`…) export kèm theo file tương ứng.
368
494
 
369
495
  ### Tier 1 — primitive (`@comitor/ui`)
@@ -458,7 +584,8 @@ Kèm 2 module thuần (không phải component, dùng được cả hai phía):
458
584
  | `AppLauncher`, `AppLauncherPanel` | **Trục 2** — lưới 3×3 đổi sản phẩm; app chưa mua hiện khoá và gọi `onUpsell` |
459
585
  | `AppHeader`, `Sidebar`, `SidebarGroup`, `SidebarMenuItem`, `MobileMenu` | Thành phần khung điều hướng |
460
586
  | `UserMenu`, `UserAvatar`, `NotificationsButton` | Vùng phải của header |
461
- | `ThemeProvider`, `ThemeToggle`, `SonnerToaster` | Theme (next-themes) + toast bám theme |
587
+ | `ThemeProvider`, `ThemeToggle`, `SonnerToaster` | Theme sáng/tối (next-themes) + toast bám theme |
588
+ | `ContrastProvider`, `ContrastToggle`, `useContrast`, `ContrastScript` | **Trục thứ hai của giao diện**: chọn BẢNG MÀU (bản duyệt comitor-ds ⇄ bản tương phản cao). Độc lập với sáng/tối; lưu ở localStorage `comitor-contrast`. Provider tự render script chống nháy — xem §6.0 |
462
589
  | `ShellNavLink` | Link nội bộ của shell; thay `next/link` bằng `LinkComponent` khi không chạy Next |
463
590
  | `ComitorLogo`, `ComitorLockup` | Vector brand — **không** có `"use client"`, RSC render thẳng (hex trong SVG là brand book, không tokenize) |
464
591
  | `DEFAULT_SHELL_LABELS` + `utils` | Chuỗi hiển thị mặc định (`ShellLabels`, 32 khoá); helper `flattenNavItems`, `isNavItemActive`, `isNavBranchActive`, `toNavGroups`, `resolveCurrentApp`, `getEntitledApps`, `getLockedApps`, `appAccentStyle`, `normalizeVi`, `getVietnameseInitials` |
@@ -470,10 +597,11 @@ Hợp đồng dữ liệu (type): `Workspace`, `AppDescriptor`, `NavItem`, `NavG
470
597
 
471
598
  | Component | Công dụng |
472
599
  |---|---|
473
- | `ChartContainer`, `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, `ChartStyle`, `useChart` | Lớp shadcn bọc recharts (giữ tên trần theo API gốc) |
600
+ | `ChartContainer`, `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, `ChartStyle`, `useChart` | Lớp shadcn bọc recharts (giữ tên trần theo API gốc). ⚠ Tự lắp `<Bar>`/`<Line>`/`<Area>` ở đây thì trên **recharts 2.15.x phải tự ghi `isAnimationActive={false}` lên từng mark** — container không đặt hộ được, quên là biểu đồ trống không báo lỗi (§4.4 mục 1b) |
474
601
  | `ComitorAreaChart`, `ComitorBarChart`, `ComitorLineChart` | Bản dựng sẵn theo brand — truyền `data` + `series` là xong |
475
602
  | `ComitorChartContainer`, `ComitorChartTooltip` | Bản tiện dụng; **có tiền tố `Comitor`** để không đụng tên trần ở trên |
476
- | `CHART_COLORS`, `CHART_SERIES_COLORS` | Bảng màu series mặc định |
603
+ | `CHART_COLORS`, `CHART_SERIES_COLORS` | Bảng màu series mặc định — **hằng dữ liệu thuần, đọc được từ Server Component** (§4.4) |
604
+ | `ChartColorName` | Khoá của `CHART_COLORS` |
477
605
 
478
606
  ### Form (`@comitor/ui/form`)
479
607
 
@@ -530,6 +658,63 @@ trong component.
530
658
 
531
659
  Toàn bộ token nằm ở [`styles.css`](./styles.css); bản mirror TS ở [`src/tokens.ts`](./src/tokens.ts).
532
660
 
661
+ ### 6.0 HAI bảng màu — đọc trước §6.1
662
+
663
+ Gói có **hai** bảng màu, cùng một bộ component:
664
+
665
+ | | Bật bằng | Là gì |
666
+ |---|---|---|
667
+ | **Mặc định** | (không cần gì) | Bảng của **comitor-ds** — repo design system đã được duyệt. Đây là nguồn sự thật về hình thức. |
668
+ | **Tương phản cao** | `<html data-contrast="high">` | Bảng đã đo WCAG (chính là bảng của `0.2.x`). Dành cho màn hình độ sáng thấp, màn hình rẻ, hoặc dùng ngoài trời. |
669
+
670
+ Bảng mặc định **cố ý có những cặp dưới ngưỡng** — đó là quyết định của người duyệt, không phải lỗi bỏ
671
+ sót. Danh sách đầy đủ nằm ở khối `NGUỒN SỰ THẬT` đầu `styles.css`; những khoản chính:
672
+
673
+ ```
674
+ viền control (--input) trên nền trang 1,16:1 (1.4.11 đòi 3:1)
675
+ rãnh Switch BẬT, gạch active Tabs (gold) 1,86:1 (1.4.11 đòi 3:1)
676
+ --muted-foreground trên --muted 3,58:1 (1.4.3 đòi 4,5:1)
677
+ chữ trắng trên nền --destructive 4,38:1 (1.4.3 đòi 4,5:1)
678
+ Button variant link (--gold-400) 3,15:1 (1.4.3 đòi 4,5:1)
679
+ ```
680
+
681
+ Hai ngoại lệ giữ bản sửa ở **cả hai** bảng, vì màu chữ của bản duyệt gần trùng màu nền tới mức không
682
+ đọc nổi trên bất kỳ màn hình nào: `Badge` variant `warning` (1,52:1 → 6,24:1) và `success`
683
+ (2,82:1 → 5,88:1). Nền giữ nguyên của bản duyệt, chỉ đậm chữ.
684
+
685
+ **Cách bật trong app** — hai bước, app không sở hữu màu nào:
686
+
687
+ ```tsx
688
+ // app/layout.tsx (Server Component)
689
+ import { ContrastProvider, ThemeProvider } from "@comitor/ui/shell";
690
+
691
+ <html lang="vi" suppressHydrationWarning>
692
+ <body>
693
+ <ThemeProvider>
694
+ <ContrastProvider>{children}</ContrastProvider>
695
+ </ThemeProvider>
696
+ </body>
697
+ </html>
698
+ ```
699
+
700
+ ```tsx
701
+ // trang cài đặt (client)
702
+ import { ContrastToggle } from "@comitor/ui/shell";
703
+
704
+ <ContrastToggle />
705
+ ```
706
+
707
+ `ContrastProvider` tự render script chống nháy trong `<body>` — app **không** phải thêm gì vào
708
+ `<head>`, và **đừng** đặt `<ContrastScript />` vào `<head>`: React 19 báo lỗi console ở mọi lần
709
+ render client (script inline không nằm trong nhóm phần tử React hoisted được).
710
+
711
+ > ⚠ **Điều kiện để hai bảng cùng sống**: mọi component đi qua **token vai trò**
712
+ > (`--control-on`, `--control-edge`, `--control-check`, `--switch-track`, `--slider-track`,
713
+ > `--input-fill`, `--primary-ink`, `--link-ink`…) chứ **không** viết thẳng `bg-gold-300` /
714
+ > `bg-ash-300` / `text-gold-400`. Viết thẳng thang màu là khoá cứng một bảng, và bảng kia lặng lẽ
715
+ > sai. Cổng `pnpm lint:a11y` bắt được lỗi này vì nó đo **bảng tương phản cao**: giá trị viết thẳng
716
+ > không đổi theo bảng nên sẽ hiện ra ở đó đúng bằng con số của bảng mặc định — tức là đỏ.
717
+
533
718
  ### 6.1 Hợp đồng BA VAI TRÒ của màu — đọc trước khi dùng bất kỳ màu nào
534
719
 
535
720
  Đây là quy tắc quan trọng nhất của gói. Mỗi màu có **ba** biến thể, đọc tên là biết dùng ở đâu:
@@ -550,9 +735,15 @@ green #2EA56A trên #E0F2E9 = 2,69:1 ✗ → --green-ink #216A48 = 5,60:1
550
735
  red #D64545 trên #F9E3E3 = 3,57:1 ✗ → --red-ink #9A3537 = 5,83:1 ✓
551
736
  ```
552
737
 
553
- Ở **dark** không có bậc riêng: khối `.dark` vốn đã nâng `--teal/--green/--red/--yellow` lên mức làm chữ
554
- được, nên bên đó `--x-ink` chỉ trỏ về `var(--x)`. Nhờ vậy `text-teal-ink` viết **một lần** là đúng cả
555
- hai theme, không cần biến thể `dark:`.
738
+ Ở **dark** không có bậc riêng: `--x-ink` trỏ về `var(--x)`. Nhờ vậy `text-teal-ink` viết **một lần**
739
+ là đúng cả hai theme, không cần biến thể `dark:`. Ở bảng **mặc định**, comitor-ds dùng cùng một hex cho
740
+ cả hai theme nên bậc ink ở dark bằng bậc ở light; ở bảng **tương phản cao**, khối
741
+ `.dark[data-contrast="high"]` nâng `--teal/--green/--red/--yellow` lên bậc sáng và `--x-ink` đi theo.
742
+
743
+ > ⚠ Hợp đồng `-ink` chỉ còn ràng buộc chặt ở các cụm **tonal** mà comitor-ds không có (`Badge`
744
+ > success/warning, `StatusPill`, `LetterAvatar`, `StatCard`, `Toast`) — đó là chỗ chữ đứng trên tint
745
+ > của chính nó và bậc ink là thứ duy nhất giữ chữ đọc được. Với primitive **có** trong comitor-ds, bản
746
+ > duyệt thắng: ở bảng mặc định `--primary-ink` = `--primary` và `--destructive-ink` = `--destructive`.
556
747
 
557
748
  Ba vai trò áp cho mọi họ token:
558
749
 
@@ -563,29 +754,36 @@ Ba vai trò áp cho mọi họ token:
563
754
  | Brand | `--primary` | `--primary-foreground` | `--primary-ink` |
564
755
  | Accent theo app | `--app-accent` | `--app-accent-foreground` | `--app-accent-ink` (mặc định `= --primary-ink`) |
565
756
 
566
- Brand cũng cần bậc `-ink` vì đúng lý do trên: gold `#E8B824` là màu **sáng** nên tô nền thì tuyệt, còn
567
- làm ranh giới trạng thái trên nền trang chỉ được **1,86:1** ✗. `--primary-ink` mượn `--gold-500`
568
- `#7A5800` ở light (**6,51:1** ✓) và trỏ về chính `--primary` ở dark (**12,67:1** ✓). Đây là màu của
569
- rãnh `Switch` khi BẬT và ô `Checkbox` đã chọn. `--ring` **cố ý vẫn là** `--primary`: vòng focus thuộc
570
- tiêu chí AAA của WCAG 2.2, ngoài mục tiêu AA của gói.
757
+ Brand cũng có bậc `-ink`, nhưng **giá trị của nó phụ thuộc bảng màu** (§6.0): gold `#E8B824` là màu
758
+ **sáng** nên tô nền thì tuyệt, còn làm ranh giới trạng thái trên nền trang chỉ được **1,86:1**.
759
+ comitor-ds chấp nhận con số đó, nên ở bảng mặc định `--primary-ink` = `--primary`; ở bảng tương phản
760
+ cao nó mượn `--gold-500` `#7A5800` (**6,51:1** ✓ light) và trỏ về `--primary` ở dark (**12,67:1** ✓).
761
+ Đây là màu của rãnh `Switch` khi BẬT và ô `Checkbox` đã chọn — component viết `bg-control-on`, token
762
+ đó trỏ vào `--primary-ink`. `--ring` **cố ý vẫn là** `--primary` ở cả hai bảng: vòng focus thuộc tiêu
763
+ chí AAA của WCAG 2.2, ngoài mục tiêu của gói.
571
764
 
572
- Ba lỗi hay gặp nhất, và cách sửa:
765
+ Các lỗi hay gặp nhất, và cách sửa. **Số đo trong bảng là của bảng TƯƠNG PHẢN CAO** — đó là bảng có hợp
766
+ đồng; ở bảng mặc định vài cặp thấp hơn theo đúng bản duyệt (§6.0):
573
767
 
574
768
  | Viết sai | Hỏng ở đâu | Viết đúng |
575
769
  |---|---|---|
576
770
  | `text-red` cho dòng báo lỗi | 4,38:1 ở light ✗; ở dark thì đây là thang brand thô | `text-destructive-ink` |
577
771
  | `text-success` / `text-info` / `text-warning` cho chữ hoặc icon trạng thái | Token TÔ NỀN: trên tint `/15` của chính nó chỉ 2,69:1 / 3,44:1 ✗ ở light; trên nền trang 3,13:1 / 4,14:1 ✗ | `text-success-ink` / `text-info-ink` / `text-warning-ink` (5,5–6,7:1 light · 7,1–12,4:1 dark) |
578
- | `bg-primary` làm rãnh `Switch` khi BẬT / ô `Checkbox` đã chọn | 1,86:1 với nền trắng ✗ — trạng thái BẬT chỉ còn phân biệt bằng vị trí núm | `bg-primary-ink` (6,51:1 light / 12,67:1 dark) |
579
- | `border-border` làm viền của control | `--border` là ranh giới **TRANG TRÍ**, 1,16:1 ✗ | `border-input` (= `--control-edge`, 3,83:1 / 4,94:1) |
772
+ | `bg-primary` / `bg-gold-300` làm rãnh `Switch` khi BẬT | Khoá cứng một bảng màu: bảng tương phản cao không đổi được nó nữa | `bg-control-on` |
773
+ | `bg-ash-300` làm rãnh `Switch` khi TẮT, `bg-muted` làm rãnh `Slider` | Cùng lý do — thang màu thô không theo bảng | `bg-switch-track` / `bg-slider-track` |
774
+ | `text-ink` làm dấu check của `Checkbox` | Ở bảng tương phản cao nền ô là gold-500 TỐI, dấu mực chỉ 1,86:1 ✗ | `text-control-check` |
775
+ | `border-border` làm viền của control | `--border` là ranh giới **TRANG TRÍ**; `--control-edge` mới là thứ bảng tương phản cao kéo đậm lên | `border-input` (= `--control-edge`) |
776
+ | `bg-input/30` làm lớp tô ô nhập ở dark | Ở bảng tương phản cao `--input` là #7E8290, tô 30% ra mảng xám sáng làm placeholder tụt còn 3,22:1 ✗ | `dark:bg-input-fill` |
580
777
  | `text-app-accent` cho icon/vạch active | accent mặc định là gold `#E8B824` → 1,7–1,9:1 trên nền sidebar ✗ | `text-app-accent-ink`, hoặc nền ĐẶC + `text-app-accent-foreground` |
581
- | `text-destructive` cho chữ trên nền trang | `--destructive` là màu **NỀN** (ghim một giá trị đậm cho chữ trắng đứng lên); dùng làm chữ ở dark chỉ 3,76:1 ✗ | `text-destructive-ink` (7,15:1 light / 6,42:1 dark) |
778
+ | `text-destructive` cho chữ trên nền trang | `--destructive` là màu **NỀN**; ở bảng tương phản cao nó là bậc đậm cho chữ trắng đứng lên và làm chữ thì chỉ 3,76:1 ✗ ở dark | `text-destructive-ink` (7,15:1 light / 6,42:1 dark ở bảng tương phản cao; = `--destructive` ở bảng mặc định, đúng `text-destructive` của comitor-ds) |
582
779
 
583
780
  > ⚠ **App ghi đè `--app-accent` thì PHẢI set kèm `--app-accent-ink`** (và `--app-accent-foreground` nếu
584
781
  > accent sáng). Gói cố ý **không** dẫn xuất `-ink` bằng `color-mix` từ `--app-accent`: dẫn xuất tự động
585
782
  > cho ra con số tương phản không ai kiểm, và mỗi màu nền cần một hệ số khác nhau.
586
783
  >
587
- > ⚠ **Ghi đè `--primary` cũng PHẢI set kèm `--primary-ink`** — cùng một luật, cùng một lý do. Quên thì
588
- > công tắc/checkbox vẫn đọc được nhưng còn màu gold mặc định: sai màu, không sai chuẩn.
784
+ > ⚠ **Ghi đè `--primary` thì nên set kèm `--primary-ink`** nếu app muốn một bậc tối riêng cho brand
785
+ > của mình. Ở bảng mặc định hai biến này trùng nhau nên quên cũng không sai màu; ở bảng tương phản cao
786
+ > thì `--primary-ink` là thứ điều khiển control BẬT, quên là control giữ nguyên gold-500 mặc định.
589
787
  >
590
788
  > ⚠ **Đổi `--teal` / `--green` / `--red` thì phải tính lại bậc `-ink` tương ứng** — chúng là hex đã đóng
591
789
  > băng, không tự đi theo. Đóng băng là chủ ý: một chỗ duy nhất để đo lại, component không phải nhắc lại
@@ -599,13 +797,15 @@ Bản mirror TS đi theo đúng hợp đồng này: `accent` (nền) ⇄ `accent
599
797
 
600
798
  | Nhóm | Token | Ghi chú |
601
799
  |---|---|---|
602
- | Brand | `gold-50 … gold-600` | `gold-300 #E8B824` = `--primary` (light); dark nâng lên `gold-200 #F5D060`. `--primary-ink` = `gold-500 #7A5800` (light) / `= --primary` (dark) — bậc gold cho vai trò **trên nền trang**, xem §6.1 |
800
+ | Brand | `gold-50 … gold-600` | `gold-300 #E8B824` = `--primary` (light); dark nâng lên `gold-200 #F5D060`. `--primary-ink` = `--primary` ở bảng mặc định; ở bảng tương phản cao là `gold-500 #7A5800` (light) / `= --primary` (dark) — xem §6.0 và §6.1 |
603
801
  | Trung tính brand | `ink`, `ink-soft`, `paper`, `warm` | `warm #EDE8D8` = `--secondary` (light) |
604
802
  | Thang xám | `ash-50/100/300/500/700/900` | **Không phải `gray-*`** — xem cạm bẫy bên dưới |
605
- | Màu phụ (NỀN) | `navy`, `teal`, `red`, `green`, `yellow` | Vai trò **tô nền** (fill đặc hoặc tint). Bản dark đã được nâng sáng. ⚠ `red #D64545` **khác** `--destructive` `#C93B3B` — cái sau là nền nút xoá, ghim một giá trị cho cả hai theme để chữ trắng đạt 5,03:1 |
803
+ | Màu phụ (NỀN) | `navy`, `teal`, `red`, `green`, `yellow` | Vai trò **tô nền** (fill đặc hoặc tint). Bảng mặc định dùng một hex cho cả hai theme (đúng comitor-ds); bảng tương phản cao nâng sáng ở dark. ⚠ `--destructive` là token RIÊNG (nền nút xoá): `#D64545` ở bảng mặc định, `#C93B3B` ở bảng tương phản cao để chữ trắng đạt 5,03:1 |
606
804
  | Mực của màu phụ | `teal-ink`, `green-ink`, `red-ink`, `yellow-ink`, `navy-ink` | Cùng màu đó ở vai trò **chữ/icon/viền trên nền trang** — xem §6.1. Ở light là hex đã đóng băng (`#1A6668` / `#216A48` / `#9A3537` / `= gold-500` / `= navy`); ở dark trỏ về chính màu phụ |
607
- | Trạng thái | `success`, `warning`, `info`, `destructive` (+ `-foreground`, + `-ink`) | Ba cái đầu alias sang `green` / `yellow` / `teal` và tự sáng lên ở dark; `destructive` ghim một giá trị cho cả hai theme. `-ink` là bậc để làm **chữ** trạng thái (`text-destructive-ink` cho dòng lỗi) |
608
- | Ranh giới control | `control-edge` `#7E8290` | Token dành riêng cho **ranh giới của control** (rãnh `Switch` khi tắt, viền ô nhập/checkbox chưa chọn) — WCAG 1.4.11 đòi ≥3:1 cho thành phần phi văn bản. Ghim một giá trị cho cả hai theme: 3,83:1 với nền trắng, 4,94:1 với nền dark. **`--input` trỏ vào đúng token này**; `--border` thì KHÔNG — nó là ranh giới trang trí (1,16:1, cố ý) |
805
+ | Trạng thái | `success`, `warning`, `info`, `destructive` (+ `-foreground`, `-ink`, `destructive-hover`) | Ba cái đầu alias sang `green` / `yellow` / `teal`; `destructive` ghim một giá trị cho cả hai theme. `-ink` là bậc để làm **chữ** trạng thái. `--destructive-hover` phải là token riêng vì hai bảng đi **ngược chiều**: comitor-ds viết `hover:bg-red/90` (nhạt dần), bảng tương phản cao phải đậm thêm |
806
+ | Ranh giới control | `control-edge`, `control-edge-strong`, `input`, `input-fill` | **Ranh giới của control** (viền ô nhập, ô checkbox chưa chọn). `--control-edge` trỏ vào `--input`. Bảng mặc định: `#EDEEF1` / `#1E2128` — bằng bậc trang trí, đúng comitor-ds. Bảng tương phản cao: `#7E8290` cho cả hai theme (3,83:1 / 4,94:1). `--control-edge-strong` là bậc hover. `--input-fill` là lớp tô mờ ở dark (`dark:bg-input-fill`) — ở bảng mặc định nó là thứ duy nhất còn khoanh vùng ô Radio/OTP, ở bảng tương phản cao nó trong suốt |
807
+ | Control BẬT / ĐÃ CHỌN | `control-on`, `control-knob`, `control-check`, `switch-track`, `slider-track` | Rãnh `Switch` BẬT / ô `Checkbox` đã chọn dùng `bg-control-on` (= `--primary-ink`); núm Switch là `bg-control-knob`; dấu check và chữ trên ngày đang chọn của `Calendar` là `text-control-check` (MỰC ở bảng mặc định vì nền là gold sáng, màu nền trang ở bảng tương phản cao vì nền là gold-500 tối). `switch-track`/`slider-track` là rãnh lúc TẮT — comitor-ds cho hai thứ này hai bậc khác nhau nên không gộp được |
808
+ | Link | `link-ink`, `link-ink-hover` | Button variant `link`. Tách khỏi `--primary-ink` vì comitor-ds dùng `gold-400` cho chữ link nhưng `gold-300` cho viền focus |
609
809
  | Biểu đồ | `chart-1 … chart-5` | Đổi bảng màu biểu đồ độc lập với brand. `chart-3` trỏ vào `--navy-ink` (nét biểu đồ phải NỔI trên nền trang) |
610
810
  | Avatar | `getAvatarToneColors(name, mode)`, `getAvatarToneIndex(name)`, type `AvatarToneColors` | Trả `{ background, foreground }` dạng **hex đục** cho canvas/email/PDF — đã trộn sẵn tint với nền `--card` vì ngoài CSS không có gì để hợp thành. Cùng bảng tone, cùng hàm băm với `getAvatarToneClasses(name)` ở `@comitor/ui`, nên một người luôn ra một màu ở mọi nơi. Khoá băm là **tên hiển thị**, không phải email/slug |
611
811
  | Theo app | `app-accent`, `app-accent-foreground`, `app-accent-ink` | Mỗi app Comitor tô một accent riêng — khai ở CSS của app (có nhánh `.dark`), không cần build lại package. Ghi đè `--app-accent` thì **phải** set kèm `--app-accent-ink` (§6.1) |
@@ -673,7 +873,8 @@ pnpm lint:fix
673
873
  pnpm lint:a11y # hợp đồng màu: vai trò token + tương phản WCAG ở CẢ hai theme
674
874
  pnpm test:cong-a11y # tự kiểm chính cổng trên: 56 khẳng định, cổng còn bắt đủ không
675
875
  pnpm test:nav-active # active matching của sidebar (hàm thuần) — 34 ca
676
- pnpm verify:directives # mọi file nguồn có "use client" ⇄ file dist tương ứng
876
+ pnpm verify:directives # ranh giới server/client: directive còn sau build + không hằng thuần kẹt trong file client
877
+ pnpm smoke:entries # nạp cả 5 entry bằng Node ESM thuần, qua đúng `exports` map (chạy SAU build)
677
878
  pnpm check:exports # publint --strict + attw
678
879
  ```
679
880
 
@@ -701,7 +902,12 @@ in ra đúng con số này — đừng chép số từ đây mà không chạy l
701
902
 
702
903
  `scripts/check-contrast.mjs` quét **toàn bộ** `src/**/*.{ts,tsx}` theo QUY TẮC (không theo danh sách
703
904
  phát hiện) và đối chiếu với `styles.css`: token TÔ NỀN đem làm màu chữ/icon/viền là sai *vai trò*, và
704
- mọi cặp chữ/nền phải đạt 4,5:1 (chữ) hoặc 3:1 (thành phần phi văn bản) ở **cả** light lẫn dark. Mọi loại
905
+ mọi cặp chữ/nền phải đạt 4,5:1 (chữ) hoặc 3:1 (thành phần phi văn bản) ở **cả** light lẫn dark.
906
+
907
+ ⚠ Cổng đo **bảng tương phản cao** (`:root` + `[data-contrast="high"]`), không đo bảng mặc định — bảng
908
+ mặc định là bản duyệt và đã được chấp nhận với đúng những con số dưới ngưỡng ở §6.0; bắt nó là bắt một
909
+ 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. Hệ quả có lợi: component nào viết
910
+ thẳng thang màu thay vì token vai trò sẽ đỏ ngay, vì giá trị viết thẳng không đổi theo bảng. Mọi loại
705
911
  phát hiện đều `exit 1` — kể cả lớp dùng bảng màu mặc định của Tailwind và miễn trừ đã thừa. Miễn trừ ghi
706
912
  ở `scripts/contrast-exemptions.json`, mỗi mục bắt buộc kèm lý do bằng chữ. Hợp đồng đầy đủ + giới hạn đã
707
913
  biết: [docs/hop-dong-mau.md](docs/hop-dong-mau.md).
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Bảng màu biểu đồ — **module dữ liệu thuần, CỐ Ý không có `"use client"`**.
3
+ *
4
+ * ─ VÌ SAO TÁCH RA KHỎI `comitor-chart.tsx` ─
5
+ * Hai hằng này từng nằm chung file với `<ComitorBarChart>`, mà file đó bắt buộc phải mở đầu
6
+ * bằng `"use client"` (recharts đo kích thước bằng DOM). Hệ quả trong React Server Components:
7
+ * mọi export của một module client — kể cả hằng dữ liệu — đến tay Server Component dưới dạng
8
+ * **client-reference proxy**, không phải giá trị thật. Đọc `CHART_COLORS.gold` ở trang server
9
+ * trả về `undefined`, KHÔNG throw, KHÔNG warning, trong khi TypeScript vẫn khai kiểu literal
10
+ * `"var(--chart-1)"`. Bằng chứng đo được (Next 16, `next build`, trang server prerender):
11
+ *
12
+ * CHART_COLORS.gold → undefined (khi export từ file có "use client")
13
+ * gold[300] @ ui/tokens → "#E8B824" (module thuần, cùng lúc, cùng trang)
14
+ *
15
+ * Kịch bản gãy điển hình: dựng mảng `series` ngay trong Server Component rồi truyền xuống
16
+ * biểu đồ — `series: [{ key: "loi", color: CHART_COLORS.red }]` ra `color: undefined`, biểu đồ
17
+ * lặng lẽ rơi về màu xoay vòng mặc định. Chính JSDoc của gói đang khuyên viết như vậy.
18
+ *
19
+ * ─ RE-EXPORT PHẢI ĐI THẲNG TỪ BARREL ─
20
+ * `chart/index.ts` phải `export * from "./chart-colors.js"` TRỰC TIẾP. Re-export vòng qua
21
+ * `comitor-chart.tsx` là vô nghĩa: bundler RSC cắt ranh giới ở FILE, nên bất cứ thứ gì đi qua
22
+ * một file `"use client"` — kể cả re-export — đều thành proxy trở lại. Cũng vì thế
23
+ * `comitor-chart.tsx` chỉ được `import` hai hằng này, tuyệt đối không `export` lại chúng
24
+ * (vừa vô hiệu, vừa đụng tên với `export *` của barrel).
25
+ */
26
+ /**
27
+ * Bảng màu series mặc định — trỏ vào `--chart-1…5` trong `styles.css`.
28
+ *
29
+ * Vì sao không lấy thẳng `var(--gold-300)`, `var(--teal)`… như comitor-ds: bảng màu biểu đồ
30
+ * cần đổi ĐỘC LẬP với brand (app đổi `--chart-*` ở `<body>` là xong), và `--chart-3` có bản
31
+ * dark riêng (navy nguyên bản chìm vào nền tối).
32
+ *
33
+ * Dùng `var(--chart-N)` chứ KHÔNG dùng `var(--color-chart-N)`: bản `--chart-*` được khai
34
+ * tường minh trong `:root` của `styles.css` nên chắc chắn tồn tại, còn `--color-chart-*` là
35
+ * biến do `@theme inline` của Tailwind sinh ra.
36
+ */
37
+ declare const CHART_SERIES_COLORS: readonly ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"];
38
+ /**
39
+ * Màu gọi theo tên — dùng khi muốn chỉ định màu cho một series cụ thể
40
+ * (`series: [{ key: "loi", color: CHART_COLORS.red }]`).
41
+ *
42
+ * ⚠ Khoá `gray` của comitor-ds đổi thành `ash`: thang xám của package là `--ash-*`,
43
+ * `gray-*` để nguyên cho Tailwind (xem chú thích thang màu trong `styles.css`).
44
+ */
45
+ declare const CHART_COLORS: {
46
+ readonly gold: "var(--chart-1)";
47
+ readonly goldLight: "var(--gold-100)";
48
+ readonly teal: "var(--chart-2)";
49
+ readonly navy: "var(--chart-3)";
50
+ readonly green: "var(--chart-4)";
51
+ readonly red: "var(--chart-5)";
52
+ readonly ash: "var(--ash-300)";
53
+ };
54
+ type ChartColorName = keyof typeof CHART_COLORS;
55
+
56
+ export { CHART_COLORS, CHART_SERIES_COLORS, type ChartColorName };
@@ -0,0 +1,21 @@
1
+ const CHART_SERIES_COLORS = [
2
+ "var(--chart-1)",
3
+ "var(--chart-2)",
4
+ "var(--chart-3)",
5
+ "var(--chart-4)",
6
+ "var(--chart-5)"
7
+ ];
8
+ const CHART_COLORS = {
9
+ gold: "var(--chart-1)",
10
+ goldLight: "var(--gold-100)",
11
+ teal: "var(--chart-2)",
12
+ navy: "var(--chart-3)",
13
+ green: "var(--chart-4)",
14
+ red: "var(--chart-5)",
15
+ ash: "var(--ash-300)"
16
+ };
17
+ export {
18
+ CHART_COLORS,
19
+ CHART_SERIES_COLORS
20
+ };
21
+ //# sourceMappingURL=chart-colors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/chart/chart-colors.ts"],"sourcesContent":["/**\n * Bảng màu biểu đồ — **module dữ liệu thuần, CỐ Ý không có `\"use client\"`**.\n *\n * ─ VÌ SAO TÁCH RA KHỎI `comitor-chart.tsx` ─\n * Hai hằng này từng nằm chung file với `<ComitorBarChart>`, mà file đó bắt buộc phải mở đầu\n * bằng `\"use client\"` (recharts đo kích thước bằng DOM). Hệ quả trong React Server Components:\n * mọi export của một module client — kể cả hằng dữ liệu — đến tay Server Component dưới dạng\n * **client-reference proxy**, không phải giá trị thật. Đọc `CHART_COLORS.gold` ở trang server\n * trả về `undefined`, KHÔNG throw, KHÔNG warning, trong khi TypeScript vẫn khai kiểu literal\n * `\"var(--chart-1)\"`. Bằng chứng đo được (Next 16, `next build`, trang server prerender):\n *\n * CHART_COLORS.gold → undefined (khi export từ file có \"use client\")\n * gold[300] @ ui/tokens → \"#E8B824\" (module thuần, cùng lúc, cùng trang)\n *\n * Kịch bản gãy điển hình: dựng mảng `series` ngay trong Server Component rồi truyền xuống\n * biểu đồ — `series: [{ key: \"loi\", color: CHART_COLORS.red }]` ra `color: undefined`, biểu đồ\n * lặng lẽ rơi về màu xoay vòng mặc định. Chính JSDoc của gói đang khuyên viết như vậy.\n *\n * ─ RE-EXPORT PHẢI ĐI THẲNG TỪ BARREL ─\n * `chart/index.ts` phải `export * from \"./chart-colors.js\"` TRỰC TIẾP. Re-export vòng qua\n * `comitor-chart.tsx` là vô nghĩa: bundler RSC cắt ranh giới ở FILE, nên bất cứ thứ gì đi qua\n * một file `\"use client\"` — kể cả re-export — đều thành proxy trở lại. Cũng vì thế\n * `comitor-chart.tsx` chỉ được `import` hai hằng này, tuyệt đối không `export` lại chúng\n * (vừa vô hiệu, vừa đụng tên với `export *` của barrel).\n */\n\n/**\n * Bảng màu series mặc định — trỏ vào `--chart-1…5` trong `styles.css`.\n *\n * Vì sao không lấy thẳng `var(--gold-300)`, `var(--teal)`… như comitor-ds: bảng màu biểu đồ\n * cần đổi ĐỘC LẬP với brand (app đổi `--chart-*` ở `<body>` là xong), và `--chart-3` có bản\n * dark riêng (navy nguyên bản chìm vào nền tối).\n *\n * Dùng `var(--chart-N)` chứ KHÔNG dùng `var(--color-chart-N)`: bản `--chart-*` được khai\n * tường minh trong `:root` của `styles.css` nên chắc chắn tồn tại, còn `--color-chart-*` là\n * biến do `@theme inline` của Tailwind sinh ra.\n */\nexport const CHART_SERIES_COLORS = [\n \"var(--chart-1)\",\n \"var(--chart-2)\",\n \"var(--chart-3)\",\n \"var(--chart-4)\",\n \"var(--chart-5)\"\n] as const;\n\n/**\n * Màu gọi theo tên — dùng khi muốn chỉ định màu cho một series cụ thể\n * (`series: [{ key: \"loi\", color: CHART_COLORS.red }]`).\n *\n * ⚠ Khoá `gray` của comitor-ds đổi thành `ash`: thang xám của package là `--ash-*`,\n * `gray-*` để nguyên cho Tailwind (xem chú thích thang màu trong `styles.css`).\n */\nexport const CHART_COLORS = {\n gold: \"var(--chart-1)\",\n goldLight: \"var(--gold-100)\",\n teal: \"var(--chart-2)\",\n navy: \"var(--chart-3)\",\n green: \"var(--chart-4)\",\n red: \"var(--chart-5)\",\n ash: \"var(--ash-300)\"\n} as const;\n\nexport type ChartColorName = keyof typeof CHART_COLORS;\n"],"mappings":"AAqCO,MAAM,sBAAsB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AASO,MAAM,eAAe;AAAA,EAC1B,MAAM;AAAA,EACN,WAAW;AAAA,EACX,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,KAAK;AAAA,EACL,KAAK;AACP;","names":[]}
@@ -6,6 +6,10 @@ import * as RechartsPrimitive from 'recharts';
6
6
  * `shadcn add` vẫn dùng lại được. Đây là tầng "thô": bạn tự lắp recharts bên trong.
7
7
  * Muốn dựng nhanh 3 loại biểu đồ quen thuộc thì xem `comitor-chart.tsx`.
8
8
  *
9
+ * ⚠⚠ TỰ LẮP RECHARTS 2.15.x THÌ PHẢI TỰ TẮT HIỆU ỨNG VẼ DẦN ⚠⚠
10
+ * Xem chú thích của {@link ChartContainer} ngay dưới. Tầng này KHÔNG đặt hộ được
11
+ * `isAnimationActive={false}` như `Comitor*Chart` làm, vì mark là children do app viết.
12
+ *
9
13
  * Cơ chế màu (điểm hay nhất, và cũng dễ hiểu sai nhất):
10
14
  * `<ChartContainer>` render kèm `<ChartStyle>` — thẻ này sinh ra biến CSS `--color-<key>`
11
15
  * cho từng key trong `config`, giới hạn phạm vi bằng `[data-chart=<id>]`. Series recharts
@@ -46,6 +50,42 @@ type ChartContainerProps = React.ComponentProps<"div"> & {
46
50
  /**
47
51
  * Khung biểu đồ: cấp context `config`, bơm biến màu, và bọc sẵn `ResponsiveContainer`.
48
52
  * Tỉ lệ mặc định `aspect-video`; muốn cao cố định thì truyền `className="h-64 aspect-auto"`.
53
+ *
54
+ * ─ ⚠ RECHARTS 2.15.x: PHẢI TỰ GHI `isAnimationActive={false}` LÊN TỪNG MARK ─
55
+ * Với `recharts` 2.15.x + React 19.2, hiệu ứng vẽ dần (mặc định BẬT của recharts) không bao giờ
56
+ * chạy tới khung hình cuối: mark đứng nguyên ở khung đầu, tức là **vô hình**. Biểu đồ ra đủ trục,
57
+ * đủ lưới, đủ chú giải, KHÔNG có dữ liệu — không lỗi, không warning, chờ bao lâu cũng không tự hết.
58
+ *
59
+ * `<ComitorBarChart>` / `<ComitorLineChart>` / `<ComitorAreaChart>` đã đặt sẵn `false` từ `0.2.2`.
60
+ * **`<ChartContainer>` thì KHÔNG đặt hộ được** — `<Bar>` / `<Line>` / `<Area>` là children do app
61
+ * viết, container không sờ vào. Đi lối shadcn quen thuộc mà quên prop này là gặp đúng lỗi im lặng
62
+ * của `0.2.1`:
63
+ *
64
+ * ```tsx
65
+ * // ✗ recharts 2.15.x: <g class="recharts-bar-rectangle"> rỗng, không cột nào
66
+ * <ChartContainer config={config}>
67
+ * <BarChart data={data}>
68
+ * <XAxis dataKey="thang" />
69
+ * <Bar dataKey="doanhThu" fill="var(--color-doanhThu)" />
70
+ * </BarChart>
71
+ * </ChartContainer>
72
+ *
73
+ * // ✓ tắt tay trên TỪNG mark
74
+ * <Bar dataKey="doanhThu" fill="var(--color-doanhThu)" isAnimationActive={false} />
75
+ * ```
76
+ *
77
+ * Đo trên DOM thật (Next 16.3.2 · React 19.2.8 · recharts 2.15.4, `next build` + `next start`,
78
+ * chờ 5000ms sau hydrate) — cùng một trang, chỉ khác một prop:
79
+ *
80
+ * | mark | thiếu prop | có `isAnimationActive={false}` |
81
+ * |---|---|---|
82
+ * | `<Bar>` | 3 `<g.recharts-bar-rectangle>` nhưng **0** `path.recharts-rectangle` | **3/3** path, cao 273/410/342px |
83
+ * | `<Line>` | `stroke-dasharray="0px 818.84px"` | `stroke-dasharray` = `null` |
84
+ * | `<Area>` | `<clipPath><rect width="0">` | không còn rect width 0 |
85
+ *
86
+ * `recharts` 3.x không dính lỗi này — bỏ prop đi là được. Dải peer của gói là `>=2.15.0` nên
87
+ * 2.15.x vẫn là cấu hình hợp lệ và phổ biến; cứ ghi `isAnimationActive={false}` cho chắc, trên
88
+ * 3.x nó chỉ có nghĩa "không hoạt hình".
49
89
  */
50
90
  declare function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps): React.JSX.Element;
51
91
  type ChartStyleProps = {