@comitor/ui 0.9.1 → 1.1.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 (33) hide show
  1. package/README.md +158 -858
  2. package/dist/components/composite/avatar-group.d.ts +18 -2
  3. package/dist/components/composite/avatar-group.js +2 -5
  4. package/dist/components/composite/avatar-group.js.map +1 -1
  5. package/dist/components/composite/combobox.d.ts +31 -2
  6. package/dist/components/composite/combobox.js +12 -9
  7. package/dist/components/composite/combobox.js.map +1 -1
  8. package/dist/components/composite/command-palette-constants.d.ts +35 -0
  9. package/dist/components/composite/command-palette-constants.js +8 -0
  10. package/dist/components/composite/command-palette-constants.js.map +1 -0
  11. package/dist/components/composite/command-palette.d.ts +4 -1
  12. package/dist/components/composite/command-palette.js +5 -2
  13. package/dist/components/composite/command-palette.js.map +1 -1
  14. package/dist/components/composite/confirm-dialog.d.ts +11 -2
  15. package/dist/components/composite/confirm-dialog.js +2 -1
  16. package/dist/components/composite/confirm-dialog.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/shell/contrast-provider.d.ts +10 -2
  21. package/dist/shell/contrast-provider.js +12 -4
  22. package/dist/shell/contrast-provider.js.map +1 -1
  23. package/dist/shell/display-axis-provider.d.ts +30 -4
  24. package/dist/shell/display-axis-provider.js +12 -4
  25. package/dist/shell/display-axis-provider.js.map +1 -1
  26. package/dist/shell/mobile-menu.js +1 -0
  27. package/dist/shell/mobile-menu.js.map +1 -1
  28. package/dist/shell/types.d.ts +8 -1
  29. package/dist/shell/types.js +1 -1
  30. package/dist/shell/types.js.map +1 -1
  31. package/dist/shell/workspace-switcher.js +1 -1
  32. package/dist/shell/workspace-switcher.js.map +1 -1
  33. package/package.json +3 -7
@@ -16,7 +16,23 @@ interface AvatarGroupProps extends Omit<ComponentProps<"div">, "children"> {
16
16
  size?: LetterAvatarSize;
17
17
  /** Bấm vào ô "+k" — vd mở danh sách đầy đủ */
18
18
  onOverflowClick?: () => void;
19
- /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định "Thành viên") */
19
+ /**
20
+ * Nhãn `sr-only` của ô "+k" khi ô đó bấm được — mặc định
21
+ * `(count) => "Xem tất cả " + count + " thành viên"`.
22
+ *
23
+ * Là HÀM chứ không phải chuỗi mẫu vì có số chèn vào giữa câu: trật tự từ quanh con số khác nhau
24
+ * theo ngôn ngữ, nên mọi cách để gói tự ghép chuỗi đều là chỗ chắc chắn dịch sai.
25
+ *
26
+ * Nhận TỔNG số thành viên (`items.length`), không phải phần dư đang bị giấu: mặt nút đã in sẵn
27
+ * "+k" là phần dư rồi, còn câu này nói ra HÀNH ĐỘNG — bấm vào là xem hết cả nhóm.
28
+ *
29
+ * Khác `label` ở tầm phủ: `label` đặt tên cho CẢ NHÓM (`aria-label` của `role="group"`), đọc lên
30
+ * khi con trỏ bước vào nhóm; `overflowLabel` đặt tên cho MỘT nút bên trong, đọc lên khi Tab tới
31
+ * đúng ô "+k". App đa ngôn ngữ phải dịch cả hai — dịch mỗi `label` thì nhóm mang tên tiếng Anh
32
+ * mà nút cuối vẫn xưng tiếng Việt.
33
+ */
34
+ overflowLabel?: (count: number) => string;
35
+ /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định "Thành viên") — xem `overflowLabel` để phân biệt */
20
36
  label?: string;
21
37
  /**
22
38
  * 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.
@@ -40,6 +56,6 @@ interface AvatarGroupProps extends Omit<ComponentProps<"div">, "children"> {
40
56
  * 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
41
57
  * — xem chú thích ở trigger bên dưới.
42
58
  */
43
- declare function AvatarGroup({ items, max, size, onOverflowClick, label, surfaceClassName, className, ...props }: AvatarGroupProps): React.JSX.Element;
59
+ declare function AvatarGroup({ items, max, size, onOverflowClick, overflowLabel, label, surfaceClassName, className, ...props }: AvatarGroupProps): React.JSX.Element;
44
60
 
45
61
  export { AvatarGroup, type AvatarGroupItem, type AvatarGroupProps };
@@ -22,6 +22,7 @@ function AvatarGroup({
22
22
  max = 4,
23
23
  size = "sm",
24
24
  onOverflowClick,
25
+ overflowLabel = (count) => `Xem t\u1EA5t c\u1EA3 ${count} th\xE0nh vi\xEAn`,
25
26
  label = "Th\xE0nh vi\xEAn",
26
27
  surfaceClassName = "bg-card ring-card",
27
28
  className,
@@ -70,11 +71,7 @@ function AvatarGroup({
70
71
  children: [
71
72
  "+",
72
73
  overflow,
73
- /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
74
- "Xem t\u1EA5t c\u1EA3 ",
75
- items.length,
76
- " th\xE0nh vi\xEAn"
77
- ] })
74
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: overflowLabel(items.length) })
78
75
  ]
79
76
  }
80
77
  ) : /* @__PURE__ */ jsxs(
@@ -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 * 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":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/avatar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip.js\";\nimport { LetterAvatar, type LetterAvatarSize } from \"./letter-avatar.js\";\n\nexport interface AvatarGroupItem {\n id: string;\n name: string;\n src?: string | null;\n /** Dòng phụ trong tooltip (vd email, vai trò) */\n caption?: string;\n}\n\nexport interface AvatarGroupProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n items: AvatarGroupItem[];\n /** Số avatar hiện tối đa; phần dư gộp vào ô \"+k\" (mặc định 4) */\n max?: number;\n size?: LetterAvatarSize;\n /** Bấm vào ô \"+k\" — vd mở danh sách đầy đủ */\n onOverflowClick?: () => void;\n /**\n * Nhãn `sr-only` của ô \"+k\" khi ô đó bấm được — mặc định\n * `(count) => \"Xem tất cả \" + count + \" thành viên\"`.\n *\n * Là HÀM chứ không phải chuỗi mẫu vì có số chèn vào giữa câu: trật tự từ quanh con số khác nhau\n * theo ngôn ngữ, nên mọi cách để gói tự ghép chuỗi đều là chỗ chắc chắn dịch sai.\n *\n * Nhận TỔNG số thành viên (`items.length`), không phải phần dư đang bị giấu: mặt nút đã in sẵn\n * \"+k\" là phần dư rồi, còn câu này nói ra HÀNH ĐỘNG — bấm vào là xem hết cả nhóm.\n *\n * Khác `label` ở tầm phủ: `label` đặt tên cho CẢ NHÓM (`aria-label` của `role=\"group\"`), đọc lên\n * khi con trỏ bước vào nhóm; `overflowLabel` đặt tên cho MỘT nút bên trong, đọc lên khi Tab tới\n * đúng ô \"+k\". App đa ngôn ngữ phải dịch cả hai — dịch mỗi `label` thì nhóm mang tên tiếng Anh\n * mà nút cuối vẫn xưng tiếng Việt.\n */\n overflowLabel?: (count: number) => string;\n /** Nhãn của cả nhóm cho trình đọc màn hình (mặc định \"Thành viên\") — xem `overflowLabel` để phân biệt */\n label?: string;\n /**\n * BỀ MẶT mà nhóm đang đứng lên — cho cả lớp nền đục dưới mỗi avatar lẫn màu vòng ngăn cách.\n *\n * Vì sao phải khai: tone avatar là các tint CÓ ALPHA (`bg-teal/15`, `bg-yellow/25`…), mà nhóm\n * thì cố tình cho chúng chồng lên nhau `-space-x-2`. Không có lớp đục ở dưới thì tint của avatar\n * trên hợp thành với MÀU CỦA AVATAR DƯỚI chứ không phải với nền trang — ba khuôn mặt lẫn vào\n * nhau đúng ở chỗ chồng. Đây cũng chính là bề mặt mà `getAvatarToneColors` giả định khi tính hex\n * đục cho canvas/email (`tintOver(…, p.card, …)`), nên mặc định là `--card`.\n *\n * Đặt nhóm lên bề mặt khác thì khai lại cả hai class, vd `\"bg-sidebar ring-sidebar\"`.\n */\n surfaceClassName?: string;\n}\n\n/**\n * Độ chồng theo CỠ, không phải một hằng số cho mọi cỡ.\n *\n * Mẫu Avatar stack ở comitor-ds là `-space-x-2` trên avatar 40px — tức chồng **20%**. Áp thẳng\n * `-space-x-2` cho mọi cỡ thì ở `xs` (24px) thành chồng 33%, và cộng thêm vòng 2px nữa là phần bị\n * che ăn vào đúng chữ cái đầu của initials: \"ĐH\" ở 10px bắt đầu cách mép ~5,5px, trong khi mép che\n * đã tới 10px. Giữ tỉ lệ ~20% thì mọi cỡ đọc được mà mật độ vẫn y như bản duyệt.\n *\n * xs 24px → 4px (17%) · sm 28px → 6px (21%) · md 36px → 8px (22%)\n * lg 44px → 10px (23%) · xl 56px → 12px (21%)\n */\nconst overlapClasses: Record<LetterAvatarSize, string> = {\n xs: \"-space-x-1\",\n sm: \"-space-x-1.5\",\n md: \"-space-x-2\",\n lg: \"-space-x-2.5\",\n xl: \"-space-x-3\"\n};\n\nconst overflowSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-7 text-xs\",\n md: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\"\n};\n\n/**\n * Chồng avatar + ô \"+k\" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).\n *\n * Tooltip hiện tên đầy đủ vì avatar chồng nhau thì initials bị che một phần; không có tooltip\n * thì nhóm avatar chỉ là trang trí, người dùng không biết ai đang ở trong việc.\n *\n * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình\n * — xem chú thích ở trigger bên dưới.\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"sm\",\n onOverflowClick,\n overflowLabel = (count) => `Xem tất cả ${count} thành viên`,\n label = \"Thành viên\",\n surfaceClassName = \"bg-card ring-card\",\n className,\n ...props\n}: AvatarGroupProps) {\n const visible = items.slice(0, Math.max(0, max));\n const overflow = items.length - visible.length;\n\n return (\n <TooltipProvider delayDuration={200}>\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center\", overlapClasses[size], className)}\n {...props}\n >\n {visible.map((item) => (\n <Tooltip key={item.id}>\n <TooltipTrigger asChild>\n {/* Trigger phải FOCUSABLE: `<span>` thường chỉ nhận sự kiện chuột nên tooltip\n (và cả tên thành viên) biến mất với người dùng bàn phím. Nút không có\n onClick là chủ ý — nó chỉ mở tooltip; tên accessible lấy từ `sr-only`\n bên trong LetterAvatar nên không cần aria-label lặp lại. */}\n {/* Viền focus dùng `foreground` chứ không `ring`: `--ring` là gold — #E8B824 chỉ\n 1,86:1 trên nền trang ở light, dưới 3:1 mà WCAG 1.4.11/2.4.11 đòi cho chỉ báo\n focus (ở dark #F5D060 mới đạt 12,67:1, tức hỏng đúng một nửa số người dùng).\n `outline-foreground` đạt 18,91:1 (light) / 17,66:1 (dark) — cùng lối với viền\n focus của dòng trong `DataTable`. */}\n {/* Nền đục nằm DƯỚI tint có alpha của avatar — xem `surfaceClassName`. Vòng\n `ring-2` cùng màu bề mặt chỉ tách hai avatar ra, nó KHÔNG che được phần tint\n chồng lên nhau bên trong vòng. */}\n <button\n type=\"button\"\n className={cn(\n \"rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n surfaceClassName\n )}\n >\n <LetterAvatar name={item.name} src={item.src} size={size} title={undefined} />\n </button>\n </TooltipTrigger>\n <TooltipContent>\n <span className=\"font-medium\">{item.name}</span>\n {/* `opacity-80` ở đây ĐẠT chuẩn và đã đo, không phải chỗ sót: Tooltip là\n `bg-foreground text-background`, nên chữ mờ 80% ra 12,15:1 (light) / 9,85:1\n (dark) — trên xa ngưỡng 4,5:1. Tooltip cũng không tương tác được. */}\n {item.caption && <span className=\"block text-xs opacity-80\">{item.caption}</span>}\n </TooltipContent>\n </Tooltip>\n ))}\n\n {overflow > 0 &&\n (onOverflowClick ? (\n <button\n type=\"button\"\n onClick={onOverflowClick}\n className={cn(\n // `surfaceClassName` đứng TRƯỚC để `bg-muted` ngay sau đè lại phần nền của nó:\n // ô \"+k\" vốn đã đục, ở đây chỉ mượn màu VÒNG của bề mặt.\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 transition-colors hover:bg-accent hover:text-accent-foreground\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n <span className=\"sr-only\">{overflowLabel(items.length)}</span>\n </button>\n ) : (\n <span\n className={cn(\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n </span>\n ))}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";AAuIgB,cAGJ,YAHI;AApIhB,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,iBAAiB,sBAAsB;AACzE,SAAS,oBAA2C;AA4DpD,MAAM,iBAAmD;AAAA,EACvD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,sBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA,gBAAgB,CAAC,UAAU,wBAAc,KAAK;AAAA,EAC9C,QAAQ;AAAA,EACR,mBAAmB;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AAC/C,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,SACE,oBAAC,mBAAgB,eAAe,KAC9B;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,eAAe,IAAI,GAAG,SAAS;AAAA,MACjE,GAAG;AAAA,MAEH;AAAA,gBAAQ,IAAI,CAAC,SACZ,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MAarB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,OAAO,QAAW;AAAA;AAAA,UAC9E,GACF;AAAA,UACA,qBAAC,kBACC;AAAA,gCAAC,UAAK,WAAU,eAAe,eAAK,MAAK;AAAA,YAIxC,KAAK,WAAW,oBAAC,UAAK,WAAU,4BAA4B,eAAK,SAAQ;AAAA,aAC5E;AAAA,aA9BY,KAAK,EA+BnB,CACD;AAAA,QAEA,WAAW,MACT,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA,cACF,oBAAC,UAAK,WAAU,WAAW,wBAAc,MAAM,MAAM,GAAE;AAAA;AAAA;AAAA,QACzD,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA;AAAA;AAAA,QACJ;AAAA;AAAA;AAAA,EAEN,GACF;AAEJ;","names":[]}
@@ -18,6 +18,11 @@ interface ComboboxSharedProps {
18
18
  placeholder?: string;
19
19
  searchPlaceholder?: string;
20
20
  emptyText?: string;
21
+ /**
22
+ * Chữ hiện trong panel khi `loading`. Mặc định tiếng Việt như mọi chuỗi khác của gói.
23
+ * Là chữ THẤY ĐƯỢC (không phải sr-only) và cả `Combobox` lẫn `MultiCombobox` đều đi qua nó.
24
+ */
25
+ loadingText?: string;
21
26
  disabled?: boolean;
22
27
  /**
23
28
  * Có mặt = chế độ BẤT ĐỒNG BỘ: tắt lọc nội bộ của cmdk, app tự lọc `options` (thường là gọi API).
@@ -50,6 +55,17 @@ interface ComboboxProps extends ComboboxSharedProps {
50
55
  /** Giá trị đang chọn; `null`/`undefined` = chưa chọn */
51
56
  value?: string | null;
52
57
  onValueChange: (value: string | null) => void;
58
+ /**
59
+ * Nhãn `sr-only` của nút xóa (mặc định "Xóa lựa chọn").
60
+ *
61
+ * Nút xóa chỉ có mỗi icon `X`, nên chuỗi này CHÍNH LÀ tên của nút với trình đọc màn hình. Ghim
62
+ * cứng tiếng Việt ở đây là lỗi không ảnh chụp nào thấy và cũng không có gì báo đỏ: app tiếng
63
+ * Anh nhìn thì đúng, mà ĐỌC ra vẫn là tiếng Việt.
64
+ *
65
+ * Nằm ở đây chứ không ở `ComboboxSharedProps` vì `MultiCombobox` xóa cả danh sách nên mặc định
66
+ * của nó là một câu khác.
67
+ */
68
+ clearLabel?: string;
53
69
  }
54
70
  interface MultiComboboxProps extends ComboboxSharedProps {
55
71
  value: string[];
@@ -58,6 +74,19 @@ interface MultiComboboxProps extends ComboboxSharedProps {
58
74
  maxDisplay?: number;
59
75
  /** Giới hạn số lựa chọn; đạt ngưỡng thì các mục còn lại bị khoá */
60
76
  maxSelected?: number;
77
+ /**
78
+ * Dòng đếm ở đáy panel khi có `maxSelected`. HÀM vì nó chèn HAI số — nối tay ở component là
79
+ * khoá cứng trật tự tiếng Việt ("Đã chọn 2/5") vào mọi bản dịch.
80
+ */
81
+ selectedCountLabel?: (selected: number, max: number) => string;
82
+ /**
83
+ * Nhãn `sr-only` của nút xóa (mặc định "Xóa tất cả lựa chọn" — nút bỏ SẠCH danh sách đang chọn,
84
+ * không phải bỏ một mục).
85
+ *
86
+ * Cùng lý do như `Combobox`, nhưng gấp hơn một bậc: ở đây `clearable` mặc định là `true`, nên
87
+ * một app tiếng Anh dính chuỗi tiếng Việt này mà không hề bật gì.
88
+ */
89
+ clearLabel?: string;
61
90
  }
62
91
  /**
63
92
  * Ô chọn một giá trị có tìm kiếm — cmdk trong Popover, rộng bằng trigger.
@@ -65,13 +94,13 @@ interface MultiComboboxProps extends ComboboxSharedProps {
65
94
  * Mặc định tìm KHÔNG DẤU tiếng Việt. Truyền `onSearchChange` để chuyển sang chế độ bất đồng bộ
66
95
  * (app tự gọi API và cập nhật `options`).
67
96
  */
68
- declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, onSearchChange, loading, clearable, className, contentClassName, id, name, ...aria }: ComboboxProps): React.JSX.Element;
97
+ declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, loadingText, disabled, onSearchChange, loading, clearable, clearLabel, className, contentClassName, id, name, ...aria }: ComboboxProps): React.JSX.Element;
69
98
  /**
70
99
  * Ô chọn NHIỀU giá trị — cùng hạ tầng với `Combobox`, popover không tự đóng sau mỗi lần chọn.
71
100
  *
72
101
  * Trigger hiện tối đa `maxDisplay` nhãn rồi gộp phần dư thành "+k": danh sách 12 người thực hiện
73
102
  * mà đổ hết ra trigger thì hàng form vỡ bố cục.
74
103
  */
75
- declare function MultiCombobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, onSearchChange, loading, clearable, maxDisplay, maxSelected, className, contentClassName, id, name, ...aria }: MultiComboboxProps): React.JSX.Element;
104
+ declare function MultiCombobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, loadingText, disabled, onSearchChange, loading, clearable, clearLabel, selectedCountLabel, maxDisplay, maxSelected, className, contentClassName, id, name, ...aria }: MultiComboboxProps): React.JSX.Element;
76
105
 
77
106
  export { Combobox, type ComboboxOption, type ComboboxProps, MultiCombobox, type MultiComboboxProps };
@@ -95,6 +95,7 @@ function ComboboxPanel({
95
95
  onSearch,
96
96
  searchPlaceholder,
97
97
  emptyText,
98
+ loadingText,
98
99
  renderItem
99
100
  }) {
100
101
  const { groups, shouldFilter } = useComboboxList(options, isAsync);
@@ -102,7 +103,7 @@ function ComboboxPanel({
102
103
  /* @__PURE__ */ jsx(CommandInput, { placeholder: searchPlaceholder, value: search, onValueChange: onSearch }),
103
104
  /* @__PURE__ */ jsx(CommandList, { children: loading ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground", children: [
104
105
  /* @__PURE__ */ jsx(Loader2, { "aria-hidden": "true", className: "size-4 animate-spin" }),
105
- "\u0110ang t\u1EA3i\u2026"
106
+ loadingText
106
107
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
107
108
  /* @__PURE__ */ jsx(CommandEmpty, { children: emptyText }),
108
109
  groups.map((group) => /* @__PURE__ */ jsx(CommandGroup, { heading: group.label, children: group.items.map(renderItem) }, group.label ?? "__default__"))
@@ -116,10 +117,12 @@ function Combobox({
116
117
  placeholder = "Ch\u1ECDn\u2026",
117
118
  searchPlaceholder = "T\xECm ki\u1EBFm\u2026",
118
119
  emptyText = "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3.",
120
+ loadingText = "\u0110ang t\u1EA3i\u2026",
119
121
  disabled = false,
120
122
  onSearchChange,
121
123
  loading = false,
122
124
  clearable = false,
125
+ clearLabel = "X\xF3a l\u1EF1a ch\u1ECDn",
123
126
  className,
124
127
  contentClassName,
125
128
  id,
@@ -169,6 +172,7 @@ function Combobox({
169
172
  onSearch: handleSearch,
170
173
  searchPlaceholder,
171
174
  emptyText,
175
+ loadingText,
172
176
  renderItem: (option) => /* @__PURE__ */ jsx(
173
177
  CommandItem,
174
178
  {
@@ -193,7 +197,7 @@ function Combobox({
193
197
  {
194
198
  showClear,
195
199
  disabled,
196
- clearLabel: "X\xF3a l\u1EF1a ch\u1ECDn",
200
+ clearLabel,
197
201
  onClear: () => onValueChange(null)
198
202
  }
199
203
  )
@@ -206,10 +210,13 @@ function MultiCombobox({
206
210
  placeholder = "Ch\u1ECDn\u2026",
207
211
  searchPlaceholder = "T\xECm ki\u1EBFm\u2026",
208
212
  emptyText = "Kh\xF4ng t\xECm th\u1EA5y k\u1EBFt qu\u1EA3.",
213
+ loadingText = "\u0110ang t\u1EA3i\u2026",
209
214
  disabled = false,
210
215
  onSearchChange,
211
216
  loading = false,
212
217
  clearable = true,
218
+ clearLabel = "X\xF3a t\u1EA5t c\u1EA3 l\u1EF1a ch\u1ECDn",
219
+ selectedCountLabel = (selected, max) => `\u0110\xE3 ch\u1ECDn ${selected}/${max}`,
213
220
  maxDisplay = 2,
214
221
  maxSelected,
215
222
  className,
@@ -279,6 +286,7 @@ function MultiCombobox({
279
286
  onSearch: handleSearch,
280
287
  searchPlaceholder,
281
288
  emptyText,
289
+ loadingText,
282
290
  renderItem: (option) => {
283
291
  const isSelected = value.includes(option.value);
284
292
  return /* @__PURE__ */ jsx(
@@ -295,12 +303,7 @@ function MultiCombobox({
295
303
  }
296
304
  }
297
305
  ),
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
- ] })
306
+ maxSelected !== void 0 && /* @__PURE__ */ jsx("p", { className: "border-t border-border px-3 py-2 text-xs text-muted-foreground", children: selectedCountLabel(value.length, maxSelected) })
304
307
  ] })
305
308
  ] }),
306
309
  /* @__PURE__ */ jsx(
@@ -308,7 +311,7 @@ function MultiCombobox({
308
311
  {
309
312
  showClear,
310
313
  disabled,
311
- clearLabel: "X\xF3a t\u1EA5t c\u1EA3 l\u1EF1a ch\u1ECDn",
314
+ clearLabel,
312
315
  onClear: () => onValueChange([])
313
316
  }
314
317
  )
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/combobox.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, ChevronsUpDown, Loader2, X } from \"lucide-react\";\nimport { type ReactNode, useMemo, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"../ui/command.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport { removeVietnameseTones } from \"./text-utils.js\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n /** Dòng phụ mờ dưới nhãn (vd email của người dùng, mã của khách hàng) */\n description?: string;\n icon?: IconComponent;\n disabled?: boolean;\n /** Nhãn nhóm — các mục cùng nhóm được gom lại và có tiêu đề nhóm */\n group?: string;\n /** Từ khoá phụ để tìm ra mục này (vd mã số, tên viết tắt) */\n keywords?: string[];\n}\n\ninterface ComboboxSharedProps {\n options: ComboboxOption[];\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n disabled?: boolean;\n /**\n * Có mặt = chế độ BẤT ĐỒNG BỘ: tắt lọc nội bộ của cmdk, app tự lọc `options` (thường là gọi API).\n * Không truyền = lọc tại chỗ, không dấu tiếng Việt.\n */\n onSearchChange?: (search: string) => void;\n /** Đang tải kết quả (chỉ có nghĩa ở chế độ bất đồng bộ) */\n loading?: boolean;\n /** Cho phép xoá lựa chọn bằng nút X trên trigger */\n clearable?: boolean;\n className?: string;\n /** className của panel popover (vd đặt bề rộng cố định) */\n contentClassName?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n /**\n * Ô đang lỗi. Ngoài `aria-invalid` cho trình đọc màn hình, trigger còn chuyển sang viền\n * `--destructive-ink` — cùng tín hiệu thị giác mà `Input` đã có (nhánh nằm ở `button.tsx`).\n *\n * Nhận cả `boolean` lẫn `\"true\"`/`\"false\"` để dùng thẳng được `aria-invalid={!!errors.x}`\n * của react-hook-form. Giá trị falsy KHÔNG phát thuộc tính ra DOM.\n */\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n \"aria-required\"?: boolean | \"true\" | \"false\";\n}\n\n/** `aria-invalid` nhận nhiều hình dạng — quy về một boolean trước khi dùng làm điều kiện. */\nfunction isAriaInvalid(value: ComboboxSharedProps[\"aria-invalid\"]): boolean {\n return value === true || value === \"true\";\n}\n\nexport interface ComboboxProps extends ComboboxSharedProps {\n /** Giá trị đang chọn; `null`/`undefined` = chưa chọn */\n value?: string | null;\n onValueChange: (value: string | null) => void;\n}\n\nexport interface MultiComboboxProps extends ComboboxSharedProps {\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Số nhãn hiện trên trigger trước khi gộp thành \"+k\" (mặc định 2) */\n maxDisplay?: number;\n /** Giới hạn số lựa chọn; đạt ngưỡng thì các mục còn lại bị khoá */\n maxSelected?: number;\n}\n\n/**\n * Lọc không dấu cho cmdk: so khớp cả value, label lẫn keywords.\n *\n * cmdk mặc định chấm điểm theo thuật toán riêng có phân biệt dấu — người Việt gõ \"nguyen\"\n * sẽ không ra \"Nguyễn\". Trả 1/0 là đủ vì thứ tự hiển thị đã theo thứ tự app truyền vào.\n */\nfunction vietnameseFilter(value: string, search: string, keywords?: string[]): number {\n if (!search) return 1;\n const haystack = removeVietnameseTones(`${value} ${keywords?.join(\" \") ?? \"\"}`).toLowerCase();\n return haystack.includes(removeVietnameseTones(search).toLowerCase()) ? 1 : 0;\n}\n\n/** Gom mục theo `group`, giữ nguyên thứ tự xuất hiện đầu tiên của mỗi nhóm. */\nfunction groupOptions(options: ComboboxOption[]): Array<{ label: string | undefined; items: ComboboxOption[] }> {\n const groups = new Map<string, ComboboxOption[]>();\n for (const option of options) {\n const key = option.group ?? \"\";\n const bucket = groups.get(key);\n if (bucket) bucket.push(option);\n else groups.set(key, [option]);\n }\n return [...groups.entries()].map(([label, items]) => ({ label: label || undefined, items }));\n}\n\nfunction OptionRow({ option, selected }: { option: ComboboxOption; selected: boolean }) {\n const Icon = option.icon;\n return (\n <>\n <span\n aria-hidden=\"true\"\n className={cn(\"flex size-4 shrink-0 items-center justify-center\", !selected && \"invisible\")}\n >\n <Check className=\"size-4\" />\n </span>\n {Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{option.label}</span>\n {option.description && (\n <span className=\"block truncate text-xs text-muted-foreground\">{option.description}</span>\n )}\n </span>\n </>\n );\n}\n\n/**\n * Hàng icon ĐÈ lên trigger: nút xóa bên trái, chevron neo cứng bên phải.\n *\n * Vì sao nút xóa phải là ANH EM của trigger chứ không nằm trong: `<button>` lồng `<button>` là\n * HTML không hợp lệ, và nếu hạ nó xuống `<span role=\"button\" tabIndex={-1}>` thì người dùng bàn\n * phím vĩnh viễn không tới được nút xóa.\n *\n * Vì sao CHEVRON cũng phải ra ngoài theo: nó là con cuối của trigger, nên chỗ trống dành cho nút\n * xóa chỉ có thể lấy từ `padding-right` của trigger — mà padding đó ĐẨY luôn chevron sang trái.\n * Kết quả là chọn xong một giá trị thì chevron nhảy chỗ, và hai ô cạnh nhau trong cùng một hàng\n * biểu mẫu (một ô đã chọn, một ô chưa) có chevron lệch nhau. Ra ngoài rồi thì chevron đứng yên ở\n * `right-4` như mọi control khác, nút xóa chèn vào BÊN TRÁI nó — đúng thứ tự của `DatePicker`.\n *\n * `pointer-events-none` ở hàng, `pointer-events-auto` ở nút xóa: chevron chỉ là trang trí, bấm\n * vào nó phải xuyên xuống trigger để mở popover (hồi nó còn là con của trigger thì miễn phí).\n *\n * Và hàng này phải TỰ mờ khi `disabled`: `disabled:opacity-50` của `Button` chỉ áp cho chính\n * trigger, không với tới anh em của nó.\n */\nfunction ComboboxAdornments({\n showClear,\n disabled,\n clearLabel,\n onClear\n}: {\n showClear: boolean;\n disabled: boolean;\n clearLabel: string;\n onClear: () => void;\n}) {\n return (\n <div\n className={cn(\n /* `border border-transparent pr-3` chứ không phải `right-3`: hàng này là ANH EM của\n trigger nên nó đo từ mép BORDER, trong khi chevron của `Select` nằm trong trigger nên\n đ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\n 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ẻ. */\n \"pointer-events-none absolute inset-y-0 right-0 flex items-center border border-transparent pr-3\",\n disabled && \"opacity-50\"\n )}\n >\n {showClear && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"pointer-events-auto size-6 text-muted-foreground hover:text-foreground\"\n onClick={onClear}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{clearLabel}</span>\n </Button>\n )}\n {/* Chevron BÁO TRẠNG THÁI (aria-expanded) trên control còn bấm được → ngưỡng 3:1 của\n WCAG 1.4.11, `opacity-*` không miễn trừ. `opacity-50` chỉ được 2,03:1 (light) / 2,30:1\n (dark) khi trigger đang ở trạng thái placeholder (kế thừa `text-muted-foreground`) ✗.\n Nêu thẳng `text-muted-foreground` cho icon: 5,23:1 (light) / 5,70:1 (dark) ✓, và không\n đổi theo việc đã chọn hay chưa. */}\n <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n </div>\n );\n}\n\n/**\n * Lề của trigger. Trigger là `Button` nên nó mang sẵn `px-4` — mà Combobox nhìn là một Ô NHẬP chứ\n * không phải cái nút, và `Input`/`Select` (cả ở đây lẫn ở comitor-ds) đều `px-3`. Để nguyên `px-4`\n * là trong cùng một cột biểu mẫu, chữ của Combobox thụt vào 4px so với chữ của Input bên trên nó.\n * Nên `px-3`, và mọi icon phải neo ở 12px như các control còn lại.\n *\n * Chỗ trống bên phải phải chừa cho hàng icon tuyệt đối, vì trigger không còn tự đẩy chỗ nữa:\n * `pr-9` = 12 (lề) + 16 (chevron) + 8 (khoảng tới chữ); `pr-15` cộng thêm 24 cho nút xóa.\n */\nconst TRIGGER_PADDING = \"px-3 pr-9\";\nconst TRIGGER_PADDING_WITH_CLEAR = \"px-3 pr-15\";\n\nfunction useComboboxList(options: ComboboxOption[], isAsync: boolean) {\n const groups = useMemo(() => groupOptions(options), [options]);\n return { groups, shouldFilter: !isAsync };\n}\n\nfunction ComboboxPanel({\n options,\n isAsync,\n loading,\n search,\n onSearch,\n searchPlaceholder,\n emptyText,\n renderItem\n}: {\n options: ComboboxOption[];\n isAsync: boolean;\n loading: boolean;\n search: string;\n onSearch: (value: string) => void;\n searchPlaceholder: string;\n emptyText: string;\n renderItem: (option: ComboboxOption) => ReactNode;\n}) {\n const { groups, shouldFilter } = useComboboxList(options, isAsync);\n\n return (\n <Command shouldFilter={shouldFilter} filter={vietnameseFilter}>\n <CommandInput placeholder={searchPlaceholder} value={search} onValueChange={onSearch} />\n <CommandList>\n {loading ? (\n <div className=\"flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground\">\n <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n Đang tải…\n </div>\n ) : (\n <>\n <CommandEmpty>{emptyText}</CommandEmpty>\n {groups.map((group) => (\n <CommandGroup key={group.label ?? \"__default__\"} heading={group.label}>\n {group.items.map(renderItem)}\n </CommandGroup>\n ))}\n </>\n )}\n </CommandList>\n </Command>\n );\n}\n\n/**\n * Ô chọn một giá trị có tìm kiếm — cmdk trong Popover, rộng bằng trigger.\n *\n * Mặc định tìm KHÔNG DẤU tiếng Việt. Truyền `onSearchChange` để chuyển sang chế độ bất đồng bộ\n * (app tự gọi API và cập nhật `options`).\n */\nexport function Combobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = false,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: ComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n const selected = options.find((option) => option.value === value);\n const invalid = isAriaInvalid(aria[\"aria-invalid\"]);\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && Boolean(selected) && !disabled;\n\n return (\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"w-full justify-start font-normal\",\n showClear ? TRIGGER_PADDING_WITH_CLEAR : TRIGGER_PADDING,\n !selected && \"text-muted-foreground\",\n className\n )}\n {...aria}\n // Sau `{...aria}`: chỉ phát thuộc tính khi THẬT SỰ lỗi, để `aria-invalid={false}`\n // không rơi ra DOM thành `aria-invalid=\"false\"` (hợp lệ nhưng thừa).\n aria-invalid={invalid || undefined}\n >\n <span className=\"truncate\">{selected ? selected.label : placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n renderItem={(option) => (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled}\n onSelect={() => {\n onValueChange(option.value === value ? null : option.value);\n setOpen(false);\n setSearch(\"\");\n if (isAsync) onSearchChange?.(\"\");\n }}\n >\n <OptionRow option={option} selected={option.value === value} />\n </CommandItem>\n )}\n />\n </PopoverContent>\n </Popover>\n\n <ComboboxAdornments\n showClear={showClear}\n disabled={disabled}\n clearLabel=\"Xóa lựa chọn\"\n onClear={() => onValueChange(null)}\n />\n </div>\n );\n}\n\n/**\n * Ô chọn NHIỀU giá trị — cùng hạ tầng với `Combobox`, popover không tự đóng sau mỗi lần chọn.\n *\n * Trigger hiện tối đa `maxDisplay` nhãn rồi gộp phần dư thành \"+k\": danh sách 12 người thực hiện\n * mà đổ hết ra trigger thì hàng form vỡ bố cục.\n */\nexport function MultiCombobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = true,\n maxDisplay = 2,\n maxSelected,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: MultiComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n const invalid = isAriaInvalid(aria[\"aria-invalid\"]);\n\n const selectedOptions = options.filter((option) => value.includes(option.value));\n const shown = selectedOptions.slice(0, Math.max(1, maxDisplay));\n const overflow = selectedOptions.length - shown.length;\n const limitReached = maxSelected !== undefined && value.length >= maxSelected;\n\n const toggle = (optionValue: string) => {\n if (value.includes(optionValue)) {\n onValueChange(value.filter((item) => item !== optionValue));\n return;\n }\n if (limitReached) return;\n onValueChange([...value, optionValue]);\n };\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && value.length > 0 && !disabled;\n\n return (\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"h-auto min-h-9 w-full justify-start py-1.5 font-normal\",\n showClear ? TRIGGER_PADDING_WITH_CLEAR : TRIGGER_PADDING,\n value.length === 0 && \"text-muted-foreground\",\n className\n )}\n {...aria}\n // Cùng lý do như Combobox đơn.\n aria-invalid={invalid || undefined}\n >\n <span className=\"flex min-w-0 flex-wrap items-center gap-1\">\n {value.length === 0 ? (\n placeholder\n ) : (\n <>\n {shown.map((option) => (\n <Badge key={option.value} variant=\"secondary\" className=\"max-w-[10rem]\">\n <span className=\"truncate\">{option.label}</span>\n </Badge>\n ))}\n {overflow > 0 && <Badge variant=\"outline\">+{overflow}</Badge>}\n </>\n )}\n </span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n renderItem={(option) => {\n const isSelected = value.includes(option.value);\n return (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled || (limitReached && !isSelected)}\n onSelect={() => toggle(option.value)}\n >\n <OptionRow option={option} selected={isSelected} />\n </CommandItem>\n );\n }}\n />\n {maxSelected !== undefined && (\n <p className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n Đã chọn {value.length}/{maxSelected}\n </p>\n )}\n </PopoverContent>\n </Popover>\n\n <ComboboxAdornments\n showClear={showClear}\n disabled={disabled}\n clearLabel=\"Xóa tất cả lựa chọn\"\n onClear={() => onValueChange([])}\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/combobox.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, ChevronsUpDown, Loader2, X } from \"lucide-react\";\nimport { type ReactNode, useMemo, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"../ui/command.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport { removeVietnameseTones } from \"./text-utils.js\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n /** Dòng phụ mờ dưới nhãn (vd email của người dùng, mã của khách hàng) */\n description?: string;\n icon?: IconComponent;\n disabled?: boolean;\n /** Nhãn nhóm — các mục cùng nhóm được gom lại và có tiêu đề nhóm */\n group?: string;\n /** Từ khoá phụ để tìm ra mục này (vd mã số, tên viết tắt) */\n keywords?: string[];\n}\n\ninterface ComboboxSharedProps {\n options: ComboboxOption[];\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n /**\n * Chữ hiện trong panel khi `loading`. Mặc định tiếng Việt như mọi chuỗi khác của gói.\n * Là chữ THẤY ĐƯỢC (không phải sr-only) và cả `Combobox` lẫn `MultiCombobox` đều đi qua nó.\n */\n loadingText?: string;\n disabled?: boolean;\n /**\n * Có mặt = chế độ BẤT ĐỒNG BỘ: tắt lọc nội bộ của cmdk, app tự lọc `options` (thường là gọi API).\n * Không truyền = lọc tại chỗ, không dấu tiếng Việt.\n */\n onSearchChange?: (search: string) => void;\n /** Đang tải kết quả (chỉ có nghĩa ở chế độ bất đồng bộ) */\n loading?: boolean;\n /** Cho phép xoá lựa chọn bằng nút X trên trigger */\n clearable?: boolean;\n className?: string;\n /** className của panel popover (vd đặt bề rộng cố định) */\n contentClassName?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n /**\n * Ô đang lỗi. Ngoài `aria-invalid` cho trình đọc màn hình, trigger còn chuyển sang viền\n * `--destructive-ink` — cùng tín hiệu thị giác mà `Input` đã có (nhánh nằm ở `button.tsx`).\n *\n * Nhận cả `boolean` lẫn `\"true\"`/`\"false\"` để dùng thẳng được `aria-invalid={!!errors.x}`\n * của react-hook-form. Giá trị falsy KHÔNG phát thuộc tính ra DOM.\n */\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n \"aria-required\"?: boolean | \"true\" | \"false\";\n}\n\n/** `aria-invalid` nhận nhiều hình dạng — quy về một boolean trước khi dùng làm điều kiện. */\nfunction isAriaInvalid(value: ComboboxSharedProps[\"aria-invalid\"]): boolean {\n return value === true || value === \"true\";\n}\n\nexport interface ComboboxProps extends ComboboxSharedProps {\n /** Giá trị đang chọn; `null`/`undefined` = chưa chọn */\n value?: string | null;\n onValueChange: (value: string | null) => void;\n /**\n * Nhãn `sr-only` của nút xóa (mặc định \"Xóa lựa chọn\").\n *\n * Nút xóa chỉ có mỗi icon `X`, nên chuỗi này CHÍNH LÀ tên của nút với trình đọc màn hình. Ghim\n * cứng tiếng Việt ở đây là lỗi không ảnh chụp nào thấy và cũng không có gì báo đỏ: app tiếng\n * Anh nhìn thì đúng, mà ĐỌC ra vẫn là tiếng Việt.\n *\n * Nằm ở đây chứ không ở `ComboboxSharedProps` vì `MultiCombobox` xóa cả danh sách nên mặc định\n * của nó là một câu khác.\n */\n clearLabel?: string;\n}\n\nexport interface MultiComboboxProps extends ComboboxSharedProps {\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Số nhãn hiện trên trigger trước khi gộp thành \"+k\" (mặc định 2) */\n maxDisplay?: number;\n /** Giới hạn số lựa chọn; đạt ngưỡng thì các mục còn lại bị khoá */\n maxSelected?: number;\n /**\n * Dòng đếm ở đáy panel khi có `maxSelected`. HÀM vì nó chèn HAI số — nối tay ở component là\n * khoá cứng trật tự tiếng Việt (\"Đã chọn 2/5\") vào mọi bản dịch.\n */\n selectedCountLabel?: (selected: number, max: number) => string;\n /**\n * Nhãn `sr-only` của nút xóa (mặc định \"Xóa tất cả lựa chọn\" — nút bỏ SẠCH danh sách đang chọn,\n * không phải bỏ một mục).\n *\n * Cùng lý do như `Combobox`, nhưng gấp hơn một bậc: ở đây `clearable` mặc định là `true`, nên\n * một app tiếng Anh dính chuỗi tiếng Việt này mà không hề bật gì.\n */\n clearLabel?: string;\n}\n\n/**\n * Lọc không dấu cho cmdk: so khớp cả value, label lẫn keywords.\n *\n * cmdk mặc định chấm điểm theo thuật toán riêng có phân biệt dấu — người Việt gõ \"nguyen\"\n * sẽ không ra \"Nguyễn\". Trả 1/0 là đủ vì thứ tự hiển thị đã theo thứ tự app truyền vào.\n */\nfunction vietnameseFilter(value: string, search: string, keywords?: string[]): number {\n if (!search) return 1;\n const haystack = removeVietnameseTones(`${value} ${keywords?.join(\" \") ?? \"\"}`).toLowerCase();\n return haystack.includes(removeVietnameseTones(search).toLowerCase()) ? 1 : 0;\n}\n\n/** Gom mục theo `group`, giữ nguyên thứ tự xuất hiện đầu tiên của mỗi nhóm. */\nfunction groupOptions(options: ComboboxOption[]): Array<{ label: string | undefined; items: ComboboxOption[] }> {\n const groups = new Map<string, ComboboxOption[]>();\n for (const option of options) {\n const key = option.group ?? \"\";\n const bucket = groups.get(key);\n if (bucket) bucket.push(option);\n else groups.set(key, [option]);\n }\n return [...groups.entries()].map(([label, items]) => ({ label: label || undefined, items }));\n}\n\nfunction OptionRow({ option, selected }: { option: ComboboxOption; selected: boolean }) {\n const Icon = option.icon;\n return (\n <>\n <span\n aria-hidden=\"true\"\n className={cn(\"flex size-4 shrink-0 items-center justify-center\", !selected && \"invisible\")}\n >\n <Check className=\"size-4\" />\n </span>\n {Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{option.label}</span>\n {option.description && (\n <span className=\"block truncate text-xs text-muted-foreground\">{option.description}</span>\n )}\n </span>\n </>\n );\n}\n\n/**\n * Hàng icon ĐÈ lên trigger: nút xóa bên trái, chevron neo cứng bên phải.\n *\n * Vì sao nút xóa phải là ANH EM của trigger chứ không nằm trong: `<button>` lồng `<button>` là\n * HTML không hợp lệ, và nếu hạ nó xuống `<span role=\"button\" tabIndex={-1}>` thì người dùng bàn\n * phím vĩnh viễn không tới được nút xóa.\n *\n * Vì sao CHEVRON cũng phải ra ngoài theo: nó là con cuối của trigger, nên chỗ trống dành cho nút\n * xóa chỉ có thể lấy từ `padding-right` của trigger — mà padding đó ĐẨY luôn chevron sang trái.\n * Kết quả là chọn xong một giá trị thì chevron nhảy chỗ, và hai ô cạnh nhau trong cùng một hàng\n * biểu mẫu (một ô đã chọn, một ô chưa) có chevron lệch nhau. Ra ngoài rồi thì chevron đứng yên ở\n * `right-4` như mọi control khác, nút xóa chèn vào BÊN TRÁI nó — đúng thứ tự của `DatePicker`.\n *\n * `pointer-events-none` ở hàng, `pointer-events-auto` ở nút xóa: chevron chỉ là trang trí, bấm\n * vào nó phải xuyên xuống trigger để mở popover (hồi nó còn là con của trigger thì miễn phí).\n *\n * Và hàng này phải TỰ mờ khi `disabled`: `disabled:opacity-50` của `Button` chỉ áp cho chính\n * trigger, không với tới anh em của nó.\n */\nfunction ComboboxAdornments({\n showClear,\n disabled,\n clearLabel,\n onClear\n}: {\n showClear: boolean;\n disabled: boolean;\n clearLabel: string;\n onClear: () => void;\n}) {\n return (\n <div\n className={cn(\n /* `border border-transparent pr-3` chứ không phải `right-3`: hàng này là ANH EM của\n trigger nên nó đo từ mép BORDER, trong khi chevron của `Select` nằm trong trigger nên\n đ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\n 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ẻ. */\n \"pointer-events-none absolute inset-y-0 right-0 flex items-center border border-transparent pr-3\",\n disabled && \"opacity-50\"\n )}\n >\n {showClear && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"pointer-events-auto size-6 text-muted-foreground hover:text-foreground\"\n onClick={onClear}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{clearLabel}</span>\n </Button>\n )}\n {/* Chevron BÁO TRẠNG THÁI (aria-expanded) trên control còn bấm được → ngưỡng 3:1 của\n WCAG 1.4.11, `opacity-*` không miễn trừ. `opacity-50` chỉ được 2,03:1 (light) / 2,30:1\n (dark) khi trigger đang ở trạng thái placeholder (kế thừa `text-muted-foreground`) ✗.\n Nêu thẳng `text-muted-foreground` cho icon: 5,23:1 (light) / 5,70:1 (dark) ✓, và không\n đổi theo việc đã chọn hay chưa. */}\n <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n </div>\n );\n}\n\n/**\n * Lề của trigger. Trigger là `Button` nên nó mang sẵn `px-4` — mà Combobox nhìn là một Ô NHẬP chứ\n * không phải cái nút, và `Input`/`Select` (cả ở đây lẫn ở comitor-ds) đều `px-3`. Để nguyên `px-4`\n * là trong cùng một cột biểu mẫu, chữ của Combobox thụt vào 4px so với chữ của Input bên trên nó.\n * Nên `px-3`, và mọi icon phải neo ở 12px như các control còn lại.\n *\n * Chỗ trống bên phải phải chừa cho hàng icon tuyệt đối, vì trigger không còn tự đẩy chỗ nữa:\n * `pr-9` = 12 (lề) + 16 (chevron) + 8 (khoảng tới chữ); `pr-15` cộng thêm 24 cho nút xóa.\n */\nconst TRIGGER_PADDING = \"px-3 pr-9\";\nconst TRIGGER_PADDING_WITH_CLEAR = \"px-3 pr-15\";\n\nfunction useComboboxList(options: ComboboxOption[], isAsync: boolean) {\n const groups = useMemo(() => groupOptions(options), [options]);\n return { groups, shouldFilter: !isAsync };\n}\n\nfunction ComboboxPanel({\n options,\n isAsync,\n loading,\n search,\n onSearch,\n searchPlaceholder,\n emptyText,\n loadingText,\n renderItem\n}: {\n options: ComboboxOption[];\n isAsync: boolean;\n loading: boolean;\n search: string;\n onSearch: (value: string) => void;\n searchPlaceholder: string;\n emptyText: string;\n loadingText: string;\n renderItem: (option: ComboboxOption) => ReactNode;\n}) {\n const { groups, shouldFilter } = useComboboxList(options, isAsync);\n\n return (\n <Command shouldFilter={shouldFilter} filter={vietnameseFilter}>\n <CommandInput placeholder={searchPlaceholder} value={search} onValueChange={onSearch} />\n <CommandList>\n {loading ? (\n <div className=\"flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground\">\n <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n {loadingText}\n </div>\n ) : (\n <>\n <CommandEmpty>{emptyText}</CommandEmpty>\n {groups.map((group) => (\n <CommandGroup key={group.label ?? \"__default__\"} heading={group.label}>\n {group.items.map(renderItem)}\n </CommandGroup>\n ))}\n </>\n )}\n </CommandList>\n </Command>\n );\n}\n\n/**\n * Ô chọn một giá trị có tìm kiếm — cmdk trong Popover, rộng bằng trigger.\n *\n * Mặc định tìm KHÔNG DẤU tiếng Việt. Truyền `onSearchChange` để chuyển sang chế độ bất đồng bộ\n * (app tự gọi API và cập nhật `options`).\n */\nexport function Combobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n loadingText = \"Đang tải…\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = false,\n clearLabel = \"Xóa lựa chọn\",\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: ComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n const selected = options.find((option) => option.value === value);\n const invalid = isAriaInvalid(aria[\"aria-invalid\"]);\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && Boolean(selected) && !disabled;\n\n return (\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"w-full justify-start font-normal\",\n showClear ? TRIGGER_PADDING_WITH_CLEAR : TRIGGER_PADDING,\n !selected && \"text-muted-foreground\",\n className\n )}\n {...aria}\n // Sau `{...aria}`: chỉ phát thuộc tính khi THẬT SỰ lỗi, để `aria-invalid={false}`\n // không rơi ra DOM thành `aria-invalid=\"false\"` (hợp lệ nhưng thừa).\n aria-invalid={invalid || undefined}\n >\n <span className=\"truncate\">{selected ? selected.label : placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n loadingText={loadingText}\n renderItem={(option) => (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled}\n onSelect={() => {\n onValueChange(option.value === value ? null : option.value);\n setOpen(false);\n setSearch(\"\");\n if (isAsync) onSearchChange?.(\"\");\n }}\n >\n <OptionRow option={option} selected={option.value === value} />\n </CommandItem>\n )}\n />\n </PopoverContent>\n </Popover>\n\n <ComboboxAdornments\n showClear={showClear}\n disabled={disabled}\n clearLabel={clearLabel}\n onClear={() => onValueChange(null)}\n />\n </div>\n );\n}\n\n/**\n * Ô chọn NHIỀU giá trị — cùng hạ tầng với `Combobox`, popover không tự đóng sau mỗi lần chọn.\n *\n * Trigger hiện tối đa `maxDisplay` nhãn rồi gộp phần dư thành \"+k\": danh sách 12 người thực hiện\n * mà đổ hết ra trigger thì hàng form vỡ bố cục.\n */\nexport function MultiCombobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n loadingText = \"Đang tải…\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = true,\n clearLabel = \"Xóa tất cả lựa chọn\",\n selectedCountLabel = (selected, max) => `Đã chọn ${selected}/${max}`,\n maxDisplay = 2,\n maxSelected,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: MultiComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n const invalid = isAriaInvalid(aria[\"aria-invalid\"]);\n\n const selectedOptions = options.filter((option) => value.includes(option.value));\n const shown = selectedOptions.slice(0, Math.max(1, maxDisplay));\n const overflow = selectedOptions.length - shown.length;\n const limitReached = maxSelected !== undefined && value.length >= maxSelected;\n\n const toggle = (optionValue: string) => {\n if (value.includes(optionValue)) {\n onValueChange(value.filter((item) => item !== optionValue));\n return;\n }\n if (limitReached) return;\n onValueChange([...value, optionValue]);\n };\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && value.length > 0 && !disabled;\n\n return (\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"h-auto min-h-9 w-full justify-start py-1.5 font-normal\",\n showClear ? TRIGGER_PADDING_WITH_CLEAR : TRIGGER_PADDING,\n value.length === 0 && \"text-muted-foreground\",\n className\n )}\n {...aria}\n // Cùng lý do như Combobox đơn.\n aria-invalid={invalid || undefined}\n >\n <span className=\"flex min-w-0 flex-wrap items-center gap-1\">\n {value.length === 0 ? (\n placeholder\n ) : (\n <>\n {shown.map((option) => (\n <Badge key={option.value} variant=\"secondary\" className=\"max-w-[10rem]\">\n <span className=\"truncate\">{option.label}</span>\n </Badge>\n ))}\n {overflow > 0 && <Badge variant=\"outline\">+{overflow}</Badge>}\n </>\n )}\n </span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n loadingText={loadingText}\n renderItem={(option) => {\n const isSelected = value.includes(option.value);\n return (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled || (limitReached && !isSelected)}\n onSelect={() => toggle(option.value)}\n >\n <OptionRow option={option} selected={isSelected} />\n </CommandItem>\n );\n }}\n />\n {maxSelected !== undefined && (\n <p className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n {selectedCountLabel(value.length, maxSelected)}\n </p>\n )}\n </PopoverContent>\n </Popover>\n\n <ComboboxAdornments\n showClear={showClear}\n disabled={disabled}\n clearLabel={clearLabel}\n onClear={() => onValueChange([])}\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Nhãn mặc định của `CommandPalette`.
3
+ *
4
+ * Module **CỐ Ý không có `"use client"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy
5
+ * đủ: mọi export của một file `"use client"` đến tay Server Component dưới dạng
6
+ * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần
7
+ * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một
8
+ * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.
9
+ *
10
+ * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `command-palette.tsx` chỉ được `import`,
11
+ * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).
12
+ */
13
+ /**
14
+ * Nhãn chỉ dành cho TRÌNH ĐỌC MÀN HÌNH của `CommandPalette` — cả hai đều nằm trong `sr-only`,
15
+ * không hiện thành chữ trên giao diện.
16
+ *
17
+ * Vì sao vẫn phải override được: `DialogTitle` và `DialogDescription` là phần bắt buộc của một
18
+ * hộp thoại, luôn được xướng lên khi palette mở. Chuỗi tiếng Việt chôn cứng ở đây khiến app
19
+ * tiếng Anh trông thì đúng nhưng ĐỌC ra vẫn là tiếng Việt — lỗi không ai thấy trên ảnh chụp và
20
+ * cũng không có gì báo đỏ.
21
+ *
22
+ * ⚠ Ba chuỗi CÓ HIỆN của palette (`placeholder`, `emptyText`, `recentLabel`) vốn đã là prop rời
23
+ * và vẫn ở nguyên đó, không gom vào đây: chúng thay đổi theo TỪNG chỗ đặt palette (mỗi app một
24
+ * cách gọi tên "trang", "người"), còn hai nhãn này chỉ đổi theo NGÔN NGỮ.
25
+ */
26
+ interface CommandPaletteLabels {
27
+ /** `DialogTitle` sr-only — tên hộp thoại, trình đọc màn hình xướng ngay khi palette mở */
28
+ title: string;
29
+ /** `DialogDescription` sr-only — cách dùng bảng lệnh, đọc tiếp sau tiêu đề */
30
+ description: string;
31
+ }
32
+ /** Nhãn tiếng Việt mặc định — cả hai khoá đều override được qua prop `labels`. */
33
+ declare const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels;
34
+
35
+ export { type CommandPaletteLabels, DEFAULT_COMMAND_PALETTE_LABELS };
@@ -0,0 +1,8 @@
1
+ const DEFAULT_COMMAND_PALETTE_LABELS = {
2
+ title: "B\u1EA3ng l\u1EC7nh",
3
+ description: "G\xF5 \u0111\u1EC3 t\xECm l\u1EC7nh, trang ho\u1EB7c ng\u01B0\u1EDDi; d\xF9ng m\u0169i t\xEAn \u0111\u1EC3 ch\u1ECDn v\xE0 Enter \u0111\u1EC3 ch\u1EA1y."
4
+ };
5
+ export {
6
+ DEFAULT_COMMAND_PALETTE_LABELS
7
+ };
8
+ //# sourceMappingURL=command-palette-constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/command-palette-constants.ts"],"sourcesContent":["/**\n * Nhãn mặc định của `CommandPalette`.\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một\n * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `command-palette.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\n\n/**\n * Nhãn chỉ dành cho TRÌNH ĐỌC MÀN HÌNH của `CommandPalette` — cả hai đều nằm trong `sr-only`,\n * không hiện thành chữ trên giao diện.\n *\n * Vì sao vẫn phải override được: `DialogTitle` và `DialogDescription` là phần bắt buộc của một\n * hộp thoại, luôn được xướng lên khi palette mở. Chuỗi tiếng Việt chôn cứng ở đây khiến app\n * tiếng Anh trông thì đúng nhưng ĐỌC ra vẫn là tiếng Việt — lỗi không ai thấy trên ảnh chụp và\n * cũng không có gì báo đỏ.\n *\n * ⚠ Ba chuỗi CÓ HIỆN của palette (`placeholder`, `emptyText`, `recentLabel`) vốn đã là prop rời\n * và vẫn ở nguyên đó, không gom vào đây: chúng thay đổi theo TỪNG chỗ đặt palette (mỗi app một\n * cách gọi tên \"trang\", \"người\"), còn hai nhãn này chỉ đổi theo NGÔN NGỮ.\n */\nexport interface CommandPaletteLabels {\n /** `DialogTitle` sr-only — tên hộp thoại, trình đọc màn hình xướng ngay khi palette mở */\n title: string;\n /** `DialogDescription` sr-only — cách dùng bảng lệnh, đọc tiếp sau tiêu đề */\n description: string;\n}\n\n/** Nhãn tiếng Việt mặc định — cả hai khoá đều override được qua prop `labels`. */\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n title: \"Bảng lệnh\",\n description: \"Gõ để tìm lệnh, trang hoặc người; dùng mũi tên để chọn và Enter để chạy.\"\n};\n"],"mappings":"AAkCO,MAAM,iCAAuD;AAAA,EAClE,OAAO;AAAA,EACP,aAAa;AACf;","names":[]}
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { ReactNode } from 'react';
3
+ import { CommandPaletteLabels } from './command-palette-constants.js';
3
4
  import { IconComponent } from './empty-state.js';
4
5
 
5
6
  interface CommandPaletteItem {
@@ -41,6 +42,8 @@ interface CommandPaletteProps {
41
42
  * hai listener cùng lật một state (mở rồi đóng ngay trong một lần bấm).
42
43
  */
43
44
  registerShortcut?: boolean;
45
+ /** Ghi đè hai chuỗi sr-only của hộp thoại (mặc định tiếng Việt) — xem `CommandPaletteLabels` */
46
+ labels?: Partial<CommandPaletteLabels>;
44
47
  className?: string;
45
48
  }
46
49
  /**
@@ -51,6 +54,6 @@ interface CommandPaletteProps {
51
54
  *
52
55
  * Tìm kiếm mặc định KHÔNG DẤU tiếng Việt: gõ "tao viec" ra "Tạo việc mới".
53
56
  */
54
- declare function CommandPalette({ open, onOpenChange, groups, recent, recentLabel, placeholder, emptyText, onSearchChange, loading, footer, registerShortcut, className }: CommandPaletteProps): React.JSX.Element;
57
+ declare function CommandPalette({ open, onOpenChange, groups, recent, recentLabel, placeholder, emptyText, onSearchChange, loading, footer, registerShortcut, labels: labelsProp, className }: CommandPaletteProps): React.JSX.Element;
55
58
 
56
59
  export { CommandPalette, type CommandPaletteGroup, type CommandPaletteItem, type CommandPaletteProps };
@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
6
6
  import { cn } from "../../lib/cn.js";
7
7
  import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "../ui/command.js";
8
8
  import { Dialog, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog.js";
9
+ import { DEFAULT_COMMAND_PALETTE_LABELS } from "./command-palette-constants.js";
9
10
  import { KeyboardHint } from "./keyboard-hint.js";
10
11
  import { removeVietnameseTones } from "./text-utils.js";
11
12
  function PaletteInput({
@@ -52,8 +53,10 @@ function CommandPalette({
52
53
  loading = false,
53
54
  footer,
54
55
  registerShortcut = true,
56
+ labels: labelsProp,
55
57
  className
56
58
  }) {
59
+ const labels = { ...DEFAULT_COMMAND_PALETTE_LABELS, ...labelsProp };
57
60
  const [search, setSearch] = useState("");
58
61
  const isAsync = onSearchChange !== void 0;
59
62
  useEffect(() => {
@@ -80,8 +83,8 @@ function CommandPalette({
80
83
  };
81
84
  const showRecent = recent.length > 0 && search.trim() === "";
82
85
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { size: "lg", showCloseButton: false, className: cn("overflow-hidden p-0", className), children: [
83
- /* @__PURE__ */ jsx(DialogTitle, { className: "sr-only", children: "B\u1EA3ng l\u1EC7nh" }),
84
- /* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: "G\xF5 \u0111\u1EC3 t\xECm l\u1EC7nh, trang ho\u1EB7c ng\u01B0\u1EDDi; d\xF9ng m\u0169i t\xEAn \u0111\u1EC3 ch\u1ECDn v\xE0 Enter \u0111\u1EC3 ch\u1EA1y." }),
86
+ /* @__PURE__ */ jsx(DialogTitle, { className: "sr-only", children: labels.title }),
87
+ /* @__PURE__ */ jsx(DialogDescription, { className: "sr-only", children: labels.description }),
85
88
  /* @__PURE__ */ jsxs(
86
89
  Command,
87
90
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/command-palette.tsx"],"sourcesContent":["\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Loader2, Search } from \"lucide-react\";\nimport { type ReactNode, useEffect, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from \"../ui/command.js\";\nimport { Dialog, DialogContent, DialogDescription, DialogTitle } from \"../ui/dialog.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport { KeyboardHint } from \"./keyboard-hint.js\";\nimport { removeVietnameseTones } from \"./text-utils.js\";\n\nexport interface CommandPaletteItem {\n id: string;\n label: string;\n /** Dòng phụ mờ (đường dẫn, mô tả ngắn, tên workspace) */\n description?: string;\n icon?: IconComponent;\n /** Gợi ý phím tắt hiện bên phải, vd \"mod+shift+n\" */\n shortcut?: string;\n /** Từ khoá phụ để tìm ra mục (tên tiếng Anh, viết tắt, mã) */\n keywords?: string[];\n disabled?: boolean;\n onSelect: () => void;\n}\n\nexport interface CommandPaletteGroup {\n id: string;\n label?: string;\n items: CommandPaletteItem[];\n}\n\nexport interface CommandPaletteProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n groups: CommandPaletteGroup[];\n /** Nhóm \"Gần đây\" luôn nằm trên cùng khi chưa gõ gì */\n recent?: CommandPaletteItem[];\n recentLabel?: string;\n placeholder?: string;\n emptyText?: string;\n /** Có mặt = app tự tìm (gọi API) → tắt lọc nội bộ của cmdk */\n onSearchChange?: (search: string) => void;\n loading?: boolean;\n /** Nội dung chân bảng (gợi ý phím, trạng thái) */\n footer?: ReactNode;\n /**\n * Tự gắn phím tắt Cmd/Ctrl+K để mở (mặc định true).\n *\n * Shell của Comitor cũng gắn phím tắt này; khi dùng trong shell hãy đặt `false` để tránh\n * hai listener cùng lật một state (mở rồi đóng ngay trong một lần bấm).\n */\n registerShortcut?: boolean;\n className?: string;\n}\n\n/**\n * Ô nhập của palette — dựng thẳng trên `cmdk` thay vì dùng `CommandInput` của tầng primitive.\n *\n * Lý do: cần thay icon kính lúp bằng vòng xoay khi đang tìm phía server, và cần chỗ đặt gợi ý\n * phím Esc bên phải — `CommandInput` khoá cứng cả hai chi tiết đó.\n */\nfunction PaletteInput({\n value,\n onValueChange,\n placeholder,\n loading\n}: {\n value: string;\n onValueChange: (value: string) => void;\n placeholder: string;\n loading: boolean;\n}) {\n return (\n <div className=\"flex h-12 items-center gap-2 border-b border-border px-3\">\n {loading ? (\n <Loader2 aria-hidden=\"true\" className=\"size-4 shrink-0 animate-spin text-muted-foreground\" />\n ) : (\n <Search aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n )}\n <CommandPrimitive.Input\n value={value}\n onValueChange={onValueChange}\n placeholder={placeholder}\n aria-label={placeholder}\n className=\"flex h-11 w-full bg-transparent text-sm outline-hidden placeholder:text-muted-foreground\"\n />\n <KeyboardHint keys=\"esc\" className=\"hidden sm:inline-flex\" />\n </div>\n );\n}\n\nfunction PaletteRow({ item }: { item: CommandPaletteItem }) {\n const Icon = item.icon;\n return (\n <>\n {Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{item.label}</span>\n {item.description && <span className=\"block truncate text-xs text-muted-foreground\">{item.description}</span>}\n </span>\n {item.shortcut && <KeyboardHint keys={item.shortcut} className=\"ml-auto\" />}\n </>\n );\n}\n\n/**\n * Bảng lệnh (Cmd/Ctrl+K) — điều hướng nhanh và chạy lệnh, kiểu Linear/Raycast.\n *\n * Đặt ở tầng composite (không phải trong shell) vì app không dùng `AppShell` — trang cài đặt\n * độc lập, công cụ nội bộ — vẫn cần bảng lệnh. Shell chỉ giữ state mở/đóng và truyền xuống.\n *\n * Tìm kiếm mặc định KHÔNG DẤU tiếng Việt: gõ \"tao viec\" ra \"Tạo việc mới\".\n */\nexport function CommandPalette({\n open,\n onOpenChange,\n groups,\n recent = [],\n recentLabel = \"Gần đây\",\n placeholder = \"Tìm lệnh, trang, người…\",\n emptyText = \"Không tìm thấy kết quả.\",\n onSearchChange,\n loading = false,\n footer,\n registerShortcut = true,\n className\n}: CommandPaletteProps) {\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n\n // Reset ô tìm mỗi lần mở: mở lại palette mà còn nguyên từ khoá cũ là hành vi gây bất ngờ.\n useEffect(() => {\n if (!open) setSearch(\"\");\n }, [open]);\n\n useEffect(() => {\n if (!registerShortcut) return;\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"k\" || !(event.metaKey || event.ctrlKey)) return;\n event.preventDefault();\n onOpenChange(!open);\n };\n\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [registerShortcut, open, onOpenChange]);\n\n const handleSearch = (value: string) => {\n setSearch(value);\n onSearchChange?.(value);\n };\n\n const runItem = (item: CommandPaletteItem) => {\n if (item.disabled) return;\n onOpenChange(false);\n item.onSelect();\n };\n\n const showRecent = recent.length > 0 && search.trim() === \"\";\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent size=\"lg\" showCloseButton={false} className={cn(\"overflow-hidden p-0\", className)}>\n <DialogTitle className=\"sr-only\">Bảng lệnh</DialogTitle>\n <DialogDescription className=\"sr-only\">\n Gõ để tìm lệnh, trang hoặc người; dùng mũi tên để chọn và Enter để chạy.\n </DialogDescription>\n\n <Command\n shouldFilter={!isAsync}\n filter={(value, query, keywords) => {\n if (!query) return 1;\n const haystack = removeVietnameseTones(`${value} ${keywords?.join(\" \") ?? \"\"}`).toLowerCase();\n return haystack.includes(removeVietnameseTones(query).toLowerCase()) ? 1 : 0;\n }}\n className=\"bg-transparent\"\n >\n <PaletteInput value={search} onValueChange={handleSearch} placeholder={placeholder} loading={loading} />\n\n <CommandList className=\"max-h-[22rem]\">\n {!loading && <CommandEmpty>{emptyText}</CommandEmpty>}\n\n {showRecent && (\n <CommandGroup heading={recentLabel}>\n {recent.map((item) => (\n <CommandItem\n key={`recent-${item.id}`}\n value={`recent-${item.id}`}\n keywords={[item.label, item.description ?? \"\", ...(item.keywords ?? [])]}\n disabled={item.disabled}\n onSelect={() => runItem(item)}\n >\n <PaletteRow item={item} />\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n {groups.map((group) => (\n <CommandGroup key={group.id} heading={group.label}>\n {group.items.map((item) => (\n <CommandItem\n key={item.id}\n value={item.id}\n keywords={[item.label, item.description ?? \"\", ...(item.keywords ?? [])]}\n disabled={item.disabled}\n onSelect={() => runItem(item)}\n >\n <PaletteRow item={item} />\n </CommandItem>\n ))}\n </CommandGroup>\n ))}\n </CommandList>\n\n {footer && (\n <div className=\"flex items-center gap-2 border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n {footer}\n </div>\n )}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/command-palette.tsx"],"sourcesContent":["\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Loader2, Search } from \"lucide-react\";\nimport { type ReactNode, useEffect, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from \"../ui/command.js\";\nimport { Dialog, DialogContent, DialogDescription, DialogTitle } from \"../ui/dialog.js\";\nimport { type CommandPaletteLabels, DEFAULT_COMMAND_PALETTE_LABELS } from \"./command-palette-constants.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport { KeyboardHint } from \"./keyboard-hint.js\";\nimport { removeVietnameseTones } from \"./text-utils.js\";\n\nexport interface CommandPaletteItem {\n id: string;\n label: string;\n /** Dòng phụ mờ (đường dẫn, mô tả ngắn, tên workspace) */\n description?: string;\n icon?: IconComponent;\n /** Gợi ý phím tắt hiện bên phải, vd \"mod+shift+n\" */\n shortcut?: string;\n /** Từ khoá phụ để tìm ra mục (tên tiếng Anh, viết tắt, mã) */\n keywords?: string[];\n disabled?: boolean;\n onSelect: () => void;\n}\n\nexport interface CommandPaletteGroup {\n id: string;\n label?: string;\n items: CommandPaletteItem[];\n}\n\nexport interface CommandPaletteProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n groups: CommandPaletteGroup[];\n /** Nhóm \"Gần đây\" luôn nằm trên cùng khi chưa gõ gì */\n recent?: CommandPaletteItem[];\n recentLabel?: string;\n placeholder?: string;\n emptyText?: string;\n /** Có mặt = app tự tìm (gọi API) → tắt lọc nội bộ của cmdk */\n onSearchChange?: (search: string) => void;\n loading?: boolean;\n /** Nội dung chân bảng (gợi ý phím, trạng thái) */\n footer?: ReactNode;\n /**\n * Tự gắn phím tắt Cmd/Ctrl+K để mở (mặc định true).\n *\n * Shell của Comitor cũng gắn phím tắt này; khi dùng trong shell hãy đặt `false` để tránh\n * hai listener cùng lật một state (mở rồi đóng ngay trong một lần bấm).\n */\n registerShortcut?: boolean;\n /** Ghi đè hai chuỗi sr-only của hộp thoại (mặc định tiếng Việt) — xem `CommandPaletteLabels` */\n labels?: Partial<CommandPaletteLabels>;\n className?: string;\n}\n\n/**\n * Ô nhập của palette — dựng thẳng trên `cmdk` thay vì dùng `CommandInput` của tầng primitive.\n *\n * Lý do: cần thay icon kính lúp bằng vòng xoay khi đang tìm phía server, và cần chỗ đặt gợi ý\n * phím Esc bên phải — `CommandInput` khoá cứng cả hai chi tiết đó.\n */\nfunction PaletteInput({\n value,\n onValueChange,\n placeholder,\n loading\n}: {\n value: string;\n onValueChange: (value: string) => void;\n placeholder: string;\n loading: boolean;\n}) {\n return (\n <div className=\"flex h-12 items-center gap-2 border-b border-border px-3\">\n {loading ? (\n <Loader2 aria-hidden=\"true\" className=\"size-4 shrink-0 animate-spin text-muted-foreground\" />\n ) : (\n <Search aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n )}\n <CommandPrimitive.Input\n value={value}\n onValueChange={onValueChange}\n placeholder={placeholder}\n aria-label={placeholder}\n className=\"flex h-11 w-full bg-transparent text-sm outline-hidden placeholder:text-muted-foreground\"\n />\n <KeyboardHint keys=\"esc\" className=\"hidden sm:inline-flex\" />\n </div>\n );\n}\n\nfunction PaletteRow({ item }: { item: CommandPaletteItem }) {\n const Icon = item.icon;\n return (\n <>\n {Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{item.label}</span>\n {item.description && <span className=\"block truncate text-xs text-muted-foreground\">{item.description}</span>}\n </span>\n {item.shortcut && <KeyboardHint keys={item.shortcut} className=\"ml-auto\" />}\n </>\n );\n}\n\n/**\n * Bảng lệnh (Cmd/Ctrl+K) — điều hướng nhanh và chạy lệnh, kiểu Linear/Raycast.\n *\n * Đặt ở tầng composite (không phải trong shell) vì app không dùng `AppShell` — trang cài đặt\n * độc lập, công cụ nội bộ — vẫn cần bảng lệnh. Shell chỉ giữ state mở/đóng và truyền xuống.\n *\n * Tìm kiếm mặc định KHÔNG DẤU tiếng Việt: gõ \"tao viec\" ra \"Tạo việc mới\".\n */\nexport function CommandPalette({\n open,\n onOpenChange,\n groups,\n recent = [],\n recentLabel = \"Gần đây\",\n placeholder = \"Tìm lệnh, trang, người…\",\n emptyText = \"Không tìm thấy kết quả.\",\n onSearchChange,\n loading = false,\n footer,\n registerShortcut = true,\n labels: labelsProp,\n className\n}: CommandPaletteProps) {\n const labels = { ...DEFAULT_COMMAND_PALETTE_LABELS, ...labelsProp };\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n\n // Reset ô tìm mỗi lần mở: mở lại palette mà còn nguyên từ khoá cũ là hành vi gây bất ngờ.\n useEffect(() => {\n if (!open) setSearch(\"\");\n }, [open]);\n\n useEffect(() => {\n if (!registerShortcut) return;\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"k\" || !(event.metaKey || event.ctrlKey)) return;\n event.preventDefault();\n onOpenChange(!open);\n };\n\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [registerShortcut, open, onOpenChange]);\n\n const handleSearch = (value: string) => {\n setSearch(value);\n onSearchChange?.(value);\n };\n\n const runItem = (item: CommandPaletteItem) => {\n if (item.disabled) return;\n onOpenChange(false);\n item.onSelect();\n };\n\n const showRecent = recent.length > 0 && search.trim() === \"\";\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent size=\"lg\" showCloseButton={false} className={cn(\"overflow-hidden p-0\", className)}>\n <DialogTitle className=\"sr-only\">{labels.title}</DialogTitle>\n <DialogDescription className=\"sr-only\">{labels.description}</DialogDescription>\n\n <Command\n shouldFilter={!isAsync}\n filter={(value, query, keywords) => {\n if (!query) return 1;\n const haystack = removeVietnameseTones(`${value} ${keywords?.join(\" \") ?? \"\"}`).toLowerCase();\n return haystack.includes(removeVietnameseTones(query).toLowerCase()) ? 1 : 0;\n }}\n className=\"bg-transparent\"\n >\n <PaletteInput value={search} onValueChange={handleSearch} placeholder={placeholder} loading={loading} />\n\n <CommandList className=\"max-h-[22rem]\">\n {!loading && <CommandEmpty>{emptyText}</CommandEmpty>}\n\n {showRecent && (\n <CommandGroup heading={recentLabel}>\n {recent.map((item) => (\n <CommandItem\n key={`recent-${item.id}`}\n value={`recent-${item.id}`}\n keywords={[item.label, item.description ?? \"\", ...(item.keywords ?? [])]}\n disabled={item.disabled}\n onSelect={() => runItem(item)}\n >\n <PaletteRow item={item} />\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n {groups.map((group) => (\n <CommandGroup key={group.id} heading={group.label}>\n {group.items.map((item) => (\n <CommandItem\n key={item.id}\n value={item.id}\n keywords={[item.label, item.description ?? \"\", ...(item.keywords ?? [])]}\n disabled={item.disabled}\n onSelect={() => runItem(item)}\n >\n <PaletteRow item={item} />\n </CommandItem>\n ))}\n </CommandGroup>\n ))}\n </CommandList>\n\n {footer && (\n <div className=\"flex items-center gap-2 border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n {footer}\n </div>\n )}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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","names":[]}
@@ -15,6 +15,14 @@ interface ConfirmDialogProps {
15
15
  icon?: IconComponent;
16
16
  confirmLabel?: string;
17
17
  cancelLabel?: string;
18
+ /**
19
+ * Nhãn nút xác nhận trong lúc `onConfirm` bất đồng bộ đang chạy (mặc định "Đang xử lý…").
20
+ *
21
+ * Phải là prop chứ không được chôn cứng: tài liệu khuyến khích `onConfirm` trả về Promise, nên
22
+ * đây KHÔNG phải nhánh hiếm — app tiếng Anh đặt `confirmLabel="Delete"` vẫn thấy nút nhảy sang
23
+ * tiếng Việt đúng vào giây người dùng đang nhìn nó.
24
+ */
25
+ pendingLabel?: string;
18
26
  /** "destructive" tô nút xác nhận màu đỏ — dùng cho xoá/thu hồi */
19
27
  variant?: "default" | "destructive";
20
28
  /**
@@ -33,8 +41,9 @@ interface ConfirmDialogProps {
33
41
  * Hộp thoại xác nhận cho hành động không hoàn tác được (xoá việc, gỡ thành viên, huỷ gói).
34
42
  *
35
43
  * Bọc `AlertDialog` để mọi app có cùng một cách hỏi lại; nút xác nhận tự vào trạng thái
36
- * "Đang xử lý…" khi `onConfirm` bất đồng bộ, và lỗi ném ra sẽ giữ dialog mở để người dùng thử lại.
44
+ * "Đang xử lý…" (đổi được qua `pendingLabel`) khi `onConfirm` bất đồng bộ, và lỗi ném ra sẽ giữ
45
+ * dialog mở để người dùng thử lại.
37
46
  */
38
- declare function ConfirmDialog({ open, onOpenChange, trigger, title, description, children, icon: Icon, confirmLabel, cancelLabel, variant, onConfirm, confirmDisabled, onError, className }: ConfirmDialogProps): React.JSX.Element;
47
+ declare function ConfirmDialog({ open, onOpenChange, trigger, title, description, children, icon: Icon, confirmLabel, cancelLabel, pendingLabel, variant, onConfirm, confirmDisabled, onError, className }: ConfirmDialogProps): React.JSX.Element;
39
48
 
40
49
  export { ConfirmDialog, type ConfirmDialogProps };
@@ -24,6 +24,7 @@ function ConfirmDialog({
24
24
  icon: Icon,
25
25
  confirmLabel = "X\xE1c nh\u1EADn",
26
26
  cancelLabel = "H\u1EE7y",
27
+ pendingLabel = "\u0110ang x\u1EED l\xFD\u2026",
27
28
  variant = "default",
28
29
  onConfirm,
29
30
  confirmDisabled = false,
@@ -83,7 +84,7 @@ function ConfirmDialog({
83
84
  void handleConfirm();
84
85
  },
85
86
  className: cn(variant === "destructive" && buttonVariants({ variant: "destructive" })),
86
- children: pending ? "\u0110ang x\u1EED l\xFD\u2026" : confirmLabel
87
+ children: pending ? pendingLabel : confirmLabel
87
88
  }
88
89
  )
89
90
  ] })