@comitor/ui 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -10
- package/dist/components/composite/avatar-group.d.ts +13 -1
- package/dist/components/composite/avatar-group.js +19 -4
- package/dist/components/composite/avatar-group.js.map +1 -1
- package/dist/components/composite/combobox.js +169 -146
- package/dist/components/composite/combobox.js.map +1 -1
- package/dist/components/composite/data-table.d.ts +23 -4
- package/dist/components/composite/data-table.js +37 -2
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/date-picker.js +46 -17
- package/dist/components/composite/date-picker.js.map +1 -1
- package/dist/components/composite/stat-card.js +6 -6
- package/dist/components/composite/stat-card.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +9 -1
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/ui/calendar.d.ts +29 -24
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/ui/checkbox.js +2 -2
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/command.js +1 -1
- package/dist/components/ui/command.js.map +1 -1
- package/dist/components/ui/context-menu.js +4 -4
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/components/ui/dropdown-menu.js +4 -4
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/empty.js +1 -1
- package/dist/components/ui/empty.js.map +1 -1
- package/dist/components/ui/field.js +1 -1
- package/dist/components/ui/field.js.map +1 -1
- package/dist/components/ui/item.js +1 -1
- package/dist/components/ui/item.js.map +1 -1
- package/dist/components/ui/menubar.js +4 -4
- package/dist/components/ui/menubar.js.map +1 -1
- package/dist/components/ui/radio-group.js +1 -1
- package/dist/components/ui/radio-group.js.map +1 -1
- package/dist/components/ui/select.d.ts +35 -12
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/select.js.map +1 -1
- package/dist/components/ui/switch.js +2 -2
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/components/ui/table.js +2 -2
- package/dist/components/ui/table.js.map +1 -1
- package/dist/shell/app-header.js +46 -48
- package/dist/shell/app-header.js.map +1 -1
- package/dist/shell/mobile-menu.d.ts +6 -1
- package/dist/shell/mobile-menu.js +5 -5
- package/dist/shell/mobile-menu.js.map +1 -1
- package/dist/shell/notifications-button.js +31 -9
- package/dist/shell/notifications-button.js.map +1 -1
- package/dist/shell/sidebar.js +1 -1
- package/dist/shell/sidebar.js.map +1 -1
- package/dist/shell/types.d.ts +13 -0
- package/dist/shell/types.js +1 -0
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/user-menu.d.ts +14 -2
- package/dist/shell/user-menu.js +3 -3
- package/dist/shell/user-menu.js.map +1 -1
- package/dist/shell/workspace-switcher.js +48 -34
- package/dist/shell/workspace-switcher.js.map +1 -1
- package/package.json +4 -4
- package/styles.css +95 -16
package/README.md
CHANGED
|
@@ -672,7 +672,7 @@ Bảng mặc định **cố ý có những cặp dưới ngưỡng** — đó l
|
|
|
672
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:
|
|
673
673
|
|
|
674
674
|
```
|
|
675
|
-
viền
|
|
675
|
+
viền checkbox/radio (--choice-edge) 2,99:1 (1.4.11 đòi 3:1)
|
|
676
676
|
rãnh Switch BẬT, gạch active Tabs (gold) 1,86:1 (1.4.11 đòi 3:1)
|
|
677
677
|
--muted-foreground trên --muted 3,58:1 (1.4.3 đòi 4,5:1)
|
|
678
678
|
chữ trắng trên nền --destructive 4,38:1 (1.4.3 đòi 4,5:1)
|
|
@@ -759,8 +759,8 @@ Brand cũng có bậc `-ink`, nhưng **giá trị của nó phụ thuộc bảng
|
|
|
759
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**.
|
|
760
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
|
|
761
761
|
cao nó mượn `--gold-500` `#7A5800` (**6,51:1** ✓ light) và trỏ về `--primary` ở dark (**12,67:1** ✓).
|
|
762
|
-
|
|
763
|
-
|
|
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
|
|
764
764
|
chí AAA của WCAG 2.2, ngoài mục tiêu của gói.
|
|
765
765
|
|
|
766
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
|
|
@@ -772,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
|
|
|
772
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) |
|
|
773
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` |
|
|
774
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` |
|
|
775
|
-
| `text-ink` làm dấu check của `Checkbox` |
|
|
776
|
-
| `border-border` làm viền của control | `--border` là ranh giới **TRANG TR
|
|
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 |
|
|
777
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` |
|
|
778
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` |
|
|
779
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) |
|
|
@@ -784,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
|
|
|
784
784
|
>
|
|
785
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
|
|
786
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
|
|
787
|
-
> thì `--primary-ink`
|
|
787
|
+
> thì `--primary-ink` điều khiển viền focus / gạch Tabs, còn control BẬT đi theo `--control-on`.
|
|
788
788
|
>
|
|
789
789
|
> ⚠ **Đổi `--teal` / `--green` / `--red` thì phải tính lại bậc `-ink` tương ứng** — chúng là hex đã đóng
|
|
790
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
|
|
@@ -804,12 +804,15 @@ Bản mirror TS đi theo đúng hợp đồng này: `accent` (nền) ⇄ `accent
|
|
|
804
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 |
|
|
805
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ụ |
|
|
806
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 |
|
|
807
|
-
| Ranh giới
|
|
808
|
-
|
|
|
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:a11y-gate` 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í" |
|
|
809
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 |
|
|
810
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) |
|
|
811
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 |
|
|
812
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 |
|
|
813
816
|
| Khung | `--sidebar-width`, `--sidebar-width-icon`, `--header-height` | Có utility `w-sidebar`, `w-sidebar-icon`, `h-header` |
|
|
814
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) |
|
|
815
818
|
| Khác | `--radius 0.375rem` | Bo góc 6px là chữ ký thị giác |
|
|
@@ -872,14 +875,14 @@ pnpm typecheck # tsc --noEmit
|
|
|
872
875
|
pnpm lint # biome check .
|
|
873
876
|
pnpm lint:fix
|
|
874
877
|
pnpm lint:a11y # hợp đồng màu: vai trò token + tương phản WCAG ở CẢ hai theme
|
|
875
|
-
pnpm test:
|
|
878
|
+
pnpm test:a11y-gate # tự kiểm chính cổng trên: 56 khẳng định, cổng còn bắt đủ không
|
|
876
879
|
pnpm test:nav-active # active matching của sidebar (hàm thuần) — 34 ca
|
|
877
880
|
pnpm verify:directives # ranh giới server/client: directive còn sau build + không hằng thuần kẹt trong file client
|
|
878
881
|
pnpm smoke:entries # nạp cả 5 entry bằng Node ESM thuần, qua đúng `exports` map (chạy SAU build)
|
|
879
882
|
pnpm check:exports # publint --strict + attw
|
|
880
883
|
```
|
|
881
884
|
|
|
882
|
-
`pnpm test:nav-active` chạy `scripts/
|
|
885
|
+
`pnpm test:nav-active` chạy `scripts/check-nav-active.mjs`: Node trần bóc kiểu ngay trên
|
|
883
886
|
`src/shell/utils.ts`, không test runner, không dependency mới. Nó khoá lại đúng lỗi mà
|
|
884
887
|
comitor-starter báo về ở `0.2.0` — mục menu có `?query` trong `href` không bao giờ sáng — cùng
|
|
885
888
|
toàn bộ hành vi active matching cũ, để lần sửa sau không đổi ngầm luật nào.
|
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/*
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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,
|