@comitor/ui 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +12 -8
  2. package/dist/components/composite/avatar-group.d.ts +13 -1
  3. package/dist/components/composite/avatar-group.js +19 -4
  4. package/dist/components/composite/avatar-group.js.map +1 -1
  5. package/dist/components/composite/combobox.js +169 -146
  6. package/dist/components/composite/combobox.js.map +1 -1
  7. package/dist/components/composite/data-table.d.ts +24 -1
  8. package/dist/components/composite/data-table.js +7 -1
  9. package/dist/components/composite/data-table.js.map +1 -1
  10. package/dist/components/composite/date-picker.js +46 -17
  11. package/dist/components/composite/date-picker.js.map +1 -1
  12. package/dist/components/composite/form-section.js +1 -1
  13. package/dist/components/composite/form-section.js.map +1 -1
  14. package/dist/components/composite/route-tabs.d.ts +75 -0
  15. package/dist/components/composite/route-tabs.js +50 -0
  16. package/dist/components/composite/route-tabs.js.map +1 -0
  17. package/dist/components/composite/stat-card.js +14 -10
  18. package/dist/components/composite/stat-card.js.map +1 -1
  19. package/dist/components/ui/calendar.d.ts +29 -24
  20. package/dist/components/ui/calendar.js +1 -1
  21. package/dist/components/ui/calendar.js.map +1 -1
  22. package/dist/components/ui/checkbox.js +2 -2
  23. package/dist/components/ui/checkbox.js.map +1 -1
  24. package/dist/components/ui/command.js +1 -1
  25. package/dist/components/ui/command.js.map +1 -1
  26. package/dist/components/ui/context-menu.js +4 -4
  27. package/dist/components/ui/context-menu.js.map +1 -1
  28. package/dist/components/ui/dropdown-menu.js +4 -4
  29. package/dist/components/ui/dropdown-menu.js.map +1 -1
  30. package/dist/components/ui/empty.js +1 -1
  31. package/dist/components/ui/empty.js.map +1 -1
  32. package/dist/components/ui/field.js +1 -1
  33. package/dist/components/ui/field.js.map +1 -1
  34. package/dist/components/ui/item.js +1 -1
  35. package/dist/components/ui/item.js.map +1 -1
  36. package/dist/components/ui/menubar.js +4 -4
  37. package/dist/components/ui/menubar.js.map +1 -1
  38. package/dist/components/ui/radio-group.js +1 -1
  39. package/dist/components/ui/radio-group.js.map +1 -1
  40. package/dist/components/ui/select.d.ts +35 -12
  41. package/dist/components/ui/select.js +1 -1
  42. package/dist/components/ui/select.js.map +1 -1
  43. package/dist/components/ui/switch.js +2 -2
  44. package/dist/components/ui/switch.js.map +1 -1
  45. package/dist/components/ui/table.js +2 -2
  46. package/dist/components/ui/table.js.map +1 -1
  47. package/dist/components/ui/tabs-variants.d.ts +21 -0
  48. package/dist/components/ui/tabs-variants.js +15 -0
  49. package/dist/components/ui/tabs-variants.js.map +1 -0
  50. package/dist/components/ui/tabs.d.ts +2 -10
  51. package/dist/components/ui/tabs.js +3 -14
  52. package/dist/components/ui/tabs.js.map +1 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/shell/app-header.js +47 -41
  57. package/dist/shell/app-header.js.map +1 -1
  58. package/dist/shell/mobile-menu.d.ts +6 -1
  59. package/dist/shell/mobile-menu.js +5 -5
  60. package/dist/shell/mobile-menu.js.map +1 -1
  61. package/dist/shell/notifications-button.js +31 -9
  62. package/dist/shell/notifications-button.js.map +1 -1
  63. package/dist/shell/sidebar.js +1 -1
  64. package/dist/shell/sidebar.js.map +1 -1
  65. package/dist/shell/types.d.ts +13 -0
  66. package/dist/shell/types.js +1 -0
  67. package/dist/shell/types.js.map +1 -1
  68. package/dist/shell/user-menu.d.ts +14 -2
  69. package/dist/shell/user-menu.js +3 -3
  70. package/dist/shell/user-menu.js.map +1 -1
  71. package/dist/shell/workspace-switcher.js +48 -34
  72. package/dist/shell/workspace-switcher.js.map +1 -1
  73. package/package.json +1 -1
  74. package/styles.css +95 -16
package/README.md CHANGED
@@ -553,6 +553,7 @@ Giá trị thật của package: pattern lặp đi lặp lại ở mọi app, gh
553
553
  | `TablePagination` | Phân trang đi kèm bảng. Chuỗi override qua prop `labels` — `TablePaginationLabels` / `DEFAULT_TABLE_PAGINATION_LABELS` (khoá có số/đơn vị chèn vào là **hàm**, vd `range(start, end, total, unitLabel)`) |
554
554
  | `SearchFilterBar`, `FilterChips` | Thanh tìm + lọc + đếm kết quả. `SearchFilterBar` override chuỗi qua `labels` — `SearchFilterBarLabels` / `DEFAULT_SEARCH_FILTER_BAR_LABELS`; `FilterChips` nhận nhãn thẳng trong dữ liệu chip nên không có `labels` |
555
555
  | `StatCard`, `StatusPill`, `EmptyState` | Số liệu có delta, nhãn trạng thái, trạng thái rỗng |
556
+ | `RouteTabs` | Tab mà mỗi mục là một **route thật** — `<nav>` chứa link, `aria-current="page"`. Dùng chung bảng lớp với `Tabs` nên nhìn không khác; đừng dùng `Tabs` cho việc này (nó dựng `role="tablist"` + `aria-controls` trỏ tới tabpanel cùng trang, mà ở đây không có). ⚠ Trên Next phải truyền `LinkComponent={Link}` |
556
557
  | `FormSection`, `FormField` | Bố cục form 12 cột; uỷ quyền label/mô tả/lỗi xuống `Field` |
557
558
  | `Combobox`, `MultiCombobox` | Chọn có tìm kiếm, **khớp không dấu tiếng Việt**. `aria-invalid` đổi trigger sang **viền đỏ** (`--destructive-ink`), cùng tín hiệu với `Input`; nhận cả `boolean` lẫn `"true"`/`"false"` để dùng thẳng `aria-invalid={!!errors.x}` |
558
559
  | `DatePicker`, `DateRangePicker` | Chọn ngày/khoảng ngày, locale `vi`, preset dựng sẵn |
@@ -671,7 +672,7 @@ Bảng mặc định **cố ý có những cặp dưới ngưỡng** — đó l
671
672
  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
 
673
674
  ```
674
- viền control (--input) trên nền trang 1,16:1 (1.4.11 đòi 3:1)
675
+ viền checkbox/radio (--choice-edge) 2,99:1 (1.4.11 đòi 3:1)
675
676
  rãnh Switch BẬT, gạch active Tabs (gold) 1,86:1 (1.4.11 đòi 3:1)
676
677
  --muted-foreground trên --muted 3,58:1 (1.4.3 đòi 4,5:1)
677
678
  chữ trắng trên nền --destructive 4,38:1 (1.4.3 đòi 4,5:1)
@@ -758,8 +759,8 @@ Brand cũng có bậc `-ink`, nhưng **giá trị của nó phụ thuộc bảng
758
759
  **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
760
  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
761
  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
762
+ ⚠ `--control-on` KHÔNG còn trỏ vào `--primary-ink`: ở bảng tương phản cao chúng tách ra (`#B48700`
763
+ so với `#7A5800`) vì `--primary-ink` còn phải làm CHỮ. Component vẫn viết `bg-control-on`. `--ring` **cố ý vẫn là** `--primary` ở cả hai bảng: vòng focus thuộc tiêu
763
764
  chí AAA của WCAG 2.2, ngoài mục tiêu của gói.
764
765
 
765
766
  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
@@ -771,8 +772,8 @@ Các lỗi hay gặp nhất, và cách sửa. **Số đo trong bảng là của
771
772
  | `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) |
772
773
  | `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
774
  | `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`) |
775
+ | `text-ink` làm dấu check của `Checkbox` | Khoá cứng một bảng: `--control-check` là thứ đi CẶP với `--control-on`, đổi bảng màu là nó tự theo | `text-control-check` |
776
+ | `border-border` làm viền của control | `--border` là ranh giới **TRANG TRÍ** và cố ý ở dưới 3:1 tại MỌI bảng màu; `--control-edge`/`--choice-edge` mới là thứ chịu ngưỡng | `border-input` (= `--control-edge`), `border-choice-edge` cho Checkbox/Radio |
776
777
  | `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` |
777
778
  | `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` |
778
779
  | `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) |
@@ -783,7 +784,7 @@ Các lỗi hay gặp nhất, và cách sửa. **Số đo trong bảng là của
783
784
  >
784
785
  > ⚠ **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
786
  > 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.
787
+ > thì `--primary-ink` điều khiển viền focus / gạch Tabs, còn control BẬT đi theo `--control-on`.
787
788
  >
788
789
  > ⚠ **Đổi `--teal` / `--green` / `--red` thì phải tính lại bậc `-ink` tương ứng** — chúng là hex đã đóng
789
790
  > 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
@@ -803,12 +804,15 @@ Bản mirror TS đi theo đúng hợp đồng này: `accent` (nền) ⇄ `accent
803
804
  | 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 |
804
805
  | 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ụ |
805
806
  | 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 |
807
+ | Ranh giới **ô nhập** | `control-edge`, `control-edge-strong`, `input`, `input-fill` | Input/Textarea/Select/Combobox/OTP, Button `outline`, Badge `outline`, rãnh Slider. `--control-edge` trỏ vào `--input`. Bảng mặc định light giữ đúng bậc của comitor-ds (`#EDEEF1`, 1,16:1) — ô nhập còn được nhận ra bằng kích thước, nhãn và placeholder. Dark thì **không** giữ được: `#1E2128` chỉ 1,10:1 trên `--card` nên đổi thành `#434A5A` (2,00:1). Bảng tương phản cao: `#7E8290` (3,83:1 / 4,64:1). `--control-edge-strong` = hover, `ash-300` light / `ash-500` dark đúng cặp bản duyệt |
808
+ | Ranh giới **checkbox/radio** | `choice-edge`, `choice-edge-strong` | Tách riêng vì ở đây viền **là toàn bộ control** khi chưa chọn — không nhãn trong, không placeholder, không nền — nên nó chịu ngưỡng 1.4.11 còn viền ô nhập thì không. Bảng mặc định `#959597` (2,99:1) / `#5B6479` (3,00:1 trên `--card`); bảng tương phản cao trỏ thẳng về `--input`. Hover **không** dùng được `--control-edge-strong` (`ash-300` = 1,62:1, nhạt hơn cả lúc nghỉ) nên `--choice-edge-strong` là `ash-500` |
809
+ | Control BẬT / ĐÃ CHỌN | `control-on`, `control-knob`, `control-check`, `switch-track`, `slider-track` | Rãnh `Switch` BẬT, ô `Checkbox` đã chọn, chấm `Radio`, đoạn `Slider`, ngày đang chọn của `Calendar` — tất cả dùng **cặp** `bg-control-on` + `text-control-check`; núm Switch là `bg-control-knob` (trắng ở cả hai bảng). Bảng mặc định `= --primary` (gold `#E8B824`); bảng tương phản cao `#B48700` — **vẫn là gold, chỉ đậm hơn**, đạt 3,28:1 ⇄ nền trang và 3,06:1 trên `--muted`. ⚠ KHÔNG dùng `gold-500 #7A5800`: đủ tương phản nhưng mắt đọc ra nâu-ô-liu, tức bảng màu thứ hai đổi luôn màu nhận diện. ⚠ Cũng KHÔNG kéo `--primary-ink` sáng theo — nó còn phải làm CHỮ (nhãn trạng thái, link) nên ở lại `gold-500`; đó chính là lý do `--control-on` và `--primary-ink` là hai token, dù ở mọi bảng khác chúng trùng giá trị |
810
+ | Ranh giới **trang trí** | `border`, `sidebar-border` | Khung Card, đường kẻ bảng, vạch ngăn menu. **Không chịu ngưỡng nào** — nó chỉ tách khối cho mắt dễ quét. Bảng mặc định `#EDEEF1` / `#1E2128` (1,16:1 / 1,10:1) đúng comitor-ds; bảng tương phản cao nâng lên `#A4A4A6` / `#50586B` (**2,49:1** ở cả hai theme) — thấy rõ nhưng **cố ý thấp hơn viền control một bậc**, vì kéo nó lên ngang `--input` là mất phân cấp "thao tác được ⇄ chỉ là đường kẻ", và làm hỏng luôn 4 ca của `test:cong-a11y` vốn dựa vào việc token này ở dưới 3:1 để bắt lỗi "vẽ hình control bằng token trang trí" |
808
811
  | 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 |
809
812
  | 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) |
810
813
  | 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 |
811
814
  | 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) |
815
+ | Hàng bảng | `table-row-stripe`, `table-row-hover` | Sọc xen kẽ và nền lúc rê chuột của `<Table striped>` / `<DataTable striped>`. **Đục, bắt buộc** — ô cột GHIM lấy nền bằng `bg-inherit` từ hàng, hàng còn alpha là ô ghim trong mờ và chữ của các cột đang cuộn lộ xuyên qua. Giá trị = `--muted` phủ 30% / 50% lên `--card`, đã hợp thành sẵn (`#FDFDFD` / `#FBFBFC` light, `#181B20` / `#1A1D23` dark) — đổi `--muted` hay `--card` thì tính lại |
812
816
  | Khung | `--sidebar-width`, `--sidebar-width-icon`, `--header-height` | Có utility `w-sidebar`, `w-sidebar-icon`, `h-header` |
813
817
  | Đổ bóng | `--elevation-surface`, `--elevation-control`, `--elevation-card`, `--elevation-overlay` | 4 bậc độ cao, sinh utility `shadow-surface` / `shadow-control` / `shadow-card` / `shadow-overlay`. Đổi theo theme (bản dark dùng đen thuần, alpha cao hơn) |
814
818
  | Khác | `--radius 0.375rem` | Bo góc 6px là chữ ký thị giác |
@@ -18,6 +18,18 @@ interface AvatarGroupProps extends Omit<ComponentProps<"div">, "children"> {
18
18
  onOverflowClick?: () => void;
19
19
  /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định "Thành viên") */
20
20
  label?: string;
21
+ /**
22
+ * BỀ MẶT mà nhóm đang đứng lên — cho cả lớp nền đục dưới mỗi avatar lẫn màu vòng ngăn cách.
23
+ *
24
+ * Vì sao phải khai: tone avatar là các tint CÓ ALPHA (`bg-teal/15`, `bg-yellow/25`…), mà nhóm
25
+ * thì cố tình cho chúng chồng lên nhau `-space-x-2`. Không có lớp đục ở dưới thì tint của avatar
26
+ * trên hợp thành với MÀU CỦA AVATAR DƯỚI chứ không phải với nền trang — ba khuôn mặt lẫn vào
27
+ * nhau đúng ở chỗ chồng. Đây cũng chính là bề mặt mà `getAvatarToneColors` giả định khi tính hex
28
+ * đục cho canvas/email (`tintOver(…, p.card, …)`), nên mặc định là `--card`.
29
+ *
30
+ * Đặt nhóm lên bề mặt khác thì khai lại cả hai class, vd `"bg-sidebar ring-sidebar"`.
31
+ */
32
+ surfaceClassName?: string;
21
33
  }
22
34
  /**
23
35
  * Chồng avatar + ô "+k" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).
@@ -28,6 +40,6 @@ interface AvatarGroupProps extends Omit<ComponentProps<"div">, "children"> {
28
40
  * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình
29
41
  * — xem chú thích ở trigger bên dưới.
30
42
  */
31
- declare function AvatarGroup({ items, max, size, onOverflowClick, label, className, ...props }: AvatarGroupProps): React.JSX.Element;
43
+ declare function AvatarGroup({ items, max, size, onOverflowClick, label, surfaceClassName, className, ...props }: AvatarGroupProps): React.JSX.Element;
32
44
 
33
45
  export { AvatarGroup, type AvatarGroupItem, type AvatarGroupProps };
@@ -3,6 +3,13 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { cn } from "../../lib/cn.js";
4
4
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip.js";
5
5
  import { LetterAvatar } from "./letter-avatar.js";
6
+ const overlapClasses = {
7
+ xs: "-space-x-1",
8
+ sm: "-space-x-1.5",
9
+ md: "-space-x-2",
10
+ lg: "-space-x-2.5",
11
+ xl: "-space-x-3"
12
+ };
6
13
  const overflowSizeClasses = {
7
14
  xs: "size-6 text-[10px]",
8
15
  sm: "size-7 text-xs",
@@ -16,6 +23,7 @@ function AvatarGroup({
16
23
  size = "sm",
17
24
  onOverflowClick,
18
25
  label = "Th\xE0nh vi\xEAn",
26
+ surfaceClassName = "bg-card ring-card",
19
27
  className,
20
28
  ...props
21
29
  }) {
@@ -27,7 +35,7 @@ function AvatarGroup({
27
35
  "data-slot": "avatar-group",
28
36
  role: "group",
29
37
  "aria-label": label,
30
- className: cn("flex items-center -space-x-2", className),
38
+ className: cn("flex items-center", overlapClasses[size], className),
31
39
  ...props,
32
40
  children: [
33
41
  visible.map((item) => /* @__PURE__ */ jsxs(Tooltip, { children: [
@@ -35,7 +43,10 @@ function AvatarGroup({
35
43
  "button",
36
44
  {
37
45
  type: "button",
38
- className: "rounded-full ring-2 ring-background transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground",
46
+ className: cn(
47
+ "rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground",
48
+ surfaceClassName
49
+ ),
39
50
  children: /* @__PURE__ */ jsx(LetterAvatar, { name: item.name, src: item.src, size, title: void 0 })
40
51
  }
41
52
  ) }),
@@ -50,7 +61,10 @@ function AvatarGroup({
50
61
  type: "button",
51
62
  onClick: onOverflowClick,
52
63
  className: cn(
53
- "z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background transition-colors hover:bg-accent hover:text-accent-foreground",
64
+ // `surfaceClassName` đứng TRƯỚC để `bg-muted` ngay sau đè lại phần nền của nó:
65
+ // ô "+k" vốn đã đục, ở đây chỉ mượn màu VÒNG của bề mặt.
66
+ surfaceClassName,
67
+ "z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 transition-colors hover:bg-accent hover:text-accent-foreground",
54
68
  overflowSizeClasses[size]
55
69
  ),
56
70
  children: [
@@ -67,7 +81,8 @@ function AvatarGroup({
67
81
  "span",
68
82
  {
69
83
  className: cn(
70
- "z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background",
84
+ surfaceClassName,
85
+ "z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2",
71
86
  overflowSizeClasses[size]
72
87
  ),
73
88
  children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/avatar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip.js\";\nimport { LetterAvatar, type LetterAvatarSize } from \"./letter-avatar.js\";\n\nexport interface AvatarGroupItem {\n id: string;\n name: string;\n src?: string | null;\n /** Dòng phụ trong tooltip (vd email, vai trò) */\n caption?: string;\n}\n\nexport interface AvatarGroupProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n items: AvatarGroupItem[];\n /** Số avatar hiện tối đa; phần dư gộp vào ô \"+k\" (mặc định 4) */\n max?: number;\n size?: LetterAvatarSize;\n /** Bấm vào ô \"+k\" — vd mở danh sách đầy đủ */\n onOverflowClick?: () => void;\n /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định \"Thành viên\") */\n label?: string;\n}\n\nconst overflowSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-7 text-xs\",\n md: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\"\n};\n\n/**\n * Chồng avatar + ô \"+k\" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).\n *\n * Tooltip hiện tên đầy đủ vì avatar chồng nhau thì initials bị che một phần; không có tooltip\n * thì nhóm avatar chỉ là trang trí, người dùng không biết ai đang ở trong việc.\n *\n * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình\n * — xem chú thích ở trigger bên dưới.\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"sm\",\n onOverflowClick,\n label = \"Thành viên\",\n className,\n ...props\n}: AvatarGroupProps) {\n const visible = items.slice(0, Math.max(0, max));\n const overflow = items.length - visible.length;\n\n return (\n <TooltipProvider delayDuration={200}>\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center -space-x-2\", className)}\n {...props}\n >\n {visible.map((item) => (\n <Tooltip key={item.id}>\n <TooltipTrigger asChild>\n {/* Trigger phải FOCUSABLE: `<span>` thường chỉ nhận sự kiện chuột nên tooltip\n (và cả tên thành viên) biến mất với người dùng bàn phím. Nút không có\n onClick là chủ ý — nó chỉ mở tooltip; tên accessible lấy từ `sr-only`\n bên trong LetterAvatar nên không cần aria-label lặp lại. */}\n {/* Viền focus dùng `foreground` chứ không `ring`: `--ring` là gold — #E8B824 chỉ\n 1,86:1 trên nền trang ở light, dưới 3:1 mà WCAG 1.4.11/2.4.11 đòi cho chỉ báo\n focus (ở dark #F5D060 mới đạt 12,67:1, tức hỏng đúng một nửa số người dùng).\n `outline-foreground` đạt 18,91:1 (light) / 17,66:1 (dark) — cùng lối với viền\n focus của dòng trong `DataTable`. */}\n <button\n type=\"button\"\n className=\"rounded-full ring-2 ring-background transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\"\n >\n <LetterAvatar name={item.name} src={item.src} size={size} title={undefined} />\n </button>\n </TooltipTrigger>\n <TooltipContent>\n <span className=\"font-medium\">{item.name}</span>\n {/* `opacity-80` ở đây ĐẠT chuẩn và đã đo, không phải chỗ sót: Tooltip là\n `bg-foreground text-background`, nên chữ mờ 80% ra 12,15:1 (light) / 9,85:1\n (dark) — trên xa ngưỡng 4,5:1. Tooltip cũng không tương tác được. */}\n {item.caption && <span className=\"block text-xs opacity-80\">{item.caption}</span>}\n </TooltipContent>\n </Tooltip>\n ))}\n\n {overflow > 0 &&\n (onOverflowClick ? (\n <button\n type=\"button\"\n onClick={onOverflowClick}\n className={cn(\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background transition-colors hover:bg-accent hover:text-accent-foreground\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n <span className=\"sr-only\">Xem tất cả {items.length} thành viên</span>\n </button>\n ) : (\n <span\n className={cn(\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 ring-background\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n </span>\n ))}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";AAgFgB,cAGJ,YAHI;AA7EhB,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,iBAAiB,sBAAsB;AACzE,SAAS,oBAA2C;AAqBpD,MAAM,sBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AAC/C,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,SACE,oBAAC,mBAAgB,eAAe,KAC9B;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,gCAAgC,SAAS;AAAA,MACtD,GAAG;AAAA,MAEH;AAAA,gBAAQ,IAAI,CAAC,SACZ,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MAUrB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAU;AAAA,cAEV,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,OAAO,QAAW;AAAA;AAAA,UAC9E,GACF;AAAA,UACA,qBAAC,kBACC;AAAA,gCAAC,UAAK,WAAU,eAAe,eAAK,MAAK;AAAA,YAIxC,KAAK,WAAW,oBAAC,UAAK,WAAU,4BAA4B,eAAK,SAAQ;AAAA,aAC5E;AAAA,aAxBY,KAAK,EAyBnB,CACD;AAAA,QAEA,WAAW,MACT,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAW;AAAA,cACT;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA,cACF,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,gBAAY,MAAM;AAAA,gBAAO;AAAA,iBAAW;AAAA;AAAA;AAAA,QAChE,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA;AAAA;AAAA,QACJ;AAAA;AAAA;AAAA,EAEN,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/avatar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip.js\";\nimport { LetterAvatar, type LetterAvatarSize } from \"./letter-avatar.js\";\n\nexport interface AvatarGroupItem {\n id: string;\n name: string;\n src?: string | null;\n /** Dòng phụ trong tooltip (vd email, vai trò) */\n caption?: string;\n}\n\nexport interface AvatarGroupProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n items: AvatarGroupItem[];\n /** Số avatar hiện tối đa; phần dư gộp vào ô \"+k\" (mặc định 4) */\n max?: number;\n size?: LetterAvatarSize;\n /** Bấm vào ô \"+k\" — vd mở danh sách đầy đủ */\n onOverflowClick?: () => void;\n /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định \"Thành viên\") */\n label?: string;\n /**\n * BỀ MẶT mà nhóm đang đứng lên — cho cả lớp nền đục dưới mỗi avatar lẫn màu vòng ngăn cách.\n *\n * Vì sao phải khai: tone avatar là các tint CÓ ALPHA (`bg-teal/15`, `bg-yellow/25`…), mà nhóm\n * thì cố tình cho chúng chồng lên nhau `-space-x-2`. Không có lớp đục ở dưới thì tint của avatar\n * trên hợp thành với MÀU CỦA AVATAR DƯỚI chứ không phải với nền trang — ba khuôn mặt lẫn vào\n * nhau đúng ở chỗ chồng. Đây cũng chính là bề mặt mà `getAvatarToneColors` giả định khi tính hex\n * đục cho canvas/email (`tintOver(…, p.card, …)`), nên mặc định là `--card`.\n *\n * Đặt nhóm lên bề mặt khác thì khai lại cả hai class, vd `\"bg-sidebar ring-sidebar\"`.\n */\n surfaceClassName?: string;\n}\n\n/**\n * Độ chồng theo CỠ, không phải một hằng số cho mọi cỡ.\n *\n * Mẫu Avatar stack ở comitor-ds là `-space-x-2` trên avatar 40px — tức chồng **20%**. Áp thẳng\n * `-space-x-2` cho mọi cỡ thì ở `xs` (24px) thành chồng 33%, và cộng thêm vòng 2px nữa là phần bị\n * che ăn vào đúng chữ cái đầu của initials: \"ĐH\" ở 10px bắt đầu cách mép ~5,5px, trong khi mép che\n * đã tới 10px. Giữ tỉ lệ ~20% thì mọi cỡ đọc được mà mật độ vẫn y như bản duyệt.\n *\n * xs 24px → 4px (17%) · sm 28px → 6px (21%) · md 36px → 8px (22%)\n * lg 44px → 10px (23%) · xl 56px → 12px (21%)\n */\nconst overlapClasses: Record<LetterAvatarSize, string> = {\n xs: \"-space-x-1\",\n sm: \"-space-x-1.5\",\n md: \"-space-x-2\",\n lg: \"-space-x-2.5\",\n xl: \"-space-x-3\"\n};\n\nconst overflowSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-7 text-xs\",\n md: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\"\n};\n\n/**\n * Chồng avatar + ô \"+k\" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).\n *\n * Tooltip hiện tên đầy đủ vì avatar chồng nhau thì initials bị che một phần; không có tooltip\n * thì nhóm avatar chỉ là trang trí, người dùng không biết ai đang ở trong việc.\n *\n * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình\n * — xem chú thích ở trigger bên dưới.\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"sm\",\n onOverflowClick,\n label = \"Thành viên\",\n surfaceClassName = \"bg-card ring-card\",\n className,\n ...props\n}: AvatarGroupProps) {\n const visible = items.slice(0, Math.max(0, max));\n const overflow = items.length - visible.length;\n\n return (\n <TooltipProvider delayDuration={200}>\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center\", overlapClasses[size], className)}\n {...props}\n >\n {visible.map((item) => (\n <Tooltip key={item.id}>\n <TooltipTrigger asChild>\n {/* Trigger phải FOCUSABLE: `<span>` thường chỉ nhận sự kiện chuột nên tooltip\n (và cả tên thành viên) biến mất với người dùng bàn phím. Nút không có\n onClick là chủ ý — nó chỉ mở tooltip; tên accessible lấy từ `sr-only`\n bên trong LetterAvatar nên không cần aria-label lặp lại. */}\n {/* Viền focus dùng `foreground` chứ không `ring`: `--ring` là gold — #E8B824 chỉ\n 1,86:1 trên nền trang ở light, dưới 3:1 mà WCAG 1.4.11/2.4.11 đòi cho chỉ báo\n focus (ở dark #F5D060 mới đạt 12,67:1, tức hỏng đúng một nửa số người dùng).\n `outline-foreground` đạt 18,91:1 (light) / 17,66:1 (dark) — cùng lối với viền\n focus của dòng trong `DataTable`. */}\n {/* Nền đục nằm DƯỚI tint có alpha của avatar — xem `surfaceClassName`. Vòng\n `ring-2` cùng màu bề mặt chỉ tách hai avatar ra, nó KHÔNG che được phần tint\n chồng lên nhau bên trong vòng. */}\n <button\n type=\"button\"\n className={cn(\n \"rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n surfaceClassName\n )}\n >\n <LetterAvatar name={item.name} src={item.src} size={size} title={undefined} />\n </button>\n </TooltipTrigger>\n <TooltipContent>\n <span className=\"font-medium\">{item.name}</span>\n {/* `opacity-80` ở đây ĐẠT chuẩn và đã đo, không phải chỗ sót: Tooltip là\n `bg-foreground text-background`, nên chữ mờ 80% ra 12,15:1 (light) / 9,85:1\n (dark) — trên xa ngưỡng 4,5:1. Tooltip cũng không tương tác được. */}\n {item.caption && <span className=\"block text-xs opacity-80\">{item.caption}</span>}\n </TooltipContent>\n </Tooltip>\n ))}\n\n {overflow > 0 &&\n (onOverflowClick ? (\n <button\n type=\"button\"\n onClick={onOverflowClick}\n className={cn(\n // `surfaceClassName` đứng TRƯỚC để `bg-muted` ngay sau đè lại phần nền của nó:\n // ô \"+k\" vốn đã đục, ở đây chỉ mượn màu VÒNG của bề mặt.\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 transition-colors hover:bg-accent hover:text-accent-foreground\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n <span className=\"sr-only\">Xem tất cả {items.length} thành viên</span>\n </button>\n ) : (\n <span\n className={cn(\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n </span>\n ))}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";AAsHgB,cAGJ,YAHI;AAnHhB,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,iBAAiB,sBAAsB;AACzE,SAAS,oBAA2C;AA4CpD,MAAM,iBAAmD;AAAA,EACvD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,sBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AAC/C,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,SACE,oBAAC,mBAAgB,eAAe,KAC9B;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,eAAe,IAAI,GAAG,SAAS;AAAA,MACjE,GAAG;AAAA,MAEH;AAAA,gBAAQ,IAAI,CAAC,SACZ,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MAarB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,OAAO,QAAW;AAAA;AAAA,UAC9E,GACF;AAAA,UACA,qBAAC,kBACC;AAAA,gCAAC,UAAK,WAAU,eAAe,eAAK,MAAK;AAAA,YAIxC,KAAK,WAAW,oBAAC,UAAK,WAAU,4BAA4B,eAAK,SAAQ;AAAA,aAC5E;AAAA,aA9BY,KAAK,EA+BnB,CACD;AAAA,QAEA,WAAW,MACT,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA,cACF,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,gBAAY,MAAM;AAAA,gBAAO;AAAA,iBAAW;AAAA;AAAA;AAAA,QAChE,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA;AAAA;AAAA,QACJ;AAAA;AAAA;AAAA,EAEN,GACF;AAEJ;","names":[]}
@@ -44,6 +44,45 @@ function OptionRow({ option, selected }) {
44
44
  ] })
45
45
  ] });
46
46
  }
47
+ function ComboboxAdornments({
48
+ showClear,
49
+ disabled,
50
+ clearLabel,
51
+ onClear
52
+ }) {
53
+ return /* @__PURE__ */ jsxs(
54
+ "div",
55
+ {
56
+ className: cn(
57
+ /* `border border-transparent pr-3` chứ không phải `right-3`: hàng này là ANH EM của
58
+ trigger nên nó đo từ mép BORDER, trong khi chevron của `Select` nằm trong trigger nên
59
+ đo từ mép CONTENT — lệch đúng 1px bề dày viền. Mượn lại mô hình hộp của trigger (viền
60
+ 1px trong suốt + lề 12px) là hai bên rơi vào cùng một mốc, không phải bịa số lẻ. */
61
+ "pointer-events-none absolute inset-y-0 right-0 flex items-center border border-transparent pr-3",
62
+ disabled && "opacity-50"
63
+ ),
64
+ children: [
65
+ showClear && /* @__PURE__ */ jsxs(
66
+ Button,
67
+ {
68
+ type: "button",
69
+ variant: "ghost",
70
+ size: "icon-sm",
71
+ className: "pointer-events-auto size-6 text-muted-foreground hover:text-foreground",
72
+ onClick: onClear,
73
+ children: [
74
+ /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" }),
75
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: clearLabel })
76
+ ]
77
+ }
78
+ ),
79
+ /* @__PURE__ */ jsx(ChevronsUpDown, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" })
80
+ ]
81
+ }
82
+ );
83
+ }
84
+ const TRIGGER_PADDING = "px-3 pr-9";
85
+ const TRIGGER_PADDING_WITH_CLEAR = "px-3 pr-15";
47
86
  function useComboboxList(options, isAsync) {
48
87
  const groups = useMemo(() => groupOptions(options), [options]);
49
88
  return { groups, shouldFilter: !isAsync };
@@ -97,77 +136,68 @@ function Combobox({
97
136
  onSearchChange?.(next);
98
137
  };
99
138
  const showClear = clearable && Boolean(selected) && !disabled;
100
- return (
101
- /* Nút xóa là ANH EM của trigger, không nằm trong nó: <button> lồng <button> là HTML không
102
- hợp lệ, và nếu hạ xuống <span role="button" tabIndex={-1}> thì người dùng bàn phím vĩnh
103
- viễn không tới được nút xóa. Đặt tuyệt đối chồng lên phần đệm phải của trigger. */
104
- /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
105
- /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
106
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
107
- Button,
108
- {
109
- type: "button",
110
- variant: "outline",
111
- role: "combobox",
112
- "aria-expanded": open,
113
- id,
114
- name,
115
- disabled,
116
- className: cn(
117
- "w-full justify-between gap-2 font-normal",
118
- !selected && "text-muted-foreground",
119
- showClear && "pr-12",
120
- className
121
- ),
122
- ...aria,
123
- "aria-invalid": invalid || void 0,
124
- children: [
125
- /* @__PURE__ */ jsx("span", { className: "truncate", children: selected ? selected.label : placeholder }),
126
- /* @__PURE__ */ jsx(ChevronsUpDown, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" })
127
- ]
128
- }
129
- ) }),
130
- /* @__PURE__ */ jsx(PopoverContent, { align: "start", className: cn("w-(--radix-popover-trigger-width) p-0", contentClassName), children: /* @__PURE__ */ jsx(
131
- ComboboxPanel,
132
- {
133
- options,
134
- isAsync,
135
- loading,
136
- search,
137
- onSearch: handleSearch,
138
- searchPlaceholder,
139
- emptyText,
140
- renderItem: (option) => /* @__PURE__ */ jsx(
141
- CommandItem,
142
- {
143
- value: option.value,
144
- keywords: [option.label, option.description ?? "", ...option.keywords ?? []],
145
- disabled: option.disabled,
146
- onSelect: () => {
147
- onValueChange(option.value === value ? null : option.value);
148
- setOpen(false);
149
- setSearch("");
150
- if (isAsync) onSearchChange?.("");
151
- },
152
- children: /* @__PURE__ */ jsx(OptionRow, { option, selected: option.value === value })
153
- },
154
- option.value
155
- )
156
- }
157
- ) })
158
- ] }),
159
- showClear && /* @__PURE__ */ jsx(
160
- "button",
139
+ return /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
140
+ /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
141
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
142
+ Button,
161
143
  {
162
144
  type: "button",
163
- "aria-label": "X\xF3a l\u1EF1a ch\u1ECDn",
164
- onClick: () => onValueChange(null),
165
- className: "absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground",
166
- children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" })
145
+ variant: "outline",
146
+ role: "combobox",
147
+ "aria-expanded": open,
148
+ id,
149
+ name,
150
+ disabled,
151
+ className: cn(
152
+ "w-full justify-start font-normal",
153
+ showClear ? TRIGGER_PADDING_WITH_CLEAR : TRIGGER_PADDING,
154
+ !selected && "text-muted-foreground",
155
+ className
156
+ ),
157
+ ...aria,
158
+ "aria-invalid": invalid || void 0,
159
+ children: /* @__PURE__ */ jsx("span", { className: "truncate", children: selected ? selected.label : placeholder })
167
160
  }
168
- )
169
- ] })
170
- );
161
+ ) }),
162
+ /* @__PURE__ */ jsx(PopoverContent, { align: "start", className: cn("w-(--radix-popover-trigger-width) p-0", contentClassName), children: /* @__PURE__ */ jsx(
163
+ ComboboxPanel,
164
+ {
165
+ options,
166
+ isAsync,
167
+ loading,
168
+ search,
169
+ onSearch: handleSearch,
170
+ searchPlaceholder,
171
+ emptyText,
172
+ renderItem: (option) => /* @__PURE__ */ jsx(
173
+ CommandItem,
174
+ {
175
+ value: option.value,
176
+ keywords: [option.label, option.description ?? "", ...option.keywords ?? []],
177
+ disabled: option.disabled,
178
+ onSelect: () => {
179
+ onValueChange(option.value === value ? null : option.value);
180
+ setOpen(false);
181
+ setSearch("");
182
+ if (isAsync) onSearchChange?.("");
183
+ },
184
+ children: /* @__PURE__ */ jsx(OptionRow, { option, selected: option.value === value })
185
+ },
186
+ option.value
187
+ )
188
+ }
189
+ ) })
190
+ ] }),
191
+ /* @__PURE__ */ jsx(
192
+ ComboboxAdornments,
193
+ {
194
+ showClear,
195
+ disabled,
196
+ clearLabel: "X\xF3a l\u1EF1a ch\u1ECDn",
197
+ onClear: () => onValueChange(null)
198
+ }
199
+ )
200
+ ] });
171
201
  }
172
202
  function MultiCombobox({
173
203
  options,
@@ -209,87 +239,80 @@ function MultiCombobox({
209
239
  onSearchChange?.(next);
210
240
  };
211
241
  const showClear = clearable && value.length > 0 && !disabled;
212
- return (
213
- // Cùng lý do như Combobox đơn: nút xóa là anh em của trigger để bấm được bằng bàn phím.
214
- /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
215
- /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
216
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
217
- Button,
218
- {
219
- type: "button",
220
- variant: "outline",
221
- role: "combobox",
222
- "aria-expanded": open,
223
- id,
224
- name,
225
- disabled,
226
- className: cn(
227
- "h-auto min-h-9 w-full justify-between gap-2 py-1.5 font-normal",
228
- value.length === 0 && "text-muted-foreground",
229
- showClear && "pr-12",
230
- className
231
- ),
232
- ...aria,
233
- "aria-invalid": invalid || void 0,
234
- children: [
235
- /* @__PURE__ */ jsx("span", { className: "flex min-w-0 flex-wrap items-center gap-1", children: value.length === 0 ? placeholder : /* @__PURE__ */ jsxs(Fragment, { children: [
236
- shown.map((option) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "max-w-[10rem]", children: /* @__PURE__ */ jsx("span", { className: "truncate", children: option.label }) }, option.value)),
237
- overflow > 0 && /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
238
- "+",
239
- overflow
240
- ] })
241
- ] }) }),
242
- /* @__PURE__ */ jsx(ChevronsUpDown, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" })
243
- ]
244
- }
245
- ) }),
246
- /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: cn("w-(--radix-popover-trigger-width) p-0", contentClassName), children: [
247
- /* @__PURE__ */ jsx(
248
- ComboboxPanel,
249
- {
250
- options,
251
- isAsync,
252
- loading,
253
- search,
254
- onSearch: handleSearch,
255
- searchPlaceholder,
256
- emptyText,
257
- renderItem: (option) => {
258
- const isSelected = value.includes(option.value);
259
- return /* @__PURE__ */ jsx(
260
- CommandItem,
261
- {
262
- value: option.value,
263
- keywords: [option.label, option.description ?? "", ...option.keywords ?? []],
264
- disabled: option.disabled || limitReached && !isSelected,
265
- onSelect: () => toggle(option.value),
266
- children: /* @__PURE__ */ jsx(OptionRow, { option, selected: isSelected })
267
- },
268
- option.value
269
- );
270
- }
271
- }
272
- ),
273
- maxSelected !== void 0 && /* @__PURE__ */ jsxs("p", { className: "border-t border-border px-3 py-2 text-xs text-muted-foreground", children: [
274
- "\u0110\xE3 ch\u1ECDn ",
275
- value.length,
276
- "/",
277
- maxSelected
278
- ] })
279
- ] })
280
- ] }),
281
- showClear && /* @__PURE__ */ jsx(
282
- "button",
242
+ return /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
243
+ /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
244
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
245
+ Button,
283
246
  {
284
247
  type: "button",
285
- "aria-label": "X\xF3a t\u1EA5t c\u1EA3 l\u1EF1a ch\u1ECDn",
286
- onClick: () => onValueChange([]),
287
- className: "absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground",
288
- children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" })
248
+ variant: "outline",
249
+ role: "combobox",
250
+ "aria-expanded": open,
251
+ id,
252
+ name,
253
+ disabled,
254
+ className: cn(
255
+ "h-auto min-h-9 w-full justify-start py-1.5 font-normal",
256
+ showClear ? TRIGGER_PADDING_WITH_CLEAR : TRIGGER_PADDING,
257
+ value.length === 0 && "text-muted-foreground",
258
+ className
259
+ ),
260
+ ...aria,
261
+ "aria-invalid": invalid || void 0,
262
+ children: /* @__PURE__ */ jsx("span", { className: "flex min-w-0 flex-wrap items-center gap-1", children: value.length === 0 ? placeholder : /* @__PURE__ */ jsxs(Fragment, { children: [
263
+ shown.map((option) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "max-w-[10rem]", children: /* @__PURE__ */ jsx("span", { className: "truncate", children: option.label }) }, option.value)),
264
+ overflow > 0 && /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
265
+ "+",
266
+ overflow
267
+ ] })
268
+ ] }) })
289
269
  }
290
- )
291
- ] })
292
- );
270
+ ) }),
271
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: cn("w-(--radix-popover-trigger-width) p-0", contentClassName), children: [
272
+ /* @__PURE__ */ jsx(
273
+ ComboboxPanel,
274
+ {
275
+ options,
276
+ isAsync,
277
+ loading,
278
+ search,
279
+ onSearch: handleSearch,
280
+ searchPlaceholder,
281
+ emptyText,
282
+ renderItem: (option) => {
283
+ const isSelected = value.includes(option.value);
284
+ return /* @__PURE__ */ jsx(
285
+ CommandItem,
286
+ {
287
+ value: option.value,
288
+ keywords: [option.label, option.description ?? "", ...option.keywords ?? []],
289
+ disabled: option.disabled || limitReached && !isSelected,
290
+ onSelect: () => toggle(option.value),
291
+ children: /* @__PURE__ */ jsx(OptionRow, { option, selected: isSelected })
292
+ },
293
+ option.value
294
+ );
295
+ }
296
+ }
297
+ ),
298
+ maxSelected !== void 0 && /* @__PURE__ */ jsxs("p", { className: "border-t border-border px-3 py-2 text-xs text-muted-foreground", children: [
299
+ "\u0110\xE3 ch\u1ECDn ",
300
+ value.length,
301
+ "/",
302
+ maxSelected
303
+ ] })
304
+ ] })
305
+ ] }),
306
+ /* @__PURE__ */ jsx(
307
+ ComboboxAdornments,
308
+ {
309
+ showClear,
310
+ disabled,
311
+ clearLabel: "X\xF3a t\u1EA5t c\u1EA3 l\u1EF1a ch\u1ECDn",
312
+ onClear: () => onValueChange([])
313
+ }
314
+ )
315
+ ] });
293
316
  }
294
317
  export {
295
318
  Combobox,