@comitor/ui 0.2.2 → 0.3.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.
Files changed (50) hide show
  1. package/README.md +98 -20
  2. package/dist/components/ui/badge.js +3 -3
  3. package/dist/components/ui/badge.js.map +1 -1
  4. package/dist/components/ui/button.js +4 -4
  5. package/dist/components/ui/button.js.map +1 -1
  6. package/dist/components/ui/calendar.js +1 -1
  7. package/dist/components/ui/calendar.js.map +1 -1
  8. package/dist/components/ui/checkbox.js +2 -2
  9. package/dist/components/ui/checkbox.js.map +1 -1
  10. package/dist/components/ui/command.js +1 -1
  11. package/dist/components/ui/command.js.map +1 -1
  12. package/dist/components/ui/input-group.js +1 -1
  13. package/dist/components/ui/input-group.js.map +1 -1
  14. package/dist/components/ui/input-otp.js +1 -1
  15. package/dist/components/ui/input-otp.js.map +1 -1
  16. package/dist/components/ui/navigation-menu.js +1 -1
  17. package/dist/components/ui/navigation-menu.js.map +1 -1
  18. package/dist/components/ui/radio-group.js +1 -1
  19. package/dist/components/ui/radio-group.js.map +1 -1
  20. package/dist/components/ui/resizable.js +1 -1
  21. package/dist/components/ui/resizable.js.map +1 -1
  22. package/dist/components/ui/slider.js +1 -1
  23. package/dist/components/ui/slider.js.map +1 -1
  24. package/dist/components/ui/switch.js +1 -1
  25. package/dist/components/ui/switch.js.map +1 -1
  26. package/dist/components/ui/textarea.js +1 -1
  27. package/dist/components/ui/textarea.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/shell/app-header.d.ts +10 -2
  30. package/dist/shell/app-header.js +72 -67
  31. package/dist/shell/app-header.js.map +1 -1
  32. package/dist/shell/contrast-constants.d.ts +30 -0
  33. package/dist/shell/contrast-constants.js +16 -0
  34. package/dist/shell/contrast-constants.js.map +1 -0
  35. package/dist/shell/contrast-provider.d.ts +60 -0
  36. package/dist/shell/contrast-provider.js +61 -0
  37. package/dist/shell/contrast-provider.js.map +1 -0
  38. package/dist/shell/contrast-toggle.d.ts +24 -0
  39. package/dist/shell/contrast-toggle.js +29 -0
  40. package/dist/shell/contrast-toggle.js.map +1 -0
  41. package/dist/shell/index.d.ts +3 -0
  42. package/dist/shell/index.js +3 -0
  43. package/dist/shell/index.js.map +1 -1
  44. package/dist/shell/sidebar-menu-item.js +0 -3
  45. package/dist/shell/sidebar-menu-item.js.map +1 -1
  46. package/dist/tokens.d.ts +170 -70
  47. package/dist/tokens.js +124 -47
  48. package/dist/tokens.js.map +1 -1
  49. package/package.json +1 -1
  50. package/styles.css +255 -120
package/README.md CHANGED
@@ -584,7 +584,8 @@ Kèm 2 module thuần (không phải component, dùng được cả hai phía):
584
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` |
585
585
  | `AppHeader`, `Sidebar`, `SidebarGroup`, `SidebarMenuItem`, `MobileMenu` | Thành phần khung điều hướng |
586
586
  | `UserMenu`, `UserAvatar`, `NotificationsButton` | Vùng phải của header |
587
- | `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 |
588
589
  | `ShellNavLink` | Link nội bộ của shell; thay `next/link` bằng `LinkComponent` khi không chạy Next |
589
590
  | `ComitorLogo`, `ComitorLockup` | Vector brand — **không** có `"use client"`, RSC render thẳng (hex trong SVG là brand book, không tokenize) |
590
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` |
@@ -657,6 +658,63 @@ trong component.
657
658
 
658
659
  Toàn bộ token nằm ở [`styles.css`](./styles.css); bản mirror TS ở [`src/tokens.ts`](./src/tokens.ts).
659
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
+
660
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
661
719
 
662
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:
@@ -677,9 +735,15 @@ green #2EA56A trên #E0F2E9 = 2,69:1 ✗ → --green-ink #216A48 = 5,60:1
677
735
  red #D64545 trên #F9E3E3 = 3,57:1 ✗ → --red-ink #9A3537 = 5,83:1 ✓
678
736
  ```
679
737
 
680
- Ở **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ữ
681
- đượ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ả
682
- 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`.
683
747
 
684
748
  Ba vai trò áp cho mọi họ token:
685
749
 
@@ -690,29 +754,36 @@ Ba vai trò áp cho mọi họ token:
690
754
  | Brand | `--primary` | `--primary-foreground` | `--primary-ink` |
691
755
  | Accent theo app | `--app-accent` | `--app-accent-foreground` | `--app-accent-ink` (mặc định `= --primary-ink`) |
692
756
 
693
- 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
694
- 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`
695
- `#7A5800` ở light (**6,51:1** ✓) và trỏ về chính `--primary` ở dark (**12,67:1** ✓). Đây là màu của
696
- rãnh `Switch` khi BẬT và ô `Checkbox` đã chọn. `--ring` **cố ý vẫn là** `--primary`: vòng focus thuộc
697
- 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.
698
764
 
699
- 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):
700
767
 
701
768
  | Viết sai | Hỏng ở đâu | Viết đúng |
702
769
  |---|---|---|
703
770
  | `text-red` cho dòng báo lỗi | 4,38:1 ở light ✗; ở dark thì đây là thang brand thô | `text-destructive-ink` |
704
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) |
705
- | `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) |
706
- | `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` |
707
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` |
708
- | `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) |
709
779
 
710
780
  > ⚠ **App ghi đè `--app-accent` thì PHẢI set kèm `--app-accent-ink`** (và `--app-accent-foreground` nếu
711
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
712
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.
713
783
  >
714
- > ⚠ **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ì
715
- > 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.
716
787
  >
717
788
  > ⚠ **Đổi `--teal` / `--green` / `--red` thì phải tính lại bậc `-ink` tương ứng** — chúng là hex đã đóng
718
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
@@ -726,13 +797,15 @@ Bản mirror TS đi theo đúng hợp đồng này: `accent` (nền) ⇄ `accent
726
797
 
727
798
  | Nhóm | Token | Ghi chú |
728
799
  |---|---|---|
729
- | 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 |
730
801
  | Trung tính brand | `ink`, `ink-soft`, `paper`, `warm` | `warm #EDE8D8` = `--secondary` (light) |
731
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 |
732
- | 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 |
733
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ụ |
734
- | 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) |
735
- | 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 |
736
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) |
737
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 |
738
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) |
@@ -829,7 +902,12 @@ in ra đúng con số này — đừng chép số từ đây mà không chạy l
829
902
 
830
903
  `scripts/check-contrast.mjs` quét **toàn bộ** `src/**/*.{ts,tsx}` theo QUY TẮC (không theo danh sách
831
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à
832
- 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
833
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
834
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 đã
835
913
  biết: [docs/hop-dong-mau.md](docs/hop-dong-mau.md).
@@ -11,9 +11,9 @@ const badgeVariants = cva(
11
11
  primary: "bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200",
12
12
  secondary: "bg-secondary text-secondary-foreground",
13
13
  outline: "bg-transparent border border-control-edge text-foreground",
14
- success: "bg-success/15 text-success-ink",
15
- warning: "bg-warning/20 text-warning-ink",
16
- destructive: "bg-destructive/15 text-destructive-ink"
14
+ success: "bg-green/10 dark:bg-green/20 text-green-ink",
15
+ warning: "bg-yellow/10 dark:bg-yellow/20 text-yellow-ink",
16
+ destructive: "bg-red/10 dark:bg-red/15 text-destructive-ink"
17
17
  },
18
18
  size: {
19
19
  sm: "px-1.5 py-0.5 text-[10px]",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/badge.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds (biến thể tonal + 2 cỡ sm/md).\n * API giữ tương thích shadcn (cva + asChild + data-slot).\n *\n * ─ BIẾN THỂ TONAL: NỀN LÀ `--x`, CHỮ LÀ `--x-ink` ─\n *\n * Badge in chữ 10–12px nên phải đạt AA 4,5:1 trên chính cái tint mà nó đứng lên. Hợp đồng\n * ba vai trò (docs/hop-dong-mau.md §1) đã có sẵn bậc chữ cho việc này: nền là `--success` /\n * `--warning` / `--destructive`, chữ là bậc `-ink` tương ứng. Đo trên tint hợp lên `--card`:\n *\n * success-ink #216A48 trên bg-success/15 #E0F2E9 = 5,60:1 ✓ (light)\n * #4ADE80 trên #1E362C = 7,44:1 ✓ (dark)\n * warning-ink #7A5800 trên bg-warning/20 #FCF4DB = 5,93:1 ✓ (light)\n * #FACC15 trên #443C1B = 7,19:1 ✓ (dark)\n * destructive-ink #9A3537 trên bg-destructive/15 #F7E2E2 = 5,76:1 ✓ (light)\n * #F87171 trên #311D22 = 5,71:1 ✓ (dark)\n *\n * Bản trước dẫn xuất bậc tối tại chỗ bằng `color-mix(--x, --ink N%)` cho light rồi lật về\n * `dark:text-success` / `dark:text-warning` / `dark:text-red` cho dark. Số thì đạt, nhưng\n * NHÁNH DARK vi phạm hợp đồng: `--success`/`--warning` là token TÔ NỀN, không phải màu chữ —\n * app nào đổi `--success` cho hợp brand mình là badge hỏng chữ mà không có gì báo. Bậc `-ink`\n * mới là biến mang ràng buộc tương phản, và nó tự lật theo theme (styles.css khai `-ink` riêng\n * cho `.dark`) nên không cần biến thể `dark:` nào ở đây nữa.\n *\n * ⚠ Trần tint: `/20` là mức đậm nhất an toàn cho MỌI tone ở cả hai theme. `warning` chịu được\n * `/20` thoải mái (trần của vàng là `/60` light · `/36` dark) nhưng đỏ ở dark chỉ tới `/22` —\n * muốn đậm hơn `/15` phải tra bảng trần tint ở docs/hop-dong-mau.md §2.\n *\n * ─ VIỀN CỦA BIẾN THỂ `outline` ─\n * `border-control-edge` chứ không `border-border`: nền badge trong suốt nên VIỀN là thứ duy\n * nhất khoanh vùng nó → chịu ngưỡng 3:1 (WCAG 1.4.11). `--border` là bậc TRANG TRÍ, chỉ\n * 1,16:1 (light) / 1,17:1 (dark) — badge gần như tàng hình. `--control-edge`: 3,83:1 / 4,94:1 ✓.\n *\n * ─ BIẾN THỂ `default`/`primary` ─\n * Vẫn là thang gold thô (`gold-100`/`gold-600` ở light, `gold-500/20`+`gold-200` ở dark) vì\n * cặp này đã đo và dư biên: 7,77:1 (light) · 10,23:1 (dark).\n */\nconst badgeVariants = cva(\n \"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n secondary: \"bg-secondary text-secondary-foreground\",\n outline: \"bg-transparent border border-control-edge text-foreground\",\n success: \"bg-success/15 text-success-ink\",\n warning: \"bg-warning/20 text-warning-ink\",\n destructive: \"bg-destructive/15 text-destructive-ink\"\n },\n size: {\n sm: \"px-1.5 py-0.5 text-[10px]\",\n md: \"px-2 py-0.5 text-xs\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\"\n }\n }\n);\n\nexport interface BadgeProps extends React.ComponentProps<\"span\">, VariantProps<typeof badgeVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. */\n asChild?: boolean;\n}\n\nfunction Badge({ className, variant, size, asChild = false, ...props }: BadgeProps) {\n const Comp = asChild ? Slot : \"span\";\n\n return <Comp data-slot=\"badge\" className={cn(badgeVariants({ variant, size }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n"],"mappings":"AA2ES;AA3ET,SAAS,YAAY;AACrB,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAuCnB,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WAAW;AAAA,QACX,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAOA,SAAS,MAAM,EAAE,WAAW,SAAS,MAAM,UAAU,OAAO,GAAG,MAAM,GAAe;AAClF,QAAM,OAAO,UAAU,OAAO;AAE9B,SAAO,oBAAC,QAAK,aAAU,SAAQ,WAAW,GAAG,cAAc,EAAE,SAAS,KAAK,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AACxG;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/badge.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds (biến thể tonal + 2 cỡ sm/md).\n * API giữ tương thích shadcn (cva + asChild + data-slot).\n *\n * ─ BIẾN THỂ TONAL: NỀN LÀ `--x`, CHỮ LÀ `--x-ink` ─\n *\n * Badge in chữ 10–12px nên phải đạt AA 4,5:1 trên chính cái tint mà nó đứng lên. Hợp đồng\n * ba vai trò (docs/hop-dong-mau.md §1) đã có sẵn bậc chữ cho việc này: nền là `--success` /\n * `--warning` / `--destructive`, chữ là bậc `-ink` tương ứng. Đo trên tint hợp lên `--card`:\n *\n * success-ink #216A48 trên bg-success/15 #E0F2E9 = 5,60:1 ✓ (light)\n * #4ADE80 trên #1E362C = 7,44:1 ✓ (dark)\n * warning-ink #7A5800 trên bg-warning/20 #FCF4DB = 5,93:1 ✓ (light)\n * #FACC15 trên #443C1B = 7,19:1 ✓ (dark)\n * destructive-ink #9A3537 trên bg-destructive/15 #F7E2E2 = 5,76:1 ✓ (light)\n * #F87171 trên #311D22 = 5,71:1 ✓ (dark)\n *\n * Bản trước dẫn xuất bậc tối tại chỗ bằng `color-mix(--x, --ink N%)` cho light rồi lật về\n * `dark:text-success` / `dark:text-warning` / `dark:text-red` cho dark. Số thì đạt, nhưng\n * NHÁNH DARK vi phạm hợp đồng: `--success`/`--warning` là token TÔ NỀN, không phải màu chữ —\n * app nào đổi `--success` cho hợp brand mình là badge hỏng chữ mà không có gì báo. Bậc `-ink`\n * mới là biến mang ràng buộc tương phản, và nó tự lật theo theme (styles.css khai `-ink` riêng\n * cho `.dark`) nên không cần biến thể `dark:` nào ở đây nữa.\n *\n * ⚠ Trần tint: `/20` là mức đậm nhất an toàn cho MỌI tone ở cả hai theme. `warning` chịu được\n * `/20` thoải mái (trần của vàng là `/60` light · `/36` dark) nhưng đỏ ở dark chỉ tới `/22` —\n * muốn đậm hơn `/15` phải tra bảng trần tint ở docs/hop-dong-mau.md §2.\n *\n * ─ VIỀN CỦA BIẾN THỂ `outline` ─\n * `border-control-edge` chứ không `border-border`: nền badge trong suốt nên VIỀN là thứ duy\n * nhất khoanh vùng nó → chịu ngưỡng 3:1 (WCAG 1.4.11). `--border` là bậc TRANG TRÍ, chỉ\n * 1,16:1 (light) / 1,17:1 (dark) — badge gần như tàng hình. `--control-edge`: 3,83:1 / 4,94:1 ✓.\n *\n * ─ BIẾN THỂ `default`/`primary` ─\n * Vẫn là thang gold thô (`gold-100`/`gold-600` ở light, `gold-500/20`+`gold-200` ở dark) vì\n * cặp này đã đo và dư biên: 7,77:1 (light) · 10,23:1 (dark).\n */\nconst badgeVariants = cva(\n \"inline-flex items-center font-medium rounded-md whitespace-nowrap gap-1 [&>svg]:size-3 [&>svg]:pointer-events-none\",\n {\n variants: {\n variant: {\n default: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n primary: \"bg-gold-100 text-gold-600 dark:bg-gold-500/20 dark:text-gold-200\",\n secondary: \"bg-secondary text-secondary-foreground\",\n outline: \"bg-transparent border border-control-edge text-foreground\",\n success: \"bg-green/10 dark:bg-green/20 text-green-ink\",\n warning: \"bg-yellow/10 dark:bg-yellow/20 text-yellow-ink\",\n destructive: \"bg-red/10 dark:bg-red/15 text-destructive-ink\"\n },\n size: {\n sm: \"px-1.5 py-0.5 text-[10px]\",\n md: \"px-2 py-0.5 text-xs\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\"\n }\n }\n);\n\nexport interface BadgeProps extends React.ComponentProps<\"span\">, VariantProps<typeof badgeVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <span>. */\n asChild?: boolean;\n}\n\nfunction Badge({ className, variant, size, asChild = false, ...props }: BadgeProps) {\n const Comp = asChild ? Slot : \"span\";\n\n return <Comp data-slot=\"badge\" className={cn(badgeVariants({ variant, size }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n"],"mappings":"AA2ES;AA3ET,SAAS,YAAY;AACrB,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAuCnB,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WAAW;AAAA,QACX,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAOA,SAAS,MAAM,EAAE,WAAW,SAAS,MAAM,UAAU,OAAO,GAAG,MAAM,GAAe;AAClF,QAAM,OAAO,UAAU,OAAO;AAE9B,SAAO,oBAAC,QAAK,aAAU,SAAQ,WAAW,GAAG,cAAc,EAAE,SAAS,KAAK,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AACxG;","names":[]}
@@ -9,13 +9,13 @@ const buttonVariants = cva(
9
9
  {
10
10
  variants: {
11
11
  variant: {
12
- default: "bg-primary text-primary-foreground hover:bg-[color:color-mix(in_srgb,var(--primary),var(--ink)_15%)] border border-transparent shadow-control",
13
- primary: "bg-primary text-primary-foreground hover:bg-[color:color-mix(in_srgb,var(--primary),var(--ink)_15%)] border border-transparent shadow-control",
12
+ default: "bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control",
13
+ primary: "bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control",
14
14
  secondary: "bg-secondary text-secondary-foreground hover:bg-warm dark:hover:bg-muted border border-control-edge",
15
15
  outline: "bg-transparent text-foreground border border-control-edge hover:border-primary-ink hover:bg-gold-50 dark:hover:bg-gold-500/10",
16
16
  ghost: "bg-transparent text-foreground hover:bg-muted border border-transparent",
17
- destructive: "bg-destructive text-destructive-foreground hover:bg-[color:color-mix(in_srgb,var(--destructive),var(--ink)_15%)] border border-transparent",
18
- link: "bg-transparent text-gold-500 hover:text-gold-600 dark:text-gold-200 dark:hover:text-gold-100 underline-offset-4 hover:underline border border-transparent p-0 h-auto"
17
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive-hover border border-transparent",
18
+ link: "bg-transparent text-link-ink hover:text-link-ink-hover underline-offset-4 hover:underline border border-transparent p-0 h-auto"
19
19
  },
20
20
  size: {
21
21
  default: "h-9 px-4 text-sm rounded-md gap-2",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Loader2 } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port từ comitor-ds để giao diện khớp bản v0.\n * API giữ tương thích shadcn (cva + asChild + data-slot); `primary` là alias của `default`\n * — đã là API công khai trong docs comitor-ds nên không bỏ.\n *\n * ─ VÌ SAO NỀN NÚT LÀ TOKEN SEMANTIC, KHÔNG PHẢI THANG BRAND THÔ ─\n *\n * 1. `default`/`primary` trước dùng `bg-gold-300 text-ink` — cùng giá trị ở cả hai theme,\n * nên bảng màu dark của chính package (`.dark { --primary: #F5D060 }`) không bao giờ được\n * áp dụng, và app nào set lại `--primary` cho sản phẩm của mình cũng không đổi được nút.\n * Nay dùng `bg-primary text-primary-foreground`: light 10,19:1 (không đổi), dark 12,67:1\n * (trước 10,19:1) — đều vượt AA.\n *\n * 2. `destructive` trước dùng `bg-red text-white`. `--red` là màu phụ được NÂNG SÁNG ở dark\n * (#F87171) để làm CHỮ trên nền tối, dùng làm NỀN nút thì chữ trắng chỉ còn 2,77:1 ✗.\n * `--destructive` cố ý ghim một giá trị ở cả hai theme đúng cho việc này. Bản #D64545 mới\n * đạt 4,38:1 — thiếu 0,12 so với AA 4,5:1 — nên token đã được đẩy đậm thêm một bậc trong\n * `styles.css`: **#C93B3B → 5,03:1** ở cả hai theme. Đó là sửa ở TẦNG TOKEN, đúng chỗ; đừng\n * \"chữa\" lại trong component bằng cách chôn hex hay đổi sang `bg-red`.\n *\n * ─ VÌ SAO HOVER TRỘN `--ink` THAY VÌ `/90` ─\n *\n * `bg-primary/90` là \"nhạt dần về phía nền\" nên ở light mode nút SÁNG lên khi rê chuột (ngược\n * với bản v0 gold-300 → gold-400) và với destructive thì tương phản chữ trắng tụt còn 3,81:1 ✗.\n * Trộn 15% `--ink` thì tối đi ở CẢ hai theme, giữ đúng ngữ nghĩa \"nhấn xuống\" và tương phản chỉ\n * tăng: destructive 6,29:1 ✓, primary 7,58:1 (light) / 9,33:1 (dark) ✓.\n *\n * ─ VIỀN CỦA `outline` VÀ `secondary`: `--control-edge`, KHÔNG PHẢI `--border` ─\n *\n * `--border` là bậc TRANG TRÍ (vạch chia, khung Card lúc nghỉ) và cố ý nhạt — 1,16:1 ở light,\n * 1,17:1 ở dark. Nút `outline` để nền trong suốt nên viền là thứ DUY NHẤT khoanh vùng nó, và\n * nút `secondary` thì viền tách khối khỏi nền trang: cả hai là \"thông tin cần để nhận ra thành\n * phần\" theo WCAG 1.4.11 → ngưỡng 3:1. Số sau khi đổi sang `--control-edge`:\n *\n * outline viền ⇄ nền trang : 3,83:1 (light) · 4,94:1 (dark) ✓\n * secondary viền ⇄ nền nút `--secondary`: 3,13:1 (light) · 4,21:1 (dark) ✓\n *\n * Hover của `outline` cũng phải ĐẬM HƠN lúc nghỉ: `hover:border-gold-300` cũ chỉ **1,86:1** ở\n * light, tức rê chuột vào là nút mất viền. `--primary-ink` giữ đúng sắc gold mà đạt 6,51:1 với\n * nền trang và 5,96:1 với chính nền hover `gold-50` (light); 12,67:1 / 11,91:1 (dark).\n *\n * ─ `link` ─\n * `text-gold-400` (#B88A00) trên nền trắng chỉ 3,15:1 ✗ — chữ link buộc phải đạt 4,5:1.\n * Đổi sang bậc tối/sáng theo theme: gold-500 6,51:1 (light) và gold-200 12,67:1 (dark).\n * Vẫn là thang gold vì --primary ở light (#E8B824) trên nền trắng chỉ 1,86:1, không dùng\n * làm chữ được; xem docs/quyet-dinh-tich-hop.md.\n *\n * ─ `aria-invalid`: VIỀN ĐỎ, KHÔNG CHỈ CHỮ ĐỎ ─\n *\n * Control dựng trên `<Button>` mà mang giá trị form — `Combobox`/`MultiCombobox` render\n * trigger bằng `<Button role=\"combobox\" aria-invalid>` — trước đây chỉ có chữ lỗi đỏ ở dưới,\n * còn chính ô thì giữ nguyên viền `--control-edge`: người dùng thấy thông báo lỗi mà không\n * thấy ô nào đang lỗi. `Input` đã có nhánh này từ đầu (`border-destructive-ink`); đặt ở đây\n * để MỌI control dựng trên Button được nhất quán, thay vì mỗi composite tự chép lại.\n *\n * Token là bậc `-ink`, đúng như `Input`: `--destructive-ink` #9A3537 (light) / #F87171 (dark)\n * đạt 7,15:1 (light) / 6,84:1 (dark) trên `--background`, và ca TỆ NHẤT trong 5 nền trang là\n * 6,68:1 (light, trên `--muted`) / 5,82:1 (dark, trên `--muted`) — đều thừa ngưỡng 3:1 của\n * viền control (§3 hợp đồng màu). KHÔNG dùng `--destructive` (nền nút xoá, 3,76:1 ở dark —\n * sát ngưỡng) và càng không dùng `--red` (token TÔ NỀN).\n *\n * Nhánh này ghi sau các biến thể trong chuỗi lớp? Không cần: `aria-invalid:` sinh selector\n * `[aria-invalid=\"true\"]` nên đặc tả cao hơn `border-transparent`/`border-control-edge` trần,\n * và `tailwind-merge` giữ cả hai vì tập biến thể khác nhau.\n */\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center font-medium select-none whitespace-nowrap transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 aria-invalid:border-destructive-ink aria-invalid:hover:border-destructive-ink aria-invalid:focus-visible:outline-destructive-ink disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground hover:bg-[color:color-mix(in_srgb,var(--primary),var(--ink)_15%)] border border-transparent shadow-control\",\n primary:\n \"bg-primary text-primary-foreground hover:bg-[color:color-mix(in_srgb,var(--primary),var(--ink)_15%)] border border-transparent shadow-control\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-warm dark:hover:bg-muted border border-control-edge\",\n outline:\n \"bg-transparent text-foreground border border-control-edge hover:border-primary-ink hover:bg-gold-50 dark:hover:bg-gold-500/10\",\n ghost: \"bg-transparent text-foreground hover:bg-muted border border-transparent\",\n destructive:\n \"bg-destructive text-destructive-foreground hover:bg-[color:color-mix(in_srgb,var(--destructive),var(--ink)_15%)] border border-transparent\",\n link: \"bg-transparent text-gold-500 hover:text-gold-600 dark:text-gold-200 dark:hover:text-gold-100 underline-offset-4 hover:underline border border-transparent p-0 h-auto\"\n },\n size: {\n default: \"h-9 px-4 text-sm rounded-md gap-2\",\n md: \"h-9 px-4 text-sm rounded-md gap-2\",\n sm: \"h-8 px-3 text-xs rounded-md gap-1.5\",\n lg: \"h-11 px-6 text-base rounded-md gap-2\",\n icon: \"h-9 w-9 rounded-md\",\n \"icon-sm\": \"h-8 w-8 rounded-md\",\n \"icon-lg\": \"h-11 w-11 rounded-md\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\"\n }\n }\n);\n\nexport interface ButtonProps extends React.ComponentProps<\"button\">, VariantProps<typeof buttonVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <button>. */\n asChild?: boolean;\n /** Hiện spinner và khoá nút. */\n loading?: boolean;\n /** Icon đặt trước nội dung; bị spinner thay thế khi `loading`. */\n leftIcon?: React.ReactNode;\n /** Icon đặt sau nội dung; ẩn khi `loading`. */\n rightIcon?: React.ReactNode;\n}\n\nfunction Button({\n className,\n variant,\n size,\n asChild = false,\n loading = false,\n leftIcon,\n rightIcon,\n disabled,\n children,\n ...props\n}: ButtonProps) {\n // Slot chỉ nhận đúng MỘT phần tử con → bỏ qua lớp bọc icon/loading khi asChild.\n if (asChild) {\n return (\n <Slot data-slot=\"button\" className={cn(buttonVariants({ variant, size, className }))} {...props}>\n {children}\n </Slot>\n );\n }\n\n return (\n <button\n data-slot=\"button\"\n disabled={disabled || loading}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n >\n {loading ? (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin\" aria-hidden />\n ) : leftIcon ? (\n <span className=\"shrink-0\">{leftIcon}</span>\n ) : null}\n {children}\n {!loading && rightIcon ? <span className=\"shrink-0\">{rightIcon}</span> : null}\n </button>\n );\n}\n\nexport { Button, buttonVariants };\n"],"mappings":";AAsIM,cAOF,YAPE;AApIN,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,eAAe;AAExB,SAAS,UAAU;AAmEnB,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SACE;AAAA,QACF,SACE;AAAA,QACF,WACE;AAAA,QACF,SACE;AAAA,QACF,OAAO;AAAA,QACP,aACE;AAAA,QACF,MAAM;AAAA,MACR;AAAA,MACA,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAaA,SAAS,OAAO;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgB;AAEd,MAAI,SAAS;AACX,WACE,oBAAC,QAAK,aAAU,UAAS,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC,GAAI,GAAG,OACvF,UACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,UAAU,YAAY;AAAA,MACtB,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC;AAAA,MACzD,GAAG;AAAA,MAEH;AAAA,kBACC,oBAAC,WAAQ,WAAU,kCAAiC,eAAW,MAAC,IAC9D,WACF,oBAAC,UAAK,WAAU,YAAY,oBAAS,IACnC;AAAA,QACH;AAAA,QACA,CAAC,WAAW,YAAY,oBAAC,UAAK,WAAU,YAAY,qBAAU,IAAU;AAAA;AAAA;AAAA,EAC3E;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Loader2 } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port NGUYÊN TRẠNG từ comitor-ds — bản duyệt là nguồn sự thật (xem khối NGUỒN SỰ THẬT ở\n * đầu styles.css). API giữ tương thích shadcn (cva + asChild + data-slot); `primary` là alias của\n * `default` — đã là API công khai trong docs comitor-ds nên không bỏ.\n *\n * ⚠ 0.2.x từng đổi nhóm biến thể này sang token semantic + bậc `-ink` để đạt WCAG. 0.3.0 kéo về\n * đúng comitor-ds. Những khoản dưới ngưỡng còn tồn ở đây, ghi ra để không ai \"sửa\" lại lần nữa:\n *\n * destructive : chữ trắng trên `--red` #D64545 = 4,38:1 (thiếu 0,12 so với AA 4,5:1)\n * link : `--gold-400` #B88A00 trên nền trang = 3,15:1 (AA đòi 4,5:1)\n * outline : viền `--border` ⇄ nền trang = 1,16:1 (1.4.11 đòi 3:1)\n *\n * Vì sao `bg-gold-300` chứ không `bg-primary`: comitor-ds tô cùng một gold ở CẢ hai theme\n * (`dark:data-[state=checked]:bg-gold-300` là lối viết quen của bản duyệt), trong khi `--primary`\n * có bản dark riêng #F5D060. Dùng thang gold thô là cách duy nhất khớp bản duyệt ở dark.\n * ⚠ Đánh đổi: app ghi đè `--primary` cho sản phẩm của mình sẽ KHÔNG đổi được nền nút. Muốn nút\n * theo brand riêng thì ghi đè thang `--gold-*`.\n *\n * ─ `aria-invalid`: VIỀN ĐỎ, KHÔNG CHỈ CHỮ ĐỎ (BỔ SUNG, comitor-ds chưa có) ─\n *\n * Control dựng trên `<Button>` mà mang giá trị form — `Combobox`/`MultiCombobox` render trigger\n * bằng `<Button role=\"combobox\" aria-invalid>` — trước đây chỉ có chữ lỗi đỏ ở dưới, còn chính ô\n * thì giữ nguyên viền: người dùng thấy thông báo lỗi mà không thấy ô nào đang lỗi. `Input` đã có\n * nhánh này từ đầu; đặt ở đây để MỌI control dựng trên Button được nhất quán. Token là\n * `--destructive-ink` (= `--destructive` = #D64545), đúng thứ `Input` của comitor-ds dùng\n * (`border-red`).\n */\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center font-medium select-none whitespace-nowrap transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 aria-invalid:border-destructive-ink aria-invalid:hover:border-destructive-ink aria-invalid:focus-visible:outline-destructive-ink disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control\",\n primary: \"bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-warm dark:hover:bg-muted border border-control-edge\",\n outline:\n \"bg-transparent text-foreground border border-control-edge hover:border-primary-ink hover:bg-gold-50 dark:hover:bg-gold-500/10\",\n ghost: \"bg-transparent text-foreground hover:bg-muted border border-transparent\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive-hover border border-transparent\",\n link: \"bg-transparent text-link-ink hover:text-link-ink-hover underline-offset-4 hover:underline border border-transparent p-0 h-auto\"\n },\n size: {\n default: \"h-9 px-4 text-sm rounded-md gap-2\",\n md: \"h-9 px-4 text-sm rounded-md gap-2\",\n sm: \"h-8 px-3 text-xs rounded-md gap-1.5\",\n lg: \"h-11 px-6 text-base rounded-md gap-2\",\n icon: \"h-9 w-9 rounded-md\",\n \"icon-sm\": \"h-8 w-8 rounded-md\",\n \"icon-lg\": \"h-11 w-11 rounded-md\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\"\n }\n }\n);\n\nexport interface ButtonProps extends React.ComponentProps<\"button\">, VariantProps<typeof buttonVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <button>. */\n asChild?: boolean;\n /** Hiện spinner và khoá nút. */\n loading?: boolean;\n /** Icon đặt trước nội dung; bị spinner thay thế khi `loading`. */\n leftIcon?: React.ReactNode;\n /** Icon đặt sau nội dung; ẩn khi `loading`. */\n rightIcon?: React.ReactNode;\n}\n\nfunction Button({\n className,\n variant,\n size,\n asChild = false,\n loading = false,\n leftIcon,\n rightIcon,\n disabled,\n children,\n ...props\n}: ButtonProps) {\n // Slot chỉ nhận đúng MỘT phần tử con → bỏ qua lớp bọc icon/loading khi asChild.\n if (asChild) {\n return (\n <Slot data-slot=\"button\" className={cn(buttonVariants({ variant, size, className }))} {...props}>\n {children}\n </Slot>\n );\n }\n\n return (\n <button\n data-slot=\"button\"\n disabled={disabled || loading}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n >\n {loading ? (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin\" aria-hidden />\n ) : leftIcon ? (\n <span className=\"shrink-0\">{leftIcon}</span>\n ) : null}\n {children}\n {!loading && rightIcon ? <span className=\"shrink-0\">{rightIcon}</span> : null}\n </button>\n );\n}\n\nexport { Button, buttonVariants };\n"],"mappings":";AA6FM,cAOF,YAPE;AA3FN,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,eAAe;AAExB,SAAS,UAAU;AA6BnB,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WACE;AAAA,QACF,SACE;AAAA,QACF,OAAO;AAAA,QACP,aAAa;AAAA,QACb,MAAM;AAAA,MACR;AAAA,MACA,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAaA,SAAS,OAAO;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgB;AAEd,MAAI,SAAS;AACX,WACE,oBAAC,QAAK,aAAU,UAAS,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC,GAAI,GAAG,OACvF,UACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,UAAU,YAAY;AAAA,MACtB,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC;AAAA,MACzD,GAAG;AAAA,MAEH;AAAA,kBACC,oBAAC,WAAQ,WAAU,kCAAiC,eAAW,MAAC,IAC9D,WACF,oBAAC,UAAK,WAAU,YAAY,oBAAS,IACnC;AAAA,QACH;AAAA,QACA,CAAC,WAAW,YAAY,oBAAC,UAAK,WAAU,YAAY,qBAAU,IAAU;AAAA;AAAA;AAAA,EAC3E;AAEJ;","names":[]}
@@ -133,7 +133,7 @@ function CalendarDayButton({ className, day, modifiers, ...props }) {
133
133
  "data-range-end": modifiers.range_end,
134
134
  "data-range-middle": modifiers.range_middle,
135
135
  className: cn(
136
- "data-[selected-single=true]:bg-primary-ink data-[selected-single=true]:text-background data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary-ink data-[range-start=true]:text-background data-[range-end=true]:bg-primary-ink data-[range-end=true]:text-background group-data-[focused=true]/day:outline-none group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-offset-2 group-data-[focused=true]/day:ring-offset-background dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-2 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-80",
136
+ "data-[selected-single=true]:bg-primary-ink data-[selected-single=true]:text-control-check data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary-ink data-[range-start=true]:text-control-check data-[range-end=true]:bg-primary-ink data-[range-end=true]:text-control-check group-data-[focused=true]/day:outline-none group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-offset-2 group-data-[focused=true]/day:ring-offset-background dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-2 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-80",
137
137
  defaultClassNames.day,
138
138
  className
139
139
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/calendar.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { type DayButton, DayPicker, getDefaultClassNames } from \"react-day-picker\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps, buttonVariants } from \"./button.js\";\n\n/**\n * Calendar (react-day-picker v9) — lịch trần, đã map class sang token Comitor.\n * Bản có ô nhập `dd/MM/yyyy` + preset (\"Hôm nay\", \"7 ngày qua\"…) là composite\n * `DatePicker` / `DateRangePicker` ở Tier 2.\n *\n * Ngôn ngữ: truyền `locale={vi}` (date-fns/locale) để đổi tên thứ/tháng.\n *\n * Khác bản comitor-ds: tên tháng rút gọn trong dropdown lấy theo `locale.code`\n * (mặc định `vi-VN`) thay vì `\"default\"` — `\"default\"` phụ thuộc locale của môi\n * trường chạy nên server và trình duyệt có thể ra chuỗi khác nhau → hydration mismatch.\n *\n * ─ Ô NGÀY ĐANG CHỌN: `bg-primary-ink` + `text-background` ─\n * Chữ của ô ngày KHÔNG đổi màu khi được chọn (nghỉ và chọn đều là bậc mực), nên \"đang chọn\"\n * chỉ còn được kể bằng NỀN ô — thông tin thị giác, ngưỡng 3:1. `bg-primary` gold #E8B824 trên\n * nền trắng chỉ **1,86:1** ✗. Đổi sang bậc ink của brand thì đạt cả hai phía, cả hai theme:\n *\n * nền ô ⇄ nền lịch : 6,51:1 (light) · 12,67:1 (dark) ✓\n * chữ `text-background` trên nền ô : 6,51:1 (light) · 12,67:1 (dark) ✓\n *\n * Chữ phải là `text-background` (trắng ở light) chứ KHÔNG `text-primary-foreground` (mực ở cả\n * hai theme): mực trên `--primary-ink` #7A5800 chỉ 1,86:1 ✗ — cùng lý do với dấu tick\n * `Checkbox` và núm `Switch`.\n *\n * ⚠ CÒN TỒN, cố ý chưa động: `today` và `range_middle` dùng `bg-accent` — 1,09:1 (light) /\n * 1,17:1 (dark) so với nền lịch. Đưa dải giữa lên 3:1 thì cả tuần biến thành một khối đặc,\n * nên đây là quyết định THIẾT KẾ (thêm viền/gạch chân cho \"hôm nay\"?) chứ không sửa được bằng\n * đổi token — để dành cho vòng riêng.\n */\n\nexport type CalendarProps = React.ComponentProps<typeof DayPicker> & {\n /** Biến thể Button dùng cho nút điều hướng tháng. */\n buttonVariant?: ButtonProps[\"variant\"];\n};\nexport type CalendarDayButtonProps = React.ComponentProps<typeof DayButton>;\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n formatters,\n components,\n locale,\n ...props\n}: CalendarProps) {\n const defaultClassNames = getDefaultClassNames();\n const localeCode = locale?.code ?? \"vi-VN\";\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n locale={locale}\n className={cn(\n \"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className\n )}\n captionLayout={captionLayout}\n formatters={{\n formatMonthDropdown: (date) => date.toLocaleString(localeCode, { month: \"short\" }),\n ...formatters\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\"flex gap-4 flex-col md:flex-row relative\", defaultClassNames.months),\n month: cn(\"flex flex-col w-full gap-4\", defaultClassNames.month),\n nav: cn(\"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between\", defaultClassNames.nav),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_previous\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_next\n ),\n month_caption: cn(\n \"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)\",\n defaultClassNames.month_caption\n ),\n dropdowns: cn(\n \"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5\",\n defaultClassNames.dropdowns\n ),\n dropdown_root: cn(\n \"relative has-focus:outline-none border border-input shadow-xs has-focus:ring-2 has-focus:ring-ring/50 has-focus:ring-offset-2 has-focus:ring-offset-background rounded-md\",\n defaultClassNames.dropdown_root\n ),\n dropdown: cn(\"absolute bg-popover inset-0 opacity-0\", defaultClassNames.dropdown),\n caption_label: cn(\n \"select-none font-medium\",\n captionLayout === \"label\"\n ? \"text-sm\"\n : \"rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5\",\n defaultClassNames.caption_label\n ),\n table: \"w-full border-collapse\",\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none\",\n defaultClassNames.weekday\n ),\n week: cn(\"flex w-full mt-2\", defaultClassNames.week),\n week_number_header: cn(\"select-none w-(--cell-size)\", defaultClassNames.week_number_header),\n week_number: cn(\"text-[0.8rem] select-none text-muted-foreground\", defaultClassNames.week_number),\n day: cn(\n \"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none\",\n defaultClassNames.day\n ),\n range_start: cn(\"rounded-l-md bg-accent\", defaultClassNames.range_start),\n range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n today: cn(\n \"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none\",\n defaultClassNames.today\n ),\n outside: cn(\"text-muted-foreground aria-selected:text-muted-foreground\", defaultClassNames.outside),\n disabled: cn(\"text-muted-foreground opacity-50\", defaultClassNames.disabled),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames\n }}\n components={{\n Root: ({ className: rootClassName, rootRef, ...rootProps }) => {\n return <div data-slot=\"calendar\" ref={rootRef} className={cn(rootClassName)} {...rootProps} />;\n },\n Chevron: ({ className: chevronClassName, orientation, ...chevronProps }) => {\n if (orientation === \"left\") {\n return <ChevronLeftIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n }\n\n if (orientation === \"right\") {\n return <ChevronRightIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n }\n\n return <ChevronDownIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n },\n DayButton: CalendarDayButton,\n WeekNumber: ({ children, week: _week, ...weekProps }) => {\n return (\n <td {...weekProps}>\n <div className=\"flex size-(--cell-size) items-center justify-center text-center\">{children}</div>\n </td>\n );\n },\n ...components\n }}\n {...props}\n />\n );\n}\n\n/**\n * Ô ngày.\n *\n * ─ `[&>span]:opacity-80` — 80% LÀ MỨC MỜ TỐI ĐA CÒN ĐẠT AA ─\n * `<span>` là nội dung phụ app truyền vào ô ngày (số việc, chấm sự kiện…), cỡ `text-xs` = 12px\n * nên chịu ngưỡng CHỮ 4,5:1. Nút ngày VẪN bấm được và vẫn nhận focus → `opacity-*` chồng lên nó\n * KHÔNG được miễn trừ theo 1.4.3. Ca tệ nhất là ngày ĐÃ CHỌN ở theme sáng (`text-background`\n * trắng trên `bg-primary-ink` #7A5800), đo theo từng mức mờ:\n *\n * opacity-70 → 4,12:1 (light) · 5,90:1 (dark) ✗ ← dưới ngưỡng ở light\n * opacity-75 → 4,46:1 · 6,82:1 ✗\n * opacity-80 → 4,85:1 · 7,97:1 ✓ ← đang dùng\n *\n * Ba con số light ở trên là của nền ô MỚI. Đừng quy chúng cho `0.1.0`: bản đó tô ô chọn bằng\n * `bg-primary` (gold #E8B824) với chữ `text-primary-foreground` (mực), nên `opacity-70` khi ấy\n * đo được 5,26:1 (light) · 5,90:1 (dark) — tức mức mờ 70% VẪN đạt. Khuyết tật thật của `0.1.0`\n * nằm ở chỗ khác: nền ô chọn ⇄ nền lịch chỉ 1,86:1 (light), tức 1.4.11 mới là điều bị vi phạm.\n * Chính việc hạ nền xuống #7A5800 + chữ trắng đã làm ca tệ nhất chặt lại và kéo 70% xuống dưới\n * ngưỡng. (Ở dark hai bản trùng số vì `--primary-ink: var(--primary)`.)\n *\n * Các trạng thái còn lại thoải mái ở 80%: giữa khoảng & hôm nay 9,70:1 (light) · 10,05:1 (dark),\n * ngày thường 10,26:1 · 11,41:1. Hạ dưới 80% là hỏng đúng một ca (chọn + theme sáng) — đúng kiểu\n * lỗi chỉ lộ ở một trạng thái mà ba vòng trước đều sót.\n */\nfunction CalendarDayButton({ className, day, modifiers, ...props }: CalendarDayButtonProps) {\n const defaultClassNames = getDefaultClassNames();\n\n const ref = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (modifiers.focused) ref.current?.focus();\n }, [modifiers.focused]);\n\n return (\n <Button\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n data-day={day.date.toLocaleDateString()}\n data-selected-single={\n modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle\n }\n data-range-start={modifiers.range_start}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n className={cn(\n \"data-[selected-single=true]:bg-primary-ink data-[selected-single=true]:text-background data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary-ink data-[range-start=true]:text-background data-[range-end=true]:bg-primary-ink data-[range-end=true]:text-background group-data-[focused=true]/day:outline-none group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-offset-2 group-data-[focused=true]/day:ring-offset-background dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-2 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-80\",\n defaultClassNames.day,\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Calendar, CalendarDayButton };\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/calendar.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { type DayButton, DayPicker, getDefaultClassNames } from \"react-day-picker\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps, buttonVariants } from \"./button.js\";\n\n/**\n * Calendar (react-day-picker v9) — lịch trần, đã map class sang token Comitor.\n * Bản có ô nhập `dd/MM/yyyy` + preset (\"Hôm nay\", \"7 ngày qua\"…) là composite\n * `DatePicker` / `DateRangePicker` ở Tier 2.\n *\n * Ngôn ngữ: truyền `locale={vi}` (date-fns/locale) để đổi tên thứ/tháng.\n *\n * Khác bản comitor-ds: tên tháng rút gọn trong dropdown lấy theo `locale.code`\n * (mặc định `vi-VN`) thay vì `\"default\"` — `\"default\"` phụ thuộc locale của môi\n * trường chạy nên server và trình duyệt có thể ra chuỗi khác nhau → hydration mismatch.\n *\n * ─ Ô NGÀY ĐANG CHỌN: `bg-primary-ink` + `text-background` ─\n * Chữ của ô ngày KHÔNG đổi màu khi được chọn (nghỉ và chọn đều là bậc mực), nên \"đang chọn\"\n * chỉ còn được kể bằng NỀN ô — thông tin thị giác, ngưỡng 3:1. `bg-primary` gold #E8B824 trên\n * nền trắng chỉ **1,86:1** ✗. Đổi sang bậc ink của brand thì đạt cả hai phía, cả hai theme:\n *\n * nền ô ⇄ nền lịch : 6,51:1 (light) · 12,67:1 (dark) ✓\n * chữ `text-background` trên nền ô : 6,51:1 (light) · 12,67:1 (dark) ✓\n *\n * Chữ phải là `text-background` (trắng ở light) chứ KHÔNG `text-primary-foreground` (mực ở cả\n * hai theme): mực trên `--primary-ink` #7A5800 chỉ 1,86:1 ✗ — cùng lý do với dấu tick\n * `Checkbox` và núm `Switch`.\n *\n * ⚠ CÒN TỒN, cố ý chưa động: `today` và `range_middle` dùng `bg-accent` — 1,09:1 (light) /\n * 1,17:1 (dark) so với nền lịch. Đưa dải giữa lên 3:1 thì cả tuần biến thành một khối đặc,\n * nên đây là quyết định THIẾT KẾ (thêm viền/gạch chân cho \"hôm nay\"?) chứ không sửa được bằng\n * đổi token — để dành cho vòng riêng.\n */\n\nexport type CalendarProps = React.ComponentProps<typeof DayPicker> & {\n /** Biến thể Button dùng cho nút điều hướng tháng. */\n buttonVariant?: ButtonProps[\"variant\"];\n};\nexport type CalendarDayButtonProps = React.ComponentProps<typeof DayButton>;\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n formatters,\n components,\n locale,\n ...props\n}: CalendarProps) {\n const defaultClassNames = getDefaultClassNames();\n const localeCode = locale?.code ?? \"vi-VN\";\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n locale={locale}\n className={cn(\n \"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className\n )}\n captionLayout={captionLayout}\n formatters={{\n formatMonthDropdown: (date) => date.toLocaleString(localeCode, { month: \"short\" }),\n ...formatters\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\"flex gap-4 flex-col md:flex-row relative\", defaultClassNames.months),\n month: cn(\"flex flex-col w-full gap-4\", defaultClassNames.month),\n nav: cn(\"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between\", defaultClassNames.nav),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_previous\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_next\n ),\n month_caption: cn(\n \"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)\",\n defaultClassNames.month_caption\n ),\n dropdowns: cn(\n \"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5\",\n defaultClassNames.dropdowns\n ),\n dropdown_root: cn(\n \"relative has-focus:outline-none border border-input shadow-xs has-focus:ring-2 has-focus:ring-ring/50 has-focus:ring-offset-2 has-focus:ring-offset-background rounded-md\",\n defaultClassNames.dropdown_root\n ),\n dropdown: cn(\"absolute bg-popover inset-0 opacity-0\", defaultClassNames.dropdown),\n caption_label: cn(\n \"select-none font-medium\",\n captionLayout === \"label\"\n ? \"text-sm\"\n : \"rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5\",\n defaultClassNames.caption_label\n ),\n table: \"w-full border-collapse\",\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none\",\n defaultClassNames.weekday\n ),\n week: cn(\"flex w-full mt-2\", defaultClassNames.week),\n week_number_header: cn(\"select-none w-(--cell-size)\", defaultClassNames.week_number_header),\n week_number: cn(\"text-[0.8rem] select-none text-muted-foreground\", defaultClassNames.week_number),\n day: cn(\n \"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none\",\n defaultClassNames.day\n ),\n range_start: cn(\"rounded-l-md bg-accent\", defaultClassNames.range_start),\n range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n today: cn(\n \"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none\",\n defaultClassNames.today\n ),\n outside: cn(\"text-muted-foreground aria-selected:text-muted-foreground\", defaultClassNames.outside),\n disabled: cn(\"text-muted-foreground opacity-50\", defaultClassNames.disabled),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames\n }}\n components={{\n Root: ({ className: rootClassName, rootRef, ...rootProps }) => {\n return <div data-slot=\"calendar\" ref={rootRef} className={cn(rootClassName)} {...rootProps} />;\n },\n Chevron: ({ className: chevronClassName, orientation, ...chevronProps }) => {\n if (orientation === \"left\") {\n return <ChevronLeftIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n }\n\n if (orientation === \"right\") {\n return <ChevronRightIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n }\n\n return <ChevronDownIcon className={cn(\"size-4\", chevronClassName)} {...chevronProps} />;\n },\n DayButton: CalendarDayButton,\n WeekNumber: ({ children, week: _week, ...weekProps }) => {\n return (\n <td {...weekProps}>\n <div className=\"flex size-(--cell-size) items-center justify-center text-center\">{children}</div>\n </td>\n );\n },\n ...components\n }}\n {...props}\n />\n );\n}\n\n/**\n * Ô ngày.\n *\n * ─ `[&>span]:opacity-80` — 80% LÀ MỨC MỜ TỐI ĐA CÒN ĐẠT AA ─\n * `<span>` là nội dung phụ app truyền vào ô ngày (số việc, chấm sự kiện…), cỡ `text-xs` = 12px\n * nên chịu ngưỡng CHỮ 4,5:1. Nút ngày VẪN bấm được và vẫn nhận focus → `opacity-*` chồng lên nó\n * KHÔNG được miễn trừ theo 1.4.3. Ca tệ nhất là ngày ĐÃ CHỌN ở theme sáng (`text-background`\n * trắng trên `bg-primary-ink` #7A5800), đo theo từng mức mờ:\n *\n * opacity-70 → 4,12:1 (light) · 5,90:1 (dark) ✗ ← dưới ngưỡng ở light\n * opacity-75 → 4,46:1 · 6,82:1 ✗\n * opacity-80 → 4,85:1 · 7,97:1 ✓ ← đang dùng\n *\n * Ba con số light ở trên là của nền ô MỚI. Đừng quy chúng cho `0.1.0`: bản đó tô ô chọn bằng\n * `bg-primary` (gold #E8B824) với chữ `text-primary-foreground` (mực), nên `opacity-70` khi ấy\n * đo được 5,26:1 (light) · 5,90:1 (dark) — tức mức mờ 70% VẪN đạt. Khuyết tật thật của `0.1.0`\n * nằm ở chỗ khác: nền ô chọn ⇄ nền lịch chỉ 1,86:1 (light), tức 1.4.11 mới là điều bị vi phạm.\n * Chính việc hạ nền xuống #7A5800 + chữ trắng đã làm ca tệ nhất chặt lại và kéo 70% xuống dưới\n * ngưỡng. (Ở dark hai bản trùng số vì `--primary-ink: var(--primary)`.)\n *\n * Các trạng thái còn lại thoải mái ở 80%: giữa khoảng & hôm nay 9,70:1 (light) · 10,05:1 (dark),\n * ngày thường 10,26:1 · 11,41:1. Hạ dưới 80% là hỏng đúng một ca (chọn + theme sáng) — đúng kiểu\n * lỗi chỉ lộ ở một trạng thái mà ba vòng trước đều sót.\n */\nfunction CalendarDayButton({ className, day, modifiers, ...props }: CalendarDayButtonProps) {\n const defaultClassNames = getDefaultClassNames();\n\n const ref = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (modifiers.focused) ref.current?.focus();\n }, [modifiers.focused]);\n\n return (\n <Button\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n data-day={day.date.toLocaleDateString()}\n data-selected-single={\n modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle\n }\n data-range-start={modifiers.range_start}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n className={cn(\n \"data-[selected-single=true]:bg-primary-ink data-[selected-single=true]:text-control-check data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary-ink data-[range-start=true]:text-control-check data-[range-end=true]:bg-primary-ink data-[range-end=true]:text-control-check group-data-[focused=true]/day:outline-none group-data-[focused=true]/day:ring-ring/50 group-data-[focused=true]/day:ring-offset-2 group-data-[focused=true]/day:ring-offset-background dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-2 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-80\",\n defaultClassNames.day,\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Calendar, CalendarDayButton };\n"],"mappings":"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","names":[]}
@@ -16,8 +16,8 @@ function Checkbox({ className, label, description, indeterminate, checked, id, .
16
16
  className: cn(
17
17
  "peer size-4 shrink-0 rounded border border-input bg-background",
18
18
  "transition-colors duration-150 ease-out",
19
- "data-[state=checked]:bg-control-on data-[state=checked]:border-control-on data-[state=checked]:text-control-knob",
20
- "data-[state=indeterminate]:bg-control-on data-[state=indeterminate]:border-control-on data-[state=indeterminate]:text-control-knob",
19
+ "data-[state=checked]:bg-control-on data-[state=checked]:border-control-on data-[state=checked]:text-control-check",
20
+ "data-[state=indeterminate]:bg-control-on data-[state=indeterminate]:border-control-on data-[state=indeterminate]:text-control-check",
21
21
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
22
22
  "hover:border-control-edge-strong",
23
23
  "disabled:cursor-not-allowed disabled:opacity-50",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/checkbox.tsx"],"sourcesContent":["\"use client\";\n\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { CheckIcon, MinusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\nexport interface CheckboxProps extends React.ComponentProps<typeof CheckboxPrimitive.Root> {\n /** Nhãn đặt cạnh ô đánh dấu; có nhãn thì component tự bọc thêm lớp bố cục. */\n label?: string;\n /** Mô tả phụ dưới nhãn. */\n description?: string;\n /**\n * Đường tắt cho trạng thái \"chọn một phần\" — đè lên `checked`.\n * Tương đương `checked=\"indeterminate\"`; giữ lại cho tương thích ngược.\n */\n indeterminate?: boolean;\n}\n\n/*\n * Style port từ comitor-ds (đổi gray-* → ash-* theo namespace của package).\n *\n * ─ ICON BÁM THEO STATE THẬT, KHÔNG THEO PROP ─\n * Bản cũ chọn icon bằng `indeterminate ? <Minus/> : <Check/>`, tức chỉ nhìn PROP. Ai truyền\n * `checked=\"indeterminate\"` thẳng (Radix cho phép, và `composite/data-table.tsx` đang làm đúng\n * như vậy cho ô \"chọn tất cả\") thì nền chuyển sang trạng thái một phần nhưng icon vẫn là dấu\n * tick → mới chọn vài dòng mà đầu bảng trông y hệt đã chọn hết. Trường hợp không kiểm soát\n * (`defaultChecked=\"indeterminate\"`) còn sai cả sau khi người dùng bấm.\n * Nay cả hai icon cùng được render và bật/tắt bằng `data-state` của chính Radix Indicator\n * (Radix phát `data-state=\"checked\" | \"indeterminate\"` trên Indicator) → luôn khớp state thật,\n * kể cả ở nhánh không kiểm soát.\n *\n * ─ MÀU: BA PHÉP ĐO, GIỐNG `Switch` ─\n *\n * Ô đánh dấu là thành phần phi văn bản (WCAG 1.4.11, ngưỡng 3:1) và phải nhận ra được ở cả hai\n * trạng thái:\n *\n * (1) viền/ô ⇄ nền trang → có nhìn thấy ô không\n * (2) dấu tick ⇄ nền ô → nó đang được chọn hay không\n *\n * `bg-primary` (gold #E8B824 ở light) chỉ đạt **1,86:1** với nền trắng, nên ô ĐÃ CHỌN mất hẳn\n * ranh giới ở light — chỉ dấu tick còn nổi. Vai trò \"nền control khi BẬT/ĐÃ CHỌN\" có token\n * riêng là `--control-on` (= `--primary-ink`: gold-500 ở light, = `--primary` ở dark), đạt cả\n * hai phía ở cả hai theme:\n *\n * ô ⇄ nền trang : 6,51:1 (light) · 12,67:1 (dark) ✓\n * tick ⇄ nền ô : 6,51:1 (light) · 12,67:1 (dark) ✓\n *\n * Dấu tick là `text-control-knob` (= `--background`: trắng ở light, #0E1116 ở dark) chứ không\n * phải `text-primary-foreground` (mực ở CẢ hai theme): mực trên gold-500 chỉ 1,86:1 ✗. Cùng một\n * lối với núm của `Switch` — phần chỉ báo mang màu NỀN TRANG nên luôn tương phản đúng chiều.\n *\n * ─ VIỀN LÚC NGHỈ VÀ LÚC HOVER ─\n * `border-input` nay trỏ vào `--control-edge` #7E8290 (3,83:1 light · 4,94:1 dark ✓) — trước đó\n * là #EDEEF1, chỉ 1,16:1, tức ô chưa chọn gần như tàng hình. Hover phải ĐẬM HƠN lúc nghỉ, không\n * nhạt đi: `border-ash-300` cũ ở light chỉ 1,62:1 ✗. Nay đi qua `--control-edge-strong` (thang\n * ash được token hoá đúng vai trò này): light 10,55:1 · dark 11,65:1 trên nền trang, 10,94:1\n * trên `--card` ✓ — dùng token thay vì `ash-700`/`ash-300` thô để app đổi thang xám không vỡ.\n */\nfunction Checkbox({ className, label, description, indeterminate, checked, id, ...props }: CheckboxProps) {\n const generatedId = React.useId();\n const checkboxId = id ?? generatedId;\n\n const control = (\n <CheckboxPrimitive.Root\n id={checkboxId}\n data-slot=\"checkbox\"\n checked={indeterminate ? \"indeterminate\" : checked}\n className={cn(\n \"peer size-4 shrink-0 rounded border border-input bg-background\",\n \"transition-colors duration-150 ease-out\",\n \"data-[state=checked]:bg-control-on data-[state=checked]:border-control-on data-[state=checked]:text-control-knob\",\n \"data-[state=indeterminate]:bg-control-on data-[state=indeterminate]:border-control-on data-[state=indeterminate]:text-control-knob\",\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 \"hover:border-control-edge-strong\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"group/checkbox-indicator flex items-center justify-center text-current transition-none\"\n >\n <MinusIcon\n className=\"hidden size-2.5 group-data-[state=indeterminate]/checkbox-indicator:block\"\n strokeWidth={3}\n />\n <CheckIcon className=\"hidden size-2.5 group-data-[state=checked]/checkbox-indicator:block\" strokeWidth={3} />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n );\n\n if (!label && !description) {\n return control;\n }\n\n return (\n <div className=\"flex items-start gap-2.5\">\n {control}\n <div className=\"flex flex-col gap-0.5\">\n {label && (\n <label\n htmlFor={checkboxId}\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 { Checkbox };\n"],"mappings":";AAgFM,SAIE,KAJF;AA9EN,YAAY,uBAAuB;AACnC,SAAS,WAAW,iBAAiB;AACrC,YAAY,WAAW;AACvB,SAAS,UAAU;AAsDnB,SAAS,SAAS,EAAE,WAAW,OAAO,aAAa,eAAe,SAAS,IAAI,GAAG,MAAM,GAAkB;AACxG,QAAM,cAAc,MAAM,MAAM;AAChC,QAAM,aAAa,MAAM;AAEzB,QAAM,UACJ;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,IAAI;AAAA,MACJ,aAAU;AAAA,MACV,SAAS,gBAAgB,kBAAkB;AAAA,MAC3C,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,kBAAkB;AAAA,QAAlB;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAEV;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,aAAa;AAAA;AAAA,YACf;AAAA,YACA,oBAAC,aAAU,WAAU,uEAAsE,aAAa,GAAG;AAAA;AAAA;AAAA,MAC7G;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
+ {"version":3,"sources":["../../../src/components/ui/checkbox.tsx"],"sourcesContent":["\"use client\";\n\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { CheckIcon, MinusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\nexport interface CheckboxProps extends React.ComponentProps<typeof CheckboxPrimitive.Root> {\n /** Nhãn đặt cạnh ô đánh dấu; có nhãn thì component tự bọc thêm lớp bố cục. */\n label?: string;\n /** Mô tả phụ dưới nhãn. */\n description?: string;\n /**\n * Đường tắt cho trạng thái \"chọn một phần\" — đè lên `checked`.\n * Tương đương `checked=\"indeterminate\"`; giữ lại cho tương thích ngược.\n */\n indeterminate?: boolean;\n}\n\n/*\n * Style port từ comitor-ds (đổi gray-* → ash-* theo namespace của package).\n *\n * ─ ICON BÁM THEO STATE THẬT, KHÔNG THEO PROP ─\n * Bản cũ chọn icon bằng `indeterminate ? <Minus/> : <Check/>`, tức chỉ nhìn PROP. Ai truyền\n * `checked=\"indeterminate\"` thẳng (Radix cho phép, và `composite/data-table.tsx` đang làm đúng\n * như vậy cho ô \"chọn tất cả\") thì nền chuyển sang trạng thái một phần nhưng icon vẫn là dấu\n * tick → mới chọn vài dòng mà đầu bảng trông y hệt đã chọn hết. Trường hợp không kiểm soát\n * (`defaultChecked=\"indeterminate\"`) còn sai cả sau khi người dùng bấm.\n * Nay cả hai icon cùng được render và bật/tắt bằng `data-state` của chính Radix Indicator\n * (Radix phát `data-state=\"checked\" | \"indeterminate\"` trên Indicator) → luôn khớp state thật,\n * kể cả ở nhánh không kiểm soát.\n *\n * ─ MÀU: ĐI QUA TOKEN VAI TRÒ, GIỐNG `Switch` ─\n *\n * Ô đánh dấu là thành phần phi văn bản (WCAG 1.4.11, ngưỡng 3:1) và phải nhận ra được ở cả hai\n * trạng thái: (1) viền/ô ⇄ nền trang, (2) dấu tick ⇄ nền ô.\n *\n * ô ĐÃ CHỌN `data-[state=checked]:bg-control-on` (comitor-ds: bg-gold-300)\n * dấu tick `data-[state=checked]:text-control-check` (comitor-ds: text-ink)\n * viền nghỉ `border-input` · hover `border-control-edge-strong`\n *\n * ⚠ Dấu tick dùng `--control-check`, KHÔNG phải `--control-knob` (núm Switch) và cũng không phải\n * `text-ink` cứng: hai bảng màu lật ngược nhau ở đúng chỗ này. Bảng mặc định tô nền ô bằng gold\n * SÁNG nên tick phải là MỰC (10,19:1); bảng tương phản cao tô gold-500 TỐI nên tick phải là màu\n * nền trang (6,51:1). Núm Switch thì trắng ở bảng duyệt nên nó ở lại `--control-knob` — đó là lý\n * do hai vai trò tách thành hai token.\n *\n * Số đo hai bảng (light):\n *\n * bảng MẶC ĐỊNH bảng TƯƠNG PHẢN CAO\n * ô ⇄ nền trang 1,86:1 ✗ (đã chấp nhận) 6,51:1 ✓\n * tick ⇄ nền ô 10,19:1 ✓ 6,51:1 ✓\n * viền nghỉ ⇄ nền 1,16:1 ✗ (đã chấp nhận) 3,83:1 ✓\n *\n * Hover phải ĐẬM HƠN lúc nghỉ, không nhạt đi — `--control-edge-strong` là ash-300 ở light và\n * ash-500 ở dark, đúng cặp `hover:border-gray-300 dark:hover:border-gray-500` của comitor-ds.\n */\nfunction Checkbox({ className, label, description, indeterminate, checked, id, ...props }: CheckboxProps) {\n const generatedId = React.useId();\n const checkboxId = id ?? generatedId;\n\n const control = (\n <CheckboxPrimitive.Root\n id={checkboxId}\n data-slot=\"checkbox\"\n checked={indeterminate ? \"indeterminate\" : checked}\n className={cn(\n \"peer size-4 shrink-0 rounded border border-input bg-background\",\n \"transition-colors duration-150 ease-out\",\n \"data-[state=checked]:bg-control-on data-[state=checked]:border-control-on data-[state=checked]:text-control-check\",\n \"data-[state=indeterminate]:bg-control-on data-[state=indeterminate]:border-control-on data-[state=indeterminate]:text-control-check\",\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 \"hover:border-control-edge-strong\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"group/checkbox-indicator flex items-center justify-center text-current transition-none\"\n >\n <MinusIcon\n className=\"hidden size-2.5 group-data-[state=indeterminate]/checkbox-indicator:block\"\n strokeWidth={3}\n />\n <CheckIcon className=\"hidden size-2.5 group-data-[state=checked]/checkbox-indicator:block\" strokeWidth={3} />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n );\n\n if (!label && !description) {\n return control;\n }\n\n return (\n <div className=\"flex items-start gap-2.5\">\n {control}\n <div className=\"flex flex-col gap-0.5\">\n {label && (\n <label\n htmlFor={checkboxId}\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 { Checkbox };\n"],"mappings":";AA8EM,SAIE,KAJF;AA5EN,YAAY,uBAAuB;AACnC,SAAS,WAAW,iBAAiB;AACrC,YAAY,WAAW;AACvB,SAAS,UAAU;AAoDnB,SAAS,SAAS,EAAE,WAAW,OAAO,aAAa,eAAe,SAAS,IAAI,GAAG,MAAM,GAAkB;AACxG,QAAM,cAAc,MAAM,MAAM;AAChC,QAAM,aAAa,MAAM;AAEzB,QAAM,UACJ;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,IAAI;AAAA,MACJ,aAAU;AAAA,MACV,SAAS,gBAAgB,kBAAkB;AAAA,MAC3C,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,kBAAkB;AAAA,QAAlB;AAAA,UACC,aAAU;AAAA,UACV,WAAU;AAAA,UAEV;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,aAAa;AAAA;AAAA,YACf;AAAA,YACA,oBAAC,aAAU,WAAU,uEAAsE,aAAa,GAAG;AAAA;AAAA;AAAA,MAC7G;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":[]}
@@ -35,7 +35,7 @@ function CommandDialog({
35
35
  }
36
36
  function CommandInput({ className, ...props }) {
37
37
  return /* @__PURE__ */ jsxs("div", { "data-slot": "command-input-wrapper", className: "flex h-9 items-center gap-2 border-b px-3", children: [
38
- /* @__PURE__ */ jsx(SearchIcon, { className: "text-muted-foreground size-4 shrink-0" }),
38
+ /* @__PURE__ */ jsx(SearchIcon, { className: "size-4 shrink-0 opacity-50" }),
39
39
  /* @__PURE__ */ jsx(
40
40
  CommandPrimitive.Input,
41
41
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/command.tsx"],"sourcesContent":["\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog.js\";\n\n/**\n * Command — danh sách lệnh có lọc theo phím gõ (cmdk).\n *\n * Đây là primitive trần: composite `CommandPalette` và `Combobox` (Tier 2) dựng lên\n * từ đây. `CommandDialog` bọc sẵn trong `Dialog` cho bảng lệnh Cmd/Ctrl+K.\n *\n * Lưu ý a11y: `CommandDialog` bắt buộc có tiêu đề + mô tả cho trình đọc màn hình\n * (đặt trong `DialogHeader` ẩn `sr-only`) — đổi bằng prop `title`/`description`.\n */\n\nexport type CommandProps = React.ComponentProps<typeof CommandPrimitive>;\n\nfunction Command({ className, ...props }: CommandProps) {\n return (\n <CommandPrimitive\n data-slot=\"command\"\n className={cn(\n \"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandDialogProps = React.ComponentProps<typeof Dialog> & {\n /** Tiêu đề cho trình đọc màn hình. Mặc định \"Bảng lệnh\". */\n title?: string;\n /** Mô tả cho trình đọc màn hình. Mặc định \"Tìm và chạy một lệnh…\". */\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n};\n\nfunction CommandDialog({\n title = \"Bảng lệnh\",\n description = \"Tìm và chạy một lệnh…\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: CommandDialogProps) {\n return (\n <Dialog {...props}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <DialogContent className={cn(\"overflow-hidden p-0\", className)} showCloseButton={showCloseButton}>\n <Command className=\"[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport type CommandInputProps = React.ComponentProps<typeof CommandPrimitive.Input>;\n\nfunction CommandInput({ className, ...props }: CommandInputProps) {\n return (\n <div data-slot=\"command-input-wrapper\" className=\"flex h-9 items-center gap-2 border-b px-3\">\n {/* Icon kính lúp là icon MANG NGHĨA (nói \"đây là ô tìm kiếm\") → ngưỡng 3:1.\n `opacity-50` cũ hạ màu kế thừa xuống 3,56:1 (light) / 4,99:1 (dark) — đạt nhưng sát,\n và là con số không token nào bảo vệ. `--muted-foreground` là bậc dành đúng cho vai trò\n này: 5,23:1 (light) · 5,35:1 (dark) trên nền popover. */}\n <SearchIcon className=\"text-muted-foreground size-4 shrink-0\" />\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n className={cn(\n \"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </div>\n );\n}\n\nexport type CommandListProps = React.ComponentProps<typeof CommandPrimitive.List>;\n\nfunction CommandList({ className, ...props }: CommandListProps) {\n return (\n <CommandPrimitive.List\n data-slot=\"command-list\"\n className={cn(\"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\", className)}\n {...props}\n />\n );\n}\n\nexport type CommandEmptyProps = React.ComponentProps<typeof CommandPrimitive.Empty>;\n\nfunction CommandEmpty({ ...props }: CommandEmptyProps) {\n return <CommandPrimitive.Empty data-slot=\"command-empty\" className=\"py-6 text-center text-sm\" {...props} />;\n}\n\nexport type CommandGroupProps = React.ComponentProps<typeof CommandPrimitive.Group>;\n\nfunction CommandGroup({ className, ...props }: CommandGroupProps) {\n return (\n <CommandPrimitive.Group\n data-slot=\"command-group\"\n className={cn(\n \"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandSeparatorProps = React.ComponentProps<typeof CommandPrimitive.Separator>;\n\nfunction CommandSeparator({ className, ...props }: CommandSeparatorProps) {\n return (\n <CommandPrimitive.Separator\n data-slot=\"command-separator\"\n className={cn(\"bg-border -mx-1 h-px\", className)}\n {...props}\n />\n );\n}\n\nexport type CommandItemProps = React.ComponentProps<typeof CommandPrimitive.Item>;\n\nfunction CommandItem({ className, ...props }: CommandItemProps) {\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\n className={cn(\n \"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandShortcutProps = React.ComponentProps<\"span\">;\n\nfunction CommandShortcut({ className, ...props }: CommandShortcutProps) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\"text-muted-foreground ml-auto text-xs tracking-widest\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut\n};\n"],"mappings":";AAsBI,cA8BE,YA9BF;AApBJ,SAAS,WAAW,wBAAwB;AAC5C,SAAS,kBAAkB;AAE3B,SAAS,UAAU;AACnB,SAAS,QAAQ,eAAe,mBAAmB,cAAc,mBAAmB;AAcpF,SAAS,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAiB;AACtD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAWA,SAAS,cAAc;AAAA,EACrB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,GAAG;AACL,GAAuB;AACrB,SACE,qBAAC,UAAQ,GAAG,OACV;AAAA,yBAAC,gBAAa,WAAU,WACtB;AAAA,0BAAC,eAAa,iBAAM;AAAA,MACpB,oBAAC,qBAAmB,uBAAY;AAAA,OAClC;AAAA,IACA,oBAAC,iBAAc,WAAW,GAAG,uBAAuB,SAAS,GAAG,iBAC9D,8BAAC,WAAQ,WAAU,yZAChB,UACH,GACF;AAAA,KACF;AAEJ;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE,qBAAC,SAAI,aAAU,yBAAwB,WAAU,6CAK/C;AAAA,wBAAC,cAAW,WAAU,yCAAwC;AAAA,IAC9D;AAAA,MAAC,iBAAiB;AAAA,MAAjB;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA,KACF;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,+DAA+D,SAAS;AAAA,MACrF,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,aAAa,EAAE,GAAG,MAAM,GAAsB;AACrD,SAAO,oBAAC,iBAAiB,OAAjB,EAAuB,aAAU,iBAAgB,WAAU,4BAA4B,GAAG,OAAO;AAC3G;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,wBAAwB,SAAS;AAAA,MAC9C,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,yDAAyD,SAAS;AAAA,MAC/E,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/command.tsx"],"sourcesContent":["\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog.js\";\n\n/**\n * Command — danh sách lệnh có lọc theo phím gõ (cmdk).\n *\n * Đây là primitive trần: composite `CommandPalette` và `Combobox` (Tier 2) dựng lên\n * từ đây. `CommandDialog` bọc sẵn trong `Dialog` cho bảng lệnh Cmd/Ctrl+K.\n *\n * Lưu ý a11y: `CommandDialog` bắt buộc có tiêu đề + mô tả cho trình đọc màn hình\n * (đặt trong `DialogHeader` ẩn `sr-only`) — đổi bằng prop `title`/`description`.\n */\n\nexport type CommandProps = React.ComponentProps<typeof CommandPrimitive>;\n\nfunction Command({ className, ...props }: CommandProps) {\n return (\n <CommandPrimitive\n data-slot=\"command\"\n className={cn(\n \"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandDialogProps = React.ComponentProps<typeof Dialog> & {\n /** Tiêu đề cho trình đọc màn hình. Mặc định \"Bảng lệnh\". */\n title?: string;\n /** Mô tả cho trình đọc màn hình. Mặc định \"Tìm và chạy một lệnh…\". */\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n};\n\nfunction CommandDialog({\n title = \"Bảng lệnh\",\n description = \"Tìm và chạy một lệnh…\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: CommandDialogProps) {\n return (\n <Dialog {...props}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <DialogContent className={cn(\"overflow-hidden p-0\", className)} showCloseButton={showCloseButton}>\n <Command className=\"[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport type CommandInputProps = React.ComponentProps<typeof CommandPrimitive.Input>;\n\nfunction CommandInput({ className, ...props }: CommandInputProps) {\n return (\n <div data-slot=\"command-input-wrapper\" className=\"flex h-9 items-center gap-2 border-b px-3\">\n {/* Icon kính lúp là icon MANG NGHĨA (nói \"đây là ô tìm kiếm\") → ngưỡng 3:1.\n `opacity-50` cũ hạ màu kế thừa xuống 3,56:1 (light) / 4,99:1 (dark) — đạt nhưng sát,\n và là con số không token nào bảo vệ. `--muted-foreground` là bậc dành đúng cho vai trò\n này: 5,23:1 (light) · 5,35:1 (dark) trên nền popover. */}\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n className={cn(\n \"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </div>\n );\n}\n\nexport type CommandListProps = React.ComponentProps<typeof CommandPrimitive.List>;\n\nfunction CommandList({ className, ...props }: CommandListProps) {\n return (\n <CommandPrimitive.List\n data-slot=\"command-list\"\n className={cn(\"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\", className)}\n {...props}\n />\n );\n}\n\nexport type CommandEmptyProps = React.ComponentProps<typeof CommandPrimitive.Empty>;\n\nfunction CommandEmpty({ ...props }: CommandEmptyProps) {\n return <CommandPrimitive.Empty data-slot=\"command-empty\" className=\"py-6 text-center text-sm\" {...props} />;\n}\n\nexport type CommandGroupProps = React.ComponentProps<typeof CommandPrimitive.Group>;\n\nfunction CommandGroup({ className, ...props }: CommandGroupProps) {\n return (\n <CommandPrimitive.Group\n data-slot=\"command-group\"\n className={cn(\n \"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandSeparatorProps = React.ComponentProps<typeof CommandPrimitive.Separator>;\n\nfunction CommandSeparator({ className, ...props }: CommandSeparatorProps) {\n return (\n <CommandPrimitive.Separator\n data-slot=\"command-separator\"\n className={cn(\"bg-border -mx-1 h-px\", className)}\n {...props}\n />\n );\n}\n\nexport type CommandItemProps = React.ComponentProps<typeof CommandPrimitive.Item>;\n\nfunction CommandItem({ className, ...props }: CommandItemProps) {\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\n className={cn(\n \"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandShortcutProps = React.ComponentProps<\"span\">;\n\nfunction CommandShortcut({ className, ...props }: CommandShortcutProps) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\"text-muted-foreground ml-auto text-xs tracking-widest\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut\n};\n"],"mappings":";AAsBI,cA8BE,YA9BF;AApBJ,SAAS,WAAW,wBAAwB;AAC5C,SAAS,kBAAkB;AAE3B,SAAS,UAAU;AACnB,SAAS,QAAQ,eAAe,mBAAmB,cAAc,mBAAmB;AAcpF,SAAS,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAiB;AACtD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAWA,SAAS,cAAc;AAAA,EACrB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,GAAG;AACL,GAAuB;AACrB,SACE,qBAAC,UAAQ,GAAG,OACV;AAAA,yBAAC,gBAAa,WAAU,WACtB;AAAA,0BAAC,eAAa,iBAAM;AAAA,MACpB,oBAAC,qBAAmB,uBAAY;AAAA,OAClC;AAAA,IACA,oBAAC,iBAAc,WAAW,GAAG,uBAAuB,SAAS,GAAG,iBAC9D,8BAAC,WAAQ,WAAU,yZAChB,UACH,GACF;AAAA,KACF;AAEJ;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE,qBAAC,SAAI,aAAU,yBAAwB,WAAU,6CAK/C;AAAA,wBAAC,cAAW,WAAU,8BAA6B;AAAA,IACnD;AAAA,MAAC,iBAAiB;AAAA,MAAjB;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA,KACF;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,+DAA+D,SAAS;AAAA,MACrF,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,aAAa,EAAE,GAAG,MAAM,GAAsB;AACrD,SAAO,oBAAC,iBAAiB,OAAjB,EAAuB,aAAU,iBAAgB,WAAU,4BAA4B,GAAG,OAAO;AAC3G;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,wBAAwB,SAAS;AAAA,MAC9C,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,yDAAyD,SAAS;AAAA,MAC/E,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -12,7 +12,7 @@ function InputGroup({ className, ...props }) {
12
12
  "data-slot": "input-group",
13
13
  role: "group",
14
14
  className: cn(
15
- "group/input-group border-input relative flex w-full items-center rounded-md border shadow-xs transition-colors duration-150 ease-out outline-none",
15
+ "group/input-group border-input dark:bg-input-fill relative flex w-full items-center rounded-md border shadow-xs transition-colors duration-150 ease-out outline-none",
16
16
  "h-9 has-[>textarea]:h-auto",
17
17
  // Biến thể theo vị trí addon.
18
18
  "has-[>[data-align=inline-start]]:[&>input]:pl-2",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/input-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps } from \"./button.js\";\nimport { Input } from \"./input.js\";\nimport { Textarea } from \"./textarea.js\";\n\n/*\n * ─ KHÔNG CÓ `dark:bg-input/30` (cùng lý do với Textarea) ─\n * `--input` = `--control-edge` là token VIỀN control (hợp đồng màu §3), không phải bậc tô nền.\n * Riêng ở đây cái sai còn kín hơn: lớp tô nằm trên thẻ CHA, còn chữ `--muted-foreground` nằm ở\n * `InputGroupAddon` và ở placeholder của `Input`/`Textarea` con — tức KHÁC chuỗi lớp, nên\n * `scripts/check-contrast.mjs` không thấy được cặp này (giới hạn §8.1). Số vẫn hỏng y hệt\n * Textarea: 3,81 · 3,54 · 3,22:1 ✗ ở dark. Bỏ lớp tô → addon và placeholder về đo với nền TRANG,\n * 5,70 · 5,35 · 4,86:1 ✓; khung nhóm vẫn do `border-input` khoanh (4,21–4,94:1 ở dark).\n */\nexport type InputGroupProps = React.ComponentProps<\"div\">;\n\nfunction InputGroup({ className, ...props }: InputGroupProps) {\n return (\n <div\n data-slot=\"input-group\"\n role=\"group\"\n className={cn(\n \"group/input-group border-input relative flex w-full items-center rounded-md border shadow-xs transition-colors duration-150 ease-out outline-none\",\n \"h-9 has-[>textarea]:h-auto\",\n\n // Biến thể theo vị trí addon.\n \"has-[>[data-align=inline-start]]:[&>input]:pl-2\",\n \"has-[>[data-align=inline-end]]:[&>input]:pr-2\",\n \"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3\",\n \"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3\",\n\n // Trạng thái focus.\n \"has-[[data-slot=input-group-control]:focus-visible]:border-primary-ink has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-background\",\n\n // Trạng thái lỗi.\n \"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive-ink dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40\",\n\n className\n )}\n {...props}\n />\n );\n}\n\nconst inputGroupAddonVariants = cva(\n \"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n {\n variants: {\n align: {\n \"inline-start\": \"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]\",\n \"inline-end\": \"order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]\",\n \"block-start\":\n \"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5\",\n \"block-end\": \"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5\"\n }\n },\n defaultVariants: {\n align: \"inline-start\"\n }\n }\n);\n\nexport interface InputGroupAddonProps\n extends React.ComponentProps<\"div\">,\n VariantProps<typeof inputGroupAddonVariants> {}\n\n/*\n * Bấm vào vùng addon thì dồn focus sang ô nhập bên trong (trừ khi bấm trúng một nút).\n * Đây là tiện ích chuột thuần tuý — bàn phím vẫn Tab thẳng vào input/button nên không\n * cần onKeyDown; addon không phải phần tử tương tác.\n */\nfunction InputGroupAddon({ className, align = \"inline-start\", ...props }: InputGroupAddonProps) {\n return (\n // biome-ignore lint/a11y/useKeyWithClickEvents: chỉ là lối tắt bằng chuột, bàn phím đã Tab thẳng tới control\n <div\n role=\"group\"\n data-slot=\"input-group-addon\"\n data-align={align}\n className={cn(inputGroupAddonVariants({ align }), className)}\n onClick={(event) => {\n if ((event.target as HTMLElement).closest(\"button\")) {\n return;\n }\n event.currentTarget.parentElement?.querySelector(\"input\")?.focus();\n }}\n {...props}\n />\n );\n}\n\nconst inputGroupButtonVariants = cva(\"text-sm shadow-none flex gap-2 items-center\", {\n variants: {\n size: {\n xs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n sm: \"h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5\",\n \"icon-xs\": \"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0\",\n \"icon-sm\": \"size-8 p-0 has-[>svg]:p-0\"\n }\n },\n defaultVariants: {\n size: \"xs\"\n }\n});\n\nexport type InputGroupButtonProps = Omit<ButtonProps, \"size\"> & VariantProps<typeof inputGroupButtonVariants>;\n\nfunction InputGroupButton({\n className,\n type = \"button\",\n variant = \"ghost\",\n size = \"xs\",\n ...props\n}: InputGroupButtonProps) {\n return (\n <Button\n type={type}\n data-size={size}\n variant={variant}\n className={cn(inputGroupButtonVariants({ size }), className)}\n {...props}\n />\n );\n}\n\nexport type InputGroupTextProps = React.ComponentProps<\"span\">;\n\nfunction InputGroupText({ className, ...props }: InputGroupTextProps) {\n return (\n <span\n className={cn(\n \"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type InputGroupInputProps = React.ComponentProps<\"input\">;\n\nfunction InputGroupInput({ className, ...props }: InputGroupInputProps) {\n return (\n <Input\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type InputGroupTextareaProps = React.ComponentProps<\"textarea\">;\n\nfunction InputGroupTextarea({ className, ...props }: InputGroupTextareaProps) {\n return (\n <Textarea\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n InputGroupText,\n InputGroupTextarea,\n inputGroupAddonVariants,\n inputGroupButtonVariants\n};\n"],"mappings":";AAsBI;AApBJ,SAAS,WAA8B;AAEvC,SAAS,UAAU;AACnB,SAAS,cAAgC;AACzC,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAazB,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,QAGA;AAAA;AAAA,QAGA;AAAA,QAEA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,MAAM,0BAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,OAAO;AAAA,QACL,gBAAgB;AAAA,QAChB,cAAc;AAAA,QACd,eACE;AAAA,QACF,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,OAAO;AAAA,IACT;AAAA,EACF;AACF;AAWA,SAAS,gBAAgB,EAAE,WAAW,QAAQ,gBAAgB,GAAG,MAAM,GAAyB;AAC9F;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAU;AAAA,QACV,cAAY;AAAA,QACZ,WAAW,GAAG,wBAAwB,EAAE,MAAM,CAAC,GAAG,SAAS;AAAA,QAC3D,SAAS,CAAC,UAAU;AAClB,cAAK,MAAM,OAAuB,QAAQ,QAAQ,GAAG;AACnD;AAAA,UACF;AACA,gBAAM,cAAc,eAAe,cAAc,OAAO,GAAG,MAAM;AAAA,QACnE;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA;AAEJ;AAEA,MAAM,2BAA2B,IAAI,+CAA+C;AAAA,EAClF,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,MAAM;AAAA,EACR;AACF,CAAC;AAID,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA,OAAO;AAAA,EACP,UAAU;AAAA,EACV,OAAO;AAAA,EACP,GAAG;AACL,GAA0B;AACxB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,aAAW;AAAA,MACX;AAAA,MACA,WAAW,GAAG,yBAAyB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MAC1D,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/input-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button, type ButtonProps } from \"./button.js\";\nimport { Input } from \"./input.js\";\nimport { Textarea } from \"./textarea.js\";\n\n/*\n * ─ LỚP TÔ Ở DARK: `dark:bg-input-fill`, KHÔNG PHẢI `bg-input/30` (cùng lý do với Textarea) ─\n * Ở bảng mặc định lớp tô là thứ khoanh vùng nhóm ở dark; ở bảng tương phản cao nó trong suốt vì\n * `--input` bên đó là #7E8290 và tô 30% sẽ kéo chữ xuống dưới ngưỡng.\n *\n * Riêng ở đây cái sai còn kín hơn Textarea nếu ai đó viết thẳng `bg-input/30`: lớp tô nằm trên\n * thẻ CHA, còn chữ `--muted-foreground` nằm ở `InputGroupAddon` và ở placeholder của\n * `Input`/`Textarea` con — tức KHÁC chuỗi lớp, nên `scripts/check-contrast.mjs` không thấy được\n * cặp này (giới hạn §8.1). Con số nếu viết thẳng: 3,81 · 3,54 · 3,22:1 ✗ ở dark.\n */\nexport type InputGroupProps = React.ComponentProps<\"div\">;\n\nfunction InputGroup({ className, ...props }: InputGroupProps) {\n return (\n <div\n data-slot=\"input-group\"\n role=\"group\"\n className={cn(\n \"group/input-group border-input dark:bg-input-fill relative flex w-full items-center rounded-md border shadow-xs transition-colors duration-150 ease-out outline-none\",\n \"h-9 has-[>textarea]:h-auto\",\n\n // Biến thể theo vị trí addon.\n \"has-[>[data-align=inline-start]]:[&>input]:pl-2\",\n \"has-[>[data-align=inline-end]]:[&>input]:pr-2\",\n \"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3\",\n \"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3\",\n\n // Trạng thái focus.\n \"has-[[data-slot=input-group-control]:focus-visible]:border-primary-ink has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-background\",\n\n // Trạng thái lỗi.\n \"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive-ink dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40\",\n\n className\n )}\n {...props}\n />\n );\n}\n\nconst inputGroupAddonVariants = cva(\n \"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n {\n variants: {\n align: {\n \"inline-start\": \"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]\",\n \"inline-end\": \"order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]\",\n \"block-start\":\n \"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5\",\n \"block-end\": \"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5\"\n }\n },\n defaultVariants: {\n align: \"inline-start\"\n }\n }\n);\n\nexport interface InputGroupAddonProps\n extends React.ComponentProps<\"div\">,\n VariantProps<typeof inputGroupAddonVariants> {}\n\n/*\n * Bấm vào vùng addon thì dồn focus sang ô nhập bên trong (trừ khi bấm trúng một nút).\n * Đây là tiện ích chuột thuần tuý — bàn phím vẫn Tab thẳng vào input/button nên không\n * cần onKeyDown; addon không phải phần tử tương tác.\n */\nfunction InputGroupAddon({ className, align = \"inline-start\", ...props }: InputGroupAddonProps) {\n return (\n // biome-ignore lint/a11y/useKeyWithClickEvents: chỉ là lối tắt bằng chuột, bàn phím đã Tab thẳng tới control\n <div\n role=\"group\"\n data-slot=\"input-group-addon\"\n data-align={align}\n className={cn(inputGroupAddonVariants({ align }), className)}\n onClick={(event) => {\n if ((event.target as HTMLElement).closest(\"button\")) {\n return;\n }\n event.currentTarget.parentElement?.querySelector(\"input\")?.focus();\n }}\n {...props}\n />\n );\n}\n\nconst inputGroupButtonVariants = cva(\"text-sm shadow-none flex gap-2 items-center\", {\n variants: {\n size: {\n xs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n sm: \"h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5\",\n \"icon-xs\": \"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0\",\n \"icon-sm\": \"size-8 p-0 has-[>svg]:p-0\"\n }\n },\n defaultVariants: {\n size: \"xs\"\n }\n});\n\nexport type InputGroupButtonProps = Omit<ButtonProps, \"size\"> & VariantProps<typeof inputGroupButtonVariants>;\n\nfunction InputGroupButton({\n className,\n type = \"button\",\n variant = \"ghost\",\n size = \"xs\",\n ...props\n}: InputGroupButtonProps) {\n return (\n <Button\n type={type}\n data-size={size}\n variant={variant}\n className={cn(inputGroupButtonVariants({ size }), className)}\n {...props}\n />\n );\n}\n\nexport type InputGroupTextProps = React.ComponentProps<\"span\">;\n\nfunction InputGroupText({ className, ...props }: InputGroupTextProps) {\n return (\n <span\n className={cn(\n \"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type InputGroupInputProps = React.ComponentProps<\"input\">;\n\nfunction InputGroupInput({ className, ...props }: InputGroupInputProps) {\n return (\n <Input\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type InputGroupTextareaProps = React.ComponentProps<\"textarea\">;\n\nfunction InputGroupTextarea({ className, ...props }: InputGroupTextareaProps) {\n return (\n <Textarea\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n InputGroupText,\n InputGroupTextarea,\n inputGroupAddonVariants,\n inputGroupButtonVariants\n};\n"],"mappings":";AAuBI;AArBJ,SAAS,WAA8B;AAEvC,SAAS,UAAU;AACnB,SAAS,cAAgC;AACzC,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAczB,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,QAGA;AAAA;AAAA,QAGA;AAAA,QAEA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,MAAM,0BAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,OAAO;AAAA,QACL,gBAAgB;AAAA,QAChB,cAAc;AAAA,QACd,eACE;AAAA,QACF,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,OAAO;AAAA,IACT;AAAA,EACF;AACF;AAWA,SAAS,gBAAgB,EAAE,WAAW,QAAQ,gBAAgB,GAAG,MAAM,GAAyB;AAC9F;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAU;AAAA,QACV,cAAY;AAAA,QACZ,WAAW,GAAG,wBAAwB,EAAE,MAAM,CAAC,GAAG,SAAS;AAAA,QAC3D,SAAS,CAAC,UAAU;AAClB,cAAK,MAAM,OAAuB,QAAQ,QAAQ,GAAG;AACnD;AAAA,UACF;AACA,gBAAM,cAAc,eAAe,cAAc,OAAO,GAAG,MAAM;AAAA,QACnE;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA;AAEJ;AAEA,MAAM,2BAA2B,IAAI,+CAA+C;AAAA,EAClF,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,IAAI;AAAA,MACJ,WAAW;AAAA,MACX,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,MAAM;AAAA,EACR;AACF,CAAC;AAID,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA,OAAO;AAAA,EACP,UAAU;AAAA,EACV,OAAO;AAAA,EACP,GAAG;AACL,GAA0B;AACxB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,aAAW;AAAA,MACX;AAAA,MACA,WAAW,GAAG,yBAAyB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MAC1D,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,mBAAmB,EAAE,WAAW,GAAG,MAAM,GAA4B;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -27,7 +27,7 @@ function InputOTPSlot({ index, className, ...props }) {
27
27
  "data-slot": "input-otp-slot",
28
28
  "data-active": slot?.isActive,
29
29
  className: cn(
30
- "data-[active=true]:border-primary-ink data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink data-[active=true]:aria-invalid:border-destructive-ink border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-colors duration-150 ease-out outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
30
+ "data-[active=true]:border-primary-ink data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink data-[active=true]:aria-invalid:border-destructive-ink dark:bg-input-fill border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-colors duration-150 ease-out outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
31
31
  className
32
32
  ),
33
33
  ...props,