@comitor/ui 3.13.2 → 3.13.4

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.
@@ -160,7 +160,7 @@ function Combobox({
160
160
  name,
161
161
  disabled,
162
162
  className: cn(
163
- "w-full justify-start font-normal",
163
+ "w-full justify-start text-base font-normal md:text-sm",
164
164
  showClear ? TRIGGER_PADDING_WITH_CLEAR : TRIGGER_PADDING,
165
165
  !selected && "text-muted-foreground",
166
166
  className
@@ -286,7 +286,7 @@ function MultiCombobox({
286
286
  name,
287
287
  disabled,
288
288
  className: cn(
289
- "h-auto min-h-9 w-full justify-start py-1.5 font-normal",
289
+ "h-auto min-h-9 w-full justify-start py-1.5 text-base font-normal md:text-sm",
290
290
  showClear ? TRIGGER_PADDING_WITH_CLEAR : TRIGGER_PADDING,
291
291
  value.length === 0 && "text-muted-foreground",
292
292
  className
@@ -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 { useComitorMessages } from \"../../i18n/locale-provider.js\";\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 { LetterAvatar } from \"./letter-avatar.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 /**\n * Mục là một NGƯỜI: có mặt khoá này (kể cả `null`) thì vẽ `LetterAvatar` theo `label` thay cho `icon` —\n * ảnh nếu có URL, initials nếu `null`. Không có khoá = không avatar (mục thường).\n *\n * Vì sao `null` vẫn vẽ: trong một danh sách người, người KHÔNG có ảnh cũng phải có ô avatar (initials),\n * không thì cột tên lệch lề theo việc ai có ảnh. Cùng hình dạng với `MarkdownEditorMentionItem.avatarUrl`.\n * Hiện ở dòng trong panel và cạnh nhãn trên trigger của `Combobox`.\n */\n avatarUrl?: string | null;\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 /*\n * Mọi chuỗi dưới đây: không truyền thì lấy namespace `combobox` của `ComitorLocaleProvider`\n * (mặc định tiếng Anh). Truyền prop là đè riêng chỗ này.\n */\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n /**\n * Chữ hiện trong panel khi `loading`.\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 (khoá `clearLabel` của namespace `combobox`).\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. Sai\n * ngôn ngữ ở đây là lỗi không ảnh chụp nào thấy và cũng không có gì báo đỏ: giao diện nhìn thì\n * đúng, mà ĐỌC ra lại là thứ tiếng khác.\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ự từ của một ngôn ngữ (\"Đã chọn 2/5\" ⇄ \"2/5 selected\") 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 (khoá `clearAllLabel` của namespace `combobox` — nút bỏ SẠCH danh\n * sách đang chọ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 * chuỗi này có mặt ở MỌI MultiCombobox mà app 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 {option.avatarUrl !== undefined ? (\n <LetterAvatar name={option.label} src={option.avatarUrl} size=\"xs\" aria-hidden=\"true\" className=\"shrink-0\" />\n ) : (\n Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n )}\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: placeholderProp,\n searchPlaceholder: searchPlaceholderProp,\n emptyText: emptyTextProp,\n loadingText: loadingTextProp,\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = false,\n clearLabel: clearLabelProp,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: ComboboxProps) {\n const t = useComitorMessages(\"combobox\");\n const placeholder = placeholderProp ?? t.placeholder;\n const searchPlaceholder = searchPlaceholderProp ?? t.searchPlaceholder;\n const emptyText = emptyTextProp ?? t.emptyText;\n const loadingText = loadingTextProp ?? t.loadingText;\n const clearLabel = clearLabelProp ?? t.clearLabel;\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 {selected && selected.avatarUrl !== undefined ? (\n <LetterAvatar\n name={selected.label}\n src={selected.avatarUrl}\n size=\"xs\"\n aria-hidden=\"true\"\n className=\"-my-1 shrink-0\"\n />\n ) : null}\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: placeholderProp,\n searchPlaceholder: searchPlaceholderProp,\n emptyText: emptyTextProp,\n loadingText: loadingTextProp,\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = true,\n clearLabel: clearLabelProp,\n selectedCountLabel: selectedCountLabelProp,\n maxDisplay = 2,\n maxSelected,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: MultiComboboxProps) {\n const t = useComitorMessages(\"combobox\");\n const placeholder = placeholderProp ?? t.placeholder;\n const searchPlaceholder = searchPlaceholderProp ?? t.searchPlaceholder;\n const emptyText = emptyTextProp ?? t.emptyText;\n const loadingText = loadingTextProp ?? t.loadingText;\n const clearLabel = clearLabelProp ?? t.clearAllLabel;\n const selectedCountLabel = selectedCountLabelProp ?? t.selectedCountLabel;\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":[]}
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 { useComitorMessages } from \"../../i18n/locale-provider.js\";\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 { LetterAvatar } from \"./letter-avatar.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 /**\n * Mục là một NGƯỜI: có mặt khoá này (kể cả `null`) thì vẽ `LetterAvatar` theo `label` thay cho `icon` —\n * ảnh nếu có URL, initials nếu `null`. Không có khoá = không avatar (mục thường).\n *\n * Vì sao `null` vẫn vẽ: trong một danh sách người, người KHÔNG có ảnh cũng phải có ô avatar (initials),\n * không thì cột tên lệch lề theo việc ai có ảnh. Cùng hình dạng với `MarkdownEditorMentionItem.avatarUrl`.\n * Hiện ở dòng trong panel và cạnh nhãn trên trigger của `Combobox`.\n */\n avatarUrl?: string | null;\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 /*\n * Mọi chuỗi dưới đây: không truyền thì lấy namespace `combobox` của `ComitorLocaleProvider`\n * (mặc định tiếng Anh). Truyền prop là đè riêng chỗ này.\n */\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n /**\n * Chữ hiện trong panel khi `loading`.\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 (khoá `clearLabel` của namespace `combobox`).\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. Sai\n * ngôn ngữ ở đây là lỗi không ảnh chụp nào thấy và cũng không có gì báo đỏ: giao diện nhìn thì\n * đúng, mà ĐỌC ra lại là thứ tiếng khác.\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ự từ của một ngôn ngữ (\"Đã chọn 2/5\" ⇄ \"2/5 selected\") 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 (khoá `clearAllLabel` của namespace `combobox` — nút bỏ SẠCH danh\n * sách đang chọ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 * chuỗi này có mặt ở MỌI MultiCombobox mà app 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 {option.avatarUrl !== undefined ? (\n <LetterAvatar name={option.label} src={option.avatarUrl} size=\"xs\" aria-hidden=\"true\" className=\"shrink-0\" />\n ) : (\n Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n )}\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: placeholderProp,\n searchPlaceholder: searchPlaceholderProp,\n emptyText: emptyTextProp,\n loadingText: loadingTextProp,\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = false,\n clearLabel: clearLabelProp,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: ComboboxProps) {\n const t = useComitorMessages(\"combobox\");\n const placeholder = placeholderProp ?? t.placeholder;\n const searchPlaceholder = searchPlaceholderProp ?? t.searchPlaceholder;\n const emptyText = emptyTextProp ?? t.emptyText;\n const loadingText = loadingTextProp ?? t.loadingText;\n const clearLabel = clearLabelProp ?? t.clearLabel;\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 text-base font-normal md:text-sm\",\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 {selected && selected.avatarUrl !== undefined ? (\n <LetterAvatar\n name={selected.label}\n src={selected.avatarUrl}\n size=\"xs\"\n aria-hidden=\"true\"\n className=\"-my-1 shrink-0\"\n />\n ) : null}\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: placeholderProp,\n searchPlaceholder: searchPlaceholderProp,\n emptyText: emptyTextProp,\n loadingText: loadingTextProp,\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = true,\n clearLabel: clearLabelProp,\n selectedCountLabel: selectedCountLabelProp,\n maxDisplay = 2,\n maxSelected,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: MultiComboboxProps) {\n const t = useComitorMessages(\"combobox\");\n const placeholder = placeholderProp ?? t.placeholder;\n const searchPlaceholder = searchPlaceholderProp ?? t.searchPlaceholder;\n const emptyText = emptyTextProp ?? t.emptyText;\n const loadingText = loadingTextProp ?? t.loadingText;\n const clearLabel = clearLabelProp ?? t.clearAllLabel;\n const selectedCountLabel = selectedCountLabelProp ?? t.selectedCountLabel;\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 text-base font-normal md:text-sm\",\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":";AAsJI,mBAKI,KAOF,YAZF;AApJJ,SAAS,OAAO,gBAAgB,SAAS,SAAS;AAClD,SAAyB,SAAS,gBAAgB;AAClD,SAAS,0BAA0B;AACnC,SAAS,UAAU;AACnB,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,SAAS,cAAc,cAAc,cAAc,aAAa,mBAAmB;AAC5F,SAAS,SAAS,gBAAgB,sBAAsB;AAExD,SAAS,oBAAoB;AAC7B,SAAS,6BAA6B;AAoEtC,SAAS,cAAc,OAAqD;AAC1E,SAAO,UAAU,QAAQ,UAAU;AACrC;AA+CA,SAAS,iBAAiB,OAAe,QAAgB,UAA6B;AACpF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,WAAW,sBAAsB,GAAG,KAAK,IAAI,UAAU,KAAK,GAAG,KAAK,EAAE,EAAE,EAAE,YAAY;AAC5F,SAAO,SAAS,SAAS,sBAAsB,MAAM,EAAE,YAAY,CAAC,IAAI,IAAI;AAC9E;AAGA,SAAS,aAAa,SAA0F;AAC9G,QAAM,SAAS,oBAAI,IAA8B;AACjD,aAAW,UAAU,SAAS;AAC5B,UAAM,MAAM,OAAO,SAAS;AAC5B,UAAM,SAAS,OAAO,IAAI,GAAG;AAC7B,QAAI,OAAQ,QAAO,KAAK,MAAM;AAAA,QACzB,QAAO,IAAI,KAAK,CAAC,MAAM,CAAC;AAAA,EAC/B;AACA,SAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,EAAE,OAAO,SAAS,QAAW,MAAM,EAAE;AAC7F;AAEA,SAAS,UAAU,EAAE,QAAQ,SAAS,GAAkD;AACtF,QAAM,OAAO,OAAO;AACpB,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAW,GAAG,oDAAoD,CAAC,YAAY,WAAW;AAAA,QAE1F,8BAAC,SAAM,WAAU,UAAS;AAAA;AAAA,IAC5B;AAAA,IACC,OAAO,cAAc,SACpB,oBAAC,gBAAa,MAAM,OAAO,OAAO,KAAK,OAAO,WAAW,MAAK,MAAK,eAAY,QAAO,WAAU,YAAW,IAE3G,QAAQ,oBAAC,QAAK,eAAY,QAAO,WAAU,yCAAwC;AAAA,IAErF,qBAAC,UAAK,WAAU,kBACd;AAAA,0BAAC,UAAK,WAAU,kBAAkB,iBAAO,OAAM;AAAA,MAC9C,OAAO,eACN,oBAAC,UAAK,WAAU,gDAAgD,iBAAO,aAAY;AAAA,OAEvF;AAAA,KACF;AAEJ;AAqBA,SAAS,mBAAmB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT;AAAA,QACA,YAAY;AAAA,MACd;AAAA,MAEC;AAAA,qBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS;AAAA,YAET;AAAA,kCAAC,KAAE,eAAY,QAAO,WAAU,YAAW;AAAA,cAC3C,oBAAC,UAAK,WAAU,WAAW,sBAAW;AAAA;AAAA;AAAA,QACxC;AAAA,QAOF,oBAAC,kBAAe,eAAY,QAAO,WAAU,yCAAwC;AAAA;AAAA;AAAA,EACvF;AAEJ;AAWA,MAAM,kBAAkB;AACxB,MAAM,6BAA6B;AAEnC,SAAS,gBAAgB,SAA2B,SAAkB;AACpE,QAAM,SAAS,QAAQ,MAAM,aAAa,OAAO,GAAG,CAAC,OAAO,CAAC;AAC7D,SAAO,EAAE,QAAQ,cAAc,CAAC,QAAQ;AAC1C;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AACD,QAAM,EAAE,QAAQ,aAAa,IAAI,gBAAgB,SAAS,OAAO;AAEjE,SACE,qBAAC,WAAQ,cAA4B,QAAQ,kBAC3C;AAAA,wBAAC,gBAAa,aAAa,mBAAmB,OAAO,QAAQ,eAAe,UAAU;AAAA,IACtF,oBAAC,eACE,oBACC,qBAAC,SAAI,WAAU,6EACb;AAAA,0BAAC,WAAQ,eAAY,QAAO,WAAU,uBAAsB;AAAA,MAC3D;AAAA,OACH,IAEA,iCACE;AAAA,0BAAC,gBAAc,qBAAU;AAAA,MACxB,OAAO,IAAI,CAAC,UACX,oBAAC,gBAAgD,SAAS,MAAM,OAC7D,gBAAM,MAAM,IAAI,UAAU,KADV,MAAM,SAAS,aAElC,CACD;AAAA,OACH,GAEJ;AAAA,KACF;AAEJ;AAQO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,WAAW;AAAA,EACX;AAAA,EACA,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,IAAI,mBAAmB,UAAU;AACvC,QAAM,cAAc,mBAAmB,EAAE;AACzC,QAAM,oBAAoB,yBAAyB,EAAE;AACrD,QAAM,YAAY,iBAAiB,EAAE;AACrC,QAAM,cAAc,mBAAmB,EAAE;AACzC,QAAM,aAAa,kBAAkB,EAAE;AACvC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,UAAU,mBAAmB;AACnC,QAAM,WAAW,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AAChE,QAAM,UAAU,cAAc,KAAK,cAAc,CAAC;AAElD,QAAM,eAAe,CAAC,SAAiB;AACrC,cAAU,IAAI;AACd,qBAAiB,IAAI;AAAA,EACvB;AAEA,QAAM,YAAY,aAAa,QAAQ,QAAQ,KAAK,CAAC;AAErD,SACE,qBAAC,SAAI,WAAU,mBACb;AAAA,yBAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,0BAAC,kBAAe,SAAO,MACrB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,iBAAe;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA,YAAY,6BAA6B;AAAA,YACzC,CAAC,YAAY;AAAA,YACb;AAAA,UACF;AAAA,UACC,GAAG;AAAA,UAGJ,gBAAc,WAAW;AAAA,UAExB;AAAA,wBAAY,SAAS,cAAc,SAClC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM,SAAS;AAAA,gBACf,KAAK,SAAS;AAAA,gBACd,MAAK;AAAA,gBACL,eAAY;AAAA,gBACZ,WAAU;AAAA;AAAA,YACZ,IACE;AAAA,YACJ,oBAAC,UAAK,WAAU,YAAY,qBAAW,SAAS,QAAQ,aAAY;AAAA;AAAA;AAAA,MACtE,GACF;AAAA,MACA,oBAAC,kBAAe,OAAM,SAAQ,WAAW,GAAG,yCAAyC,gBAAgB,GACnG;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA,YAAY,CAAC,WACX;AAAA,YAAC;AAAA;AAAA,cAEC,OAAO,OAAO;AAAA,cACd,UAAU,CAAC,OAAO,OAAO,OAAO,eAAe,IAAI,GAAI,OAAO,YAAY,CAAC,CAAE;AAAA,cAC7E,UAAU,OAAO;AAAA,cACjB,UAAU,MAAM;AACd,8BAAc,OAAO,UAAU,QAAQ,OAAO,OAAO,KAAK;AAC1D,wBAAQ,KAAK;AACb,0BAAU,EAAE;AACZ,oBAAI,QAAS,kBAAiB,EAAE;AAAA,cAClC;AAAA,cAEA,8BAAC,aAAU,QAAgB,UAAU,OAAO,UAAU,OAAO;AAAA;AAAA,YAXxD,OAAO;AAAA,UAYd;AAAA;AAAA,MAEJ,GACF;AAAA,OACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,MAAM,cAAc,IAAI;AAAA;AAAA,IACnC;AAAA,KACF;AAEJ;AAQO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,WAAW;AAAA,EACX;AAAA,EACA,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuB;AACrB,QAAM,IAAI,mBAAmB,UAAU;AACvC,QAAM,cAAc,mBAAmB,EAAE;AACzC,QAAM,oBAAoB,yBAAyB,EAAE;AACrD,QAAM,YAAY,iBAAiB,EAAE;AACrC,QAAM,cAAc,mBAAmB,EAAE;AACzC,QAAM,aAAa,kBAAkB,EAAE;AACvC,QAAM,qBAAqB,0BAA0B,EAAE;AACvD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,UAAU,mBAAmB;AACnC,QAAM,UAAU,cAAc,KAAK,cAAc,CAAC;AAElD,QAAM,kBAAkB,QAAQ,OAAO,CAAC,WAAW,MAAM,SAAS,OAAO,KAAK,CAAC;AAC/E,QAAM,QAAQ,gBAAgB,MAAM,GAAG,KAAK,IAAI,GAAG,UAAU,CAAC;AAC9D,QAAM,WAAW,gBAAgB,SAAS,MAAM;AAChD,QAAM,eAAe,gBAAgB,UAAa,MAAM,UAAU;AAElE,QAAM,SAAS,CAAC,gBAAwB;AACtC,QAAI,MAAM,SAAS,WAAW,GAAG;AAC/B,oBAAc,MAAM,OAAO,CAAC,SAAS,SAAS,WAAW,CAAC;AAC1D;AAAA,IACF;AACA,QAAI,aAAc;AAClB,kBAAc,CAAC,GAAG,OAAO,WAAW,CAAC;AAAA,EACvC;AAEA,QAAM,eAAe,CAAC,SAAiB;AACrC,cAAU,IAAI;AACd,qBAAiB,IAAI;AAAA,EACvB;AAEA,QAAM,YAAY,aAAa,MAAM,SAAS,KAAK,CAAC;AAEpD,SACE,qBAAC,SAAI,WAAU,mBACb;AAAA,yBAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,0BAAC,kBAAe,SAAO,MACrB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,iBAAe;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA,YAAY,6BAA6B;AAAA,YACzC,MAAM,WAAW,KAAK;AAAA,YACtB;AAAA,UACF;AAAA,UACC,GAAG;AAAA,UAEJ,gBAAc,WAAW;AAAA,UAEzB,8BAAC,UAAK,WAAU,6CACb,gBAAM,WAAW,IAChB,cAEA,iCACG;AAAA,kBAAM,IAAI,CAAC,WACV,oBAAC,SAAyB,SAAQ,aAAY,WAAU,iBACtD,8BAAC,UAAK,WAAU,YAAY,iBAAO,OAAM,KAD/B,OAAO,KAEnB,CACD;AAAA,YACA,WAAW,KAAK,qBAAC,SAAM,SAAQ,WAAU;AAAA;AAAA,cAAE;AAAA,eAAS;AAAA,aACvD,GAEJ;AAAA;AAAA,MACF,GACF;AAAA,MACA,qBAAC,kBAAe,OAAM,SAAQ,WAAW,GAAG,yCAAyC,gBAAgB,GACnG;AAAA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,UAAU;AAAA,YACV;AAAA,YACA;AAAA,YACA;AAAA,YACA,YAAY,CAAC,WAAW;AACtB,oBAAM,aAAa,MAAM,SAAS,OAAO,KAAK;AAC9C,qBACE;AAAA,gBAAC;AAAA;AAAA,kBAEC,OAAO,OAAO;AAAA,kBACd,UAAU,CAAC,OAAO,OAAO,OAAO,eAAe,IAAI,GAAI,OAAO,YAAY,CAAC,CAAE;AAAA,kBAC7E,UAAU,OAAO,YAAa,gBAAgB,CAAC;AAAA,kBAC/C,UAAU,MAAM,OAAO,OAAO,KAAK;AAAA,kBAEnC,8BAAC,aAAU,QAAgB,UAAU,YAAY;AAAA;AAAA,gBAN5C,OAAO;AAAA,cAOd;AAAA,YAEJ;AAAA;AAAA,QACF;AAAA,QACC,gBAAgB,UACf,oBAAC,OAAE,WAAU,kEACV,6BAAmB,MAAM,QAAQ,WAAW,GAC/C;AAAA,SAEJ;AAAA,OACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,MAAM,cAAc,CAAC,CAAC;AAAA;AAAA,IACjC;AAAA,KACF;AAEJ;","names":[]}
@@ -24,7 +24,7 @@ function PaletteInput({
24
24
  onValueChange,
25
25
  placeholder,
26
26
  "aria-label": placeholder,
27
- className: "flex h-11 w-full bg-transparent text-sm outline-hidden placeholder:text-muted-foreground"
27
+ className: "flex h-11 w-full bg-transparent text-base md:text-sm outline-hidden placeholder:text-muted-foreground"
28
28
  }
29
29
  ),
30
30
  /* @__PURE__ */ jsx(KeyboardHint, { keys: "esc", className: "hidden sm:inline-flex" })
@@ -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 { useComitorLabels } from \"../../i18n/locale-provider.js\";\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 } 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 /*\n * Ba chuỗi dưới đây: không truyền thì lấy namespace `commandPalette` của `ComitorLocaleProvider`\n * (mặc định tiếng Anh). Truyền prop là đè riêng chỗ này.\n */\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 theo provider) — 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.\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: recentLabelProp,\n placeholder: placeholderProp,\n emptyText: emptyTextProp,\n onSearchChange,\n loading = false,\n footer,\n registerShortcut = true,\n labels: labelsProp,\n className\n}: CommandPaletteProps) {\n // Cả hai đường chữ đọc CHUNG một namespace: `labels` (hai chuỗi sr-only) trộn lên bảng, còn ba\n // prop rời thì lui về đúng khoá cùng tên của bảng đã trộn đó.\n const labels = useComitorLabels(\"commandPalette\", labelsProp);\n const recentLabel = recentLabelProp ?? labels.recentLabel;\n const placeholder = placeholderProp ?? labels.placeholder;\n const emptyText = emptyTextProp ?? labels.emptyText;\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":[]}
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 { useComitorLabels } from \"../../i18n/locale-provider.js\";\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 } 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 /*\n * Ba chuỗi dưới đây: không truyền thì lấy namespace `commandPalette` của `ComitorLocaleProvider`\n * (mặc định tiếng Anh). Truyền prop là đè riêng chỗ này.\n */\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 theo provider) — 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-base md: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.\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: recentLabelProp,\n placeholder: placeholderProp,\n emptyText: emptyTextProp,\n onSearchChange,\n loading = false,\n footer,\n registerShortcut = true,\n labels: labelsProp,\n className\n}: CommandPaletteProps) {\n // Cả hai đường chữ đọc CHUNG một namespace: `labels` (hai chuỗi sr-only) trộn lên bảng, còn ba\n // prop rời thì lui về đúng khoá cùng tên của bảng đã trộn đó.\n const labels = useComitorLabels(\"commandPalette\", labelsProp);\n const recentLabel = recentLabelProp ?? labels.recentLabel;\n const placeholder = placeholderProp ?? labels.placeholder;\n const emptyText = emptyTextProp ?? labels.emptyText;\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":[]}
@@ -221,7 +221,7 @@ function DateRangePicker({
221
221
  // `px-3` chứ không phải `px-4` của `Button`: đây là một Ô NHẬP nằm cùng cột với
222
222
  // `Input`/`Select`/`Combobox`, và cả ba đều thụt 12px. `pr-10` = 12 + 24 (nút xóa)
223
223
  // + 4, chỉ cần khi nút xóa có mặt.
224
- "w-full justify-start gap-2 px-3 font-normal",
224
+ "w-full justify-start gap-2 px-3 text-base font-normal md:text-sm",
225
225
  !label && "text-muted-foreground",
226
226
  showClear && "pr-10"
227
227
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/date-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport { format as formatDate, isAfter, isBefore, isValid, type Locale, parse, startOfDay } from \"date-fns\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\nimport { useComitorLabels, useComitorLocale } from \"../../i18n/locale-provider.js\";\nimport type { DatePresetMessages } from \"../../i18n/namespaces/date-picker.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Calendar } from \"../ui/calendar.js\";\nimport { Input } from \"../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport {\n type DatePickerLabels,\n type DatePreset,\n type DateRangePreset,\n DEFAULT_DATE_PRESETS,\n DEFAULT_DATE_RANGE_PRESETS,\n dateFormatForLocale\n} from \"./date-picker-constants.js\";\n\nexport type { DateRange };\n\n/** `id` của preset mặc định → khoá nhãn trong namespace `datePicker.presets`. */\nconst PRESET_MESSAGE_KEY_BY_ID = {\n today: \"today\",\n yesterday: \"yesterday\",\n \"week-ago\": \"weekAgo\",\n \"last-7\": \"last7Days\",\n \"last-30\": \"last30Days\",\n \"this-month\": \"thisMonth\",\n \"last-month\": \"lastMonth\"\n} satisfies Record<string, keyof DatePresetMessages>;\n\n/**\n * CHÍNH các object preset của gói → khoá nhãn của chúng. Nhận ra bằng THAM CHIẾU, không bằng `id`:\n * · preset app tự dựng hiện đúng `label` app đưa, kể cả khi trùng `id` (\"today\") với mặc định;\n * · app truyền lại object của gói — `presets={DEFAULT_DATE_PRESETS}` (cách DUY NHẤT để\n * `DatePicker` có preset), hay một mảng `.filter()` từ đó — thì nhãn vẫn theo provider thay vì\n * kẹt ở `label` tiếng Anh của hằng.\n */\nconst DEFAULT_PRESET_MESSAGE_KEYS = new Map<DatePreset | DateRangePreset, keyof DatePresetMessages>(\n [...DEFAULT_DATE_PRESETS, ...DEFAULT_DATE_RANGE_PRESETS].flatMap((preset) => {\n const key = PRESET_MESSAGE_KEY_BY_ID[preset.id as keyof typeof PRESET_MESSAGE_KEY_BY_ID];\n return key ? [[preset, key] as const] : [];\n })\n);\n\n/** Nhãn hiện ra của một preset: preset của gói theo provider, preset của app nguyên văn. */\nfunction presetLabel(preset: DatePreset | DateRangePreset, messages: DatePresetMessages): string {\n const key = DEFAULT_PRESET_MESSAGE_KEYS.get(preset);\n return key ? messages[key] : preset.label;\n}\n\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n if (minDate && isBefore(date, startOfDay(minDate))) return true;\n if (maxDate && isAfter(date, startOfDay(maxDate))) return true;\n return false;\n}\n\n/**\n * Chặn ngày ngoài khoảng cho phép — trả về MẢNG matcher, không phải một object.\n *\n * Cạm bẫy react-day-picker: `{ before, after }` trong CÙNG một object là `DateInterval`, nghĩa là\n * \"các ngày NẰM GIỮA\" — ngược hẳn ý định. Tách thành hai matcher riêng mới ra \"trước min HOẶC sau max\".\n */\nfunction buildDisabledMatchers(minDate?: Date, maxDate?: Date): Matcher[] | undefined {\n const matchers: Matcher[] = [];\n if (minDate) matchers.push({ before: startOfDay(minDate) });\n if (maxDate) matchers.push({ after: startOfDay(maxDate) });\n return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n value?: Date | null;\n onValueChange: (value: Date | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Ngày nhỏ nhất / lớn nhất được chọn */\n minDate?: Date;\n maxDate?: Date;\n /** Hiện nút xoá khi đã có giá trị (mặc định true) */\n clearable?: boolean;\n /**\n * Cột preset bên trái lịch (mặc định ẩn). `DEFAULT_DATE_PRESETS` hiện nhãn theo\n * `ComitorLocaleProvider`; preset app tự dựng hiện nguyên `label`.\n */\n presets?: DatePreset[] | false;\n /**\n * Ngôn ngữ của date-fns — quyết định tên thứ/tháng trong lịch VÀ thứ tự ngày/tháng của ô nhập\n * (gõ, hiển thị lẫn placeholder), qua `dateFormatForLocale()`.\n *\n * Không truyền thì theo `ComitorLocaleProvider` (`vi` → `dd/MM/yyyy`, còn lại `enUS` →\n * `MM/dd/yyyy`). Trước `3.0.0` mặc định cứng là `vi`, nên app tiếng Anh phải nhớ truyền.\n */\n locale?: Locale;\n /**\n * Ghi đè định dạng gõ/hiển thị. HIẾM khi cần: bỏ trống thì nó suy từ `locale`, và đó gần như\n * luôn là thứ đúng. Chỉ dùng khi một màn hình phải ghim cứng một định dạng bất kể ngôn ngữ.\n */\n dateFormat?: string;\n /** Đè nhãn `sr-only` — không truyền thì theo namespace `datePicker` của `ComitorLocaleProvider` */\n labels?: Partial<DatePickerLabels>;\n className?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\n/**\n * Ô chọn ngày: gõ tay theo định dạng của `locale` (`dd/MM/yyyy`, `MM/dd/yyyy`…) HOẶC bấm lịch.\n *\n * Vì sao vẫn giữ ô gõ tay thay vì chỉ có nút mở lịch: người nhập liệu chuyên nghiệp (kế toán,\n * nhân sự) gõ nhanh hơn bấm nhiều lần, và ngày sinh năm 1975 mà bắt lật lịch 50 năm thì không\n * ai chịu nổi. Chuỗi gõ dở KHÔNG bị ghi đè trong lúc gõ; sai định dạng thì trả về giá trị cũ\n * khi rời ô, không âm thầm nuốt dữ liệu.\n */\nexport function DatePicker({\n value,\n onValueChange,\n placeholder,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n locale: localeProp,\n dateFormat,\n labels,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const { dateFnsLocale } = useComitorLocale();\n const locale = localeProp ?? dateFnsLocale;\n const pattern = dateFormat ?? dateFormatForLocale(locale);\n const label = useComitorLabels(\"datePicker\", labels);\n\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, pattern, { locale }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về) — VÀ khi đổi\n // ngôn ngữ giữa chừng: chuỗi đang hiện là `dd/MM/yyyy` mà ô nay chờ `MM/dd/yyyy` thì lần rời ô\n // kế tiếp sẽ đọc sai ngày.\n useEffect(() => {\n setText(value ? formatDate(value, pattern, { locale }) : \"\");\n }, [value, pattern, locale]);\n\n const commitText = (raw: string) => {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n onValueChange(null);\n return;\n }\n const parsed = parse(trimmed, pattern, new Date(), { locale });\n if (!isValid(parsed) || isOutOfRange(parsed, minDate, maxDate)) {\n // Không hợp lệ → khôi phục hiển thị theo giá trị đang có (không đổi dữ liệu).\n setText(value ? formatDate(value, pattern, { locale }) : \"\");\n return;\n }\n onValueChange(startOfDay(parsed));\n };\n\n const presetList = presets === false ? [] : presets;\n\n return (\n <div className={cn(\"relative\", className)}>\n <Input\n id={id}\n name={name}\n value={text}\n disabled={disabled}\n placeholder={placeholder ?? pattern}\n inputMode=\"numeric\"\n autoComplete=\"off\"\n onChange={(event) => setText(event.target.value)}\n onBlur={(event) => commitText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commitText(text);\n }\n }}\n className=\"pr-16\"\n {...aria}\n />\n\n {/* Nút `size-6`: glyph 16px nằm giữa ô 24px nên nó thụt thêm 4px so với mép ô — cộng với\n `pr-2` của hàng là glyph rơi đúng mốc 12px của `Select`/`Combobox`. Ba loại control này\n hay đứng chung một cột biểu mẫu, lệch vài px là mép phải nhìn răng cưa.\n\n `border border-transparent`: hàng nằm ngoài `Input` nên đo từ mép BORDER, còn icon nằm\n trong ô thì đo từ mép CONTENT — mượn lại viền 1px là hai mốc trùng nhau.\n\n Icon lấy `text-muted-foreground`: `Button variant=\"ghost\"` mặc định là `text-foreground`,\n đậm hơn hẳn chevron của `Select` (vốn `text-muted-foreground`). Chúng là cùng một hạng\n trang trí của ô nhập thì phải cùng một sắc. */}\n {/* `pointer-events-none` ở hàng, `pointer-events-auto` ở từng nút: phần đệm của hàng phủ lên\n dải sát mép phải của ô nhập, để nguyên là bấm vào đó không đặt được con trỏ vào ô. */}\n <div className=\"pointer-events-none absolute inset-y-0 right-0 flex items-center border border-transparent pr-2\">\n {clearable && value && !disabled && (\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={() => onValueChange(null)}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{label.clear}</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\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 disabled={disabled}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{label.openCalendar}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"flex w-auto gap-2 p-2\">\n {presetList.length > 0 && (\n <div className=\"flex w-32 flex-col gap-1 border-r border-border pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {presetLabel(preset, label.presets)}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={locale}\n autoFocus\n selected={value ?? undefined}\n defaultMonth={value ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(date) => {\n onValueChange(date ? startOfDay(date) : null);\n setOpen(false);\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n );\n}\n\nexport interface DateRangePickerProps {\n value?: DateRange | null;\n onValueChange: (value: DateRange | null) => void;\n /** Chữ khi chưa chọn — không truyền thì theo khoá `rangePlaceholder` của namespace `datePicker` */\n placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /**\n * Cột preset bên trái (mặc định `DEFAULT_DATE_RANGE_PRESETS`, nhãn theo `ComitorLocaleProvider`;\n * `false` để tắt). Preset app tự dựng hiện nguyên `label`.\n */\n presets?: DateRangePreset[] | false;\n /** Số tháng hiện cùng lúc (mặc định 2 trên màn rộng) */\n numberOfMonths?: number;\n /** Ngôn ngữ date-fns (mặc định theo `ComitorLocaleProvider`) — xem `DatePickerProps.locale`. */\n locale?: Locale;\n /** Ghi đè định dạng ngày; bỏ trống thì suy từ `locale`. */\n dateFormat?: string;\n /** Đè nhãn `sr-only` — không truyền thì theo namespace `datePicker` của `ComitorLocaleProvider` */\n labels?: Partial<DatePickerLabels>;\n /**\n * Áp lên KHUNG ngoài (như `DatePicker`), nút bên trong luôn `w-full`. Trước 3.13.2 nó tới NÚT, nên\n * `className=\"w-60\"` không thu được khung `w-full` và ô chiếm trọn hàng flex, đẩy ô bên cạnh xuống dòng.\n */\n className?: string;\n id?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n}\n\nfunction formatRange(range: DateRange | null | undefined, pattern: string, locale: Locale): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, pattern, { locale });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, pattern, { locale })}`;\n}\n\n/**\n * Ô chọn khoảng ngày cho báo cáo và bộ lọc danh sách: cột preset + lịch 2 tháng.\n *\n * Preset đứng trước lịch vì 9/10 lần người dùng muốn \"7 ngày qua\" chứ không phải một khoảng\n * tuỳ ý — bắt họ bấm hai lần trên lịch mỗi lần xem báo cáo là thuế lặp lại vô ích.\n */\nexport function DateRangePicker({\n value,\n onValueChange,\n placeholder: placeholderProp,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n locale: localeProp,\n dateFormat,\n labels,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const { dateFnsLocale } = useComitorLocale();\n const locale = localeProp ?? dateFnsLocale;\n const pattern = dateFormat ?? dateFormatForLocale(locale);\n const text = useComitorLabels(\"datePicker\", labels);\n const placeholder = placeholderProp ?? text.rangePlaceholder;\n\n const [open, setOpen] = useState(false);\n const label = formatRange(value, pattern, locale);\n const presetList = presets === false ? [] : presets;\n\n const showClear = clearable && Boolean(label) && !disabled;\n\n return (\n /* Nút xóa nằm NGOÀI trigger (anh em), không lồng trong <button> — vừa hợp lệ về HTML vừa\n đến được bằng phím Tab. */\n <div className={cn(\"relative w-full\", className)}>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n id={id}\n variant=\"outline\"\n disabled={disabled}\n className={cn(\n // `px-3` chứ không phải `px-4` của `Button`: đây là một Ô NHẬP nằm cùng cột với\n // `Input`/`Select`/`Combobox`, và cả ba đều thụt 12px. `pr-10` = 12 + 24 (nút xóa)\n // + 4, chỉ cần khi nút xóa có mặt.\n \"w-full justify-start gap-2 px-3 font-normal\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-10\"\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{label ?? placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"flex w-auto flex-col gap-2 p-2 sm:flex-row\">\n {presetList.length > 0 && (\n <div className=\"flex flex-row flex-wrap gap-1 sm:w-36 sm:flex-col sm:border-r sm:border-border sm:pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {presetLabel(preset, text.presets)}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={locale}\n autoFocus\n numberOfMonths={numberOfMonths}\n selected={value ?? undefined}\n defaultMonth={value?.from ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(range) => onValueChange(range ?? null)}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n // Cùng hình dạng với nút xóa của `DatePicker`: ô 24px ở `right-2`. Là `<Button>` thật nên\n // có viền focus và vùng bấm đạt 24px — bản `<button>` thô trước đây chỉ 18px.\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"absolute right-2 top-1/2 size-6 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n onClick={() => onValueChange(null)}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{text.clearRange}</span>\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/date-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport { format as formatDate, isAfter, isBefore, isValid, type Locale, parse, startOfDay } from \"date-fns\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\nimport { useComitorLabels, useComitorLocale } from \"../../i18n/locale-provider.js\";\nimport type { DatePresetMessages } from \"../../i18n/namespaces/date-picker.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Calendar } from \"../ui/calendar.js\";\nimport { Input } from \"../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport {\n type DatePickerLabels,\n type DatePreset,\n type DateRangePreset,\n DEFAULT_DATE_PRESETS,\n DEFAULT_DATE_RANGE_PRESETS,\n dateFormatForLocale\n} from \"./date-picker-constants.js\";\n\nexport type { DateRange };\n\n/** `id` của preset mặc định → khoá nhãn trong namespace `datePicker.presets`. */\nconst PRESET_MESSAGE_KEY_BY_ID = {\n today: \"today\",\n yesterday: \"yesterday\",\n \"week-ago\": \"weekAgo\",\n \"last-7\": \"last7Days\",\n \"last-30\": \"last30Days\",\n \"this-month\": \"thisMonth\",\n \"last-month\": \"lastMonth\"\n} satisfies Record<string, keyof DatePresetMessages>;\n\n/**\n * CHÍNH các object preset của gói → khoá nhãn của chúng. Nhận ra bằng THAM CHIẾU, không bằng `id`:\n * · preset app tự dựng hiện đúng `label` app đưa, kể cả khi trùng `id` (\"today\") với mặc định;\n * · app truyền lại object của gói — `presets={DEFAULT_DATE_PRESETS}` (cách DUY NHẤT để\n * `DatePicker` có preset), hay một mảng `.filter()` từ đó — thì nhãn vẫn theo provider thay vì\n * kẹt ở `label` tiếng Anh của hằng.\n */\nconst DEFAULT_PRESET_MESSAGE_KEYS = new Map<DatePreset | DateRangePreset, keyof DatePresetMessages>(\n [...DEFAULT_DATE_PRESETS, ...DEFAULT_DATE_RANGE_PRESETS].flatMap((preset) => {\n const key = PRESET_MESSAGE_KEY_BY_ID[preset.id as keyof typeof PRESET_MESSAGE_KEY_BY_ID];\n return key ? [[preset, key] as const] : [];\n })\n);\n\n/** Nhãn hiện ra của một preset: preset của gói theo provider, preset của app nguyên văn. */\nfunction presetLabel(preset: DatePreset | DateRangePreset, messages: DatePresetMessages): string {\n const key = DEFAULT_PRESET_MESSAGE_KEYS.get(preset);\n return key ? messages[key] : preset.label;\n}\n\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n if (minDate && isBefore(date, startOfDay(minDate))) return true;\n if (maxDate && isAfter(date, startOfDay(maxDate))) return true;\n return false;\n}\n\n/**\n * Chặn ngày ngoài khoảng cho phép — trả về MẢNG matcher, không phải một object.\n *\n * Cạm bẫy react-day-picker: `{ before, after }` trong CÙNG một object là `DateInterval`, nghĩa là\n * \"các ngày NẰM GIỮA\" — ngược hẳn ý định. Tách thành hai matcher riêng mới ra \"trước min HOẶC sau max\".\n */\nfunction buildDisabledMatchers(minDate?: Date, maxDate?: Date): Matcher[] | undefined {\n const matchers: Matcher[] = [];\n if (minDate) matchers.push({ before: startOfDay(minDate) });\n if (maxDate) matchers.push({ after: startOfDay(maxDate) });\n return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n value?: Date | null;\n onValueChange: (value: Date | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Ngày nhỏ nhất / lớn nhất được chọn */\n minDate?: Date;\n maxDate?: Date;\n /** Hiện nút xoá khi đã có giá trị (mặc định true) */\n clearable?: boolean;\n /**\n * Cột preset bên trái lịch (mặc định ẩn). `DEFAULT_DATE_PRESETS` hiện nhãn theo\n * `ComitorLocaleProvider`; preset app tự dựng hiện nguyên `label`.\n */\n presets?: DatePreset[] | false;\n /**\n * Ngôn ngữ của date-fns — quyết định tên thứ/tháng trong lịch VÀ thứ tự ngày/tháng của ô nhập\n * (gõ, hiển thị lẫn placeholder), qua `dateFormatForLocale()`.\n *\n * Không truyền thì theo `ComitorLocaleProvider` (`vi` → `dd/MM/yyyy`, còn lại `enUS` →\n * `MM/dd/yyyy`). Trước `3.0.0` mặc định cứng là `vi`, nên app tiếng Anh phải nhớ truyền.\n */\n locale?: Locale;\n /**\n * Ghi đè định dạng gõ/hiển thị. HIẾM khi cần: bỏ trống thì nó suy từ `locale`, và đó gần như\n * luôn là thứ đúng. Chỉ dùng khi một màn hình phải ghim cứng một định dạng bất kể ngôn ngữ.\n */\n dateFormat?: string;\n /** Đè nhãn `sr-only` — không truyền thì theo namespace `datePicker` của `ComitorLocaleProvider` */\n labels?: Partial<DatePickerLabels>;\n className?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\n/**\n * Ô chọn ngày: gõ tay theo định dạng của `locale` (`dd/MM/yyyy`, `MM/dd/yyyy`…) HOẶC bấm lịch.\n *\n * Vì sao vẫn giữ ô gõ tay thay vì chỉ có nút mở lịch: người nhập liệu chuyên nghiệp (kế toán,\n * nhân sự) gõ nhanh hơn bấm nhiều lần, và ngày sinh năm 1975 mà bắt lật lịch 50 năm thì không\n * ai chịu nổi. Chuỗi gõ dở KHÔNG bị ghi đè trong lúc gõ; sai định dạng thì trả về giá trị cũ\n * khi rời ô, không âm thầm nuốt dữ liệu.\n */\nexport function DatePicker({\n value,\n onValueChange,\n placeholder,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n locale: localeProp,\n dateFormat,\n labels,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const { dateFnsLocale } = useComitorLocale();\n const locale = localeProp ?? dateFnsLocale;\n const pattern = dateFormat ?? dateFormatForLocale(locale);\n const label = useComitorLabels(\"datePicker\", labels);\n\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, pattern, { locale }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về) — VÀ khi đổi\n // ngôn ngữ giữa chừng: chuỗi đang hiện là `dd/MM/yyyy` mà ô nay chờ `MM/dd/yyyy` thì lần rời ô\n // kế tiếp sẽ đọc sai ngày.\n useEffect(() => {\n setText(value ? formatDate(value, pattern, { locale }) : \"\");\n }, [value, pattern, locale]);\n\n const commitText = (raw: string) => {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n onValueChange(null);\n return;\n }\n const parsed = parse(trimmed, pattern, new Date(), { locale });\n if (!isValid(parsed) || isOutOfRange(parsed, minDate, maxDate)) {\n // Không hợp lệ → khôi phục hiển thị theo giá trị đang có (không đổi dữ liệu).\n setText(value ? formatDate(value, pattern, { locale }) : \"\");\n return;\n }\n onValueChange(startOfDay(parsed));\n };\n\n const presetList = presets === false ? [] : presets;\n\n return (\n <div className={cn(\"relative\", className)}>\n <Input\n id={id}\n name={name}\n value={text}\n disabled={disabled}\n placeholder={placeholder ?? pattern}\n inputMode=\"numeric\"\n autoComplete=\"off\"\n onChange={(event) => setText(event.target.value)}\n onBlur={(event) => commitText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commitText(text);\n }\n }}\n className=\"pr-16\"\n {...aria}\n />\n\n {/* Nút `size-6`: glyph 16px nằm giữa ô 24px nên nó thụt thêm 4px so với mép ô — cộng với\n `pr-2` của hàng là glyph rơi đúng mốc 12px của `Select`/`Combobox`. Ba loại control này\n hay đứng chung một cột biểu mẫu, lệch vài px là mép phải nhìn răng cưa.\n\n `border border-transparent`: hàng nằm ngoài `Input` nên đo từ mép BORDER, còn icon nằm\n trong ô thì đo từ mép CONTENT — mượn lại viền 1px là hai mốc trùng nhau.\n\n Icon lấy `text-muted-foreground`: `Button variant=\"ghost\"` mặc định là `text-foreground`,\n đậm hơn hẳn chevron của `Select` (vốn `text-muted-foreground`). Chúng là cùng một hạng\n trang trí của ô nhập thì phải cùng một sắc. */}\n {/* `pointer-events-none` ở hàng, `pointer-events-auto` ở từng nút: phần đệm của hàng phủ lên\n dải sát mép phải của ô nhập, để nguyên là bấm vào đó không đặt được con trỏ vào ô. */}\n <div className=\"pointer-events-none absolute inset-y-0 right-0 flex items-center border border-transparent pr-2\">\n {clearable && value && !disabled && (\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={() => onValueChange(null)}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{label.clear}</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\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 disabled={disabled}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">{label.openCalendar}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"flex w-auto gap-2 p-2\">\n {presetList.length > 0 && (\n <div className=\"flex w-32 flex-col gap-1 border-r border-border pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {presetLabel(preset, label.presets)}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={locale}\n autoFocus\n selected={value ?? undefined}\n defaultMonth={value ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(date) => {\n onValueChange(date ? startOfDay(date) : null);\n setOpen(false);\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n );\n}\n\nexport interface DateRangePickerProps {\n value?: DateRange | null;\n onValueChange: (value: DateRange | null) => void;\n /** Chữ khi chưa chọn — không truyền thì theo khoá `rangePlaceholder` của namespace `datePicker` */\n placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /**\n * Cột preset bên trái (mặc định `DEFAULT_DATE_RANGE_PRESETS`, nhãn theo `ComitorLocaleProvider`;\n * `false` để tắt). Preset app tự dựng hiện nguyên `label`.\n */\n presets?: DateRangePreset[] | false;\n /** Số tháng hiện cùng lúc (mặc định 2 trên màn rộng) */\n numberOfMonths?: number;\n /** Ngôn ngữ date-fns (mặc định theo `ComitorLocaleProvider`) — xem `DatePickerProps.locale`. */\n locale?: Locale;\n /** Ghi đè định dạng ngày; bỏ trống thì suy từ `locale`. */\n dateFormat?: string;\n /** Đè nhãn `sr-only` — không truyền thì theo namespace `datePicker` của `ComitorLocaleProvider` */\n labels?: Partial<DatePickerLabels>;\n /**\n * Áp lên KHUNG ngoài (như `DatePicker`), nút bên trong luôn `w-full`. Trước 3.13.2 nó tới NÚT, nên\n * `className=\"w-60\"` không thu được khung `w-full` và ô chiếm trọn hàng flex, đẩy ô bên cạnh xuống dòng.\n */\n className?: string;\n id?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n}\n\nfunction formatRange(range: DateRange | null | undefined, pattern: string, locale: Locale): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, pattern, { locale });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, pattern, { locale })}`;\n}\n\n/**\n * Ô chọn khoảng ngày cho báo cáo và bộ lọc danh sách: cột preset + lịch 2 tháng.\n *\n * Preset đứng trước lịch vì 9/10 lần người dùng muốn \"7 ngày qua\" chứ không phải một khoảng\n * tuỳ ý — bắt họ bấm hai lần trên lịch mỗi lần xem báo cáo là thuế lặp lại vô ích.\n */\nexport function DateRangePicker({\n value,\n onValueChange,\n placeholder: placeholderProp,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n locale: localeProp,\n dateFormat,\n labels,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const { dateFnsLocale } = useComitorLocale();\n const locale = localeProp ?? dateFnsLocale;\n const pattern = dateFormat ?? dateFormatForLocale(locale);\n const text = useComitorLabels(\"datePicker\", labels);\n const placeholder = placeholderProp ?? text.rangePlaceholder;\n\n const [open, setOpen] = useState(false);\n const label = formatRange(value, pattern, locale);\n const presetList = presets === false ? [] : presets;\n\n const showClear = clearable && Boolean(label) && !disabled;\n\n return (\n /* Nút xóa nằm NGOÀI trigger (anh em), không lồng trong <button> — vừa hợp lệ về HTML vừa\n đến được bằng phím Tab. */\n <div className={cn(\"relative w-full\", className)}>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n id={id}\n variant=\"outline\"\n disabled={disabled}\n className={cn(\n // `px-3` chứ không phải `px-4` của `Button`: đây là một Ô NHẬP nằm cùng cột với\n // `Input`/`Select`/`Combobox`, và cả ba đều thụt 12px. `pr-10` = 12 + 24 (nút xóa)\n // + 4, chỉ cần khi nút xóa có mặt.\n \"w-full justify-start gap-2 px-3 text-base font-normal md:text-sm\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-10\"\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{label ?? placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"flex w-auto flex-col gap-2 p-2 sm:flex-row\">\n {presetList.length > 0 && (\n <div className=\"flex flex-row flex-wrap gap-1 sm:w-36 sm:flex-col sm:border-r sm:border-border sm:pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {presetLabel(preset, text.presets)}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={locale}\n autoFocus\n numberOfMonths={numberOfMonths}\n selected={value ?? undefined}\n defaultMonth={value?.from ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(range) => onValueChange(range ?? null)}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n // Cùng hình dạng với nút xóa của `DatePicker`: ô 24px ở `right-2`. Là `<Button>` thật nên\n // có viền focus và vùng bấm đạt 24px — bản `<button>` thô trước đây chỉ 18px.\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"absolute right-2 top-1/2 size-6 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n onClick={() => onValueChange(null)}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{text.clearRange}</span>\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":";AA4KM,cAkCI,YAlCJ;AA1KN,SAAS,UAAU,YAAY,SAAS,UAAU,SAAsB,OAAO,kBAAkB;AACjG,SAAS,cAAc,SAAS;AAChC,SAAS,WAAW,gBAAgB;AAEpC,SAAS,kBAAkB,wBAAwB;AAEnD,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,aAAa;AACtB,SAAS,SAAS,gBAAgB,sBAAsB;AACxD;AAAA,EAIE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAKP,MAAM,2BAA2B;AAAA,EAC/B,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,WAAW;AAAA,EACX,cAAc;AAAA,EACd,cAAc;AAChB;AASA,MAAM,8BAA8B,IAAI;AAAA,EACtC,CAAC,GAAG,sBAAsB,GAAG,0BAA0B,EAAE,QAAQ,CAAC,WAAW;AAC3E,UAAM,MAAM,yBAAyB,OAAO,EAA2C;AACvF,WAAO,MAAM,CAAC,CAAC,QAAQ,GAAG,CAAU,IAAI,CAAC;AAAA,EAC3C,CAAC;AACH;AAGA,SAAS,YAAY,QAAsC,UAAsC;AAC/F,QAAM,MAAM,4BAA4B,IAAI,MAAM;AAClD,SAAO,MAAM,SAAS,GAAG,IAAI,OAAO;AACtC;AAEA,SAAS,aAAa,MAAY,SAAgB,SAAyB;AACzE,MAAI,WAAW,SAAS,MAAM,WAAW,OAAO,CAAC,EAAG,QAAO;AAC3D,MAAI,WAAW,QAAQ,MAAM,WAAW,OAAO,CAAC,EAAG,QAAO;AAC1D,SAAO;AACT;AAQA,SAAS,sBAAsB,SAAgB,SAAuC;AACpF,QAAM,WAAsB,CAAC;AAC7B,MAAI,QAAS,UAAS,KAAK,EAAE,QAAQ,WAAW,OAAO,EAAE,CAAC;AAC1D,MAAI,QAAS,UAAS,KAAK,EAAE,OAAO,WAAW,OAAO,EAAE,CAAC;AACzD,SAAO,SAAS,SAAS,IAAI,WAAW;AAC1C;AAiDO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,EAAE,cAAc,IAAI,iBAAiB;AAC3C,QAAM,SAAS,cAAc;AAC7B,QAAM,UAAU,cAAc,oBAAoB,MAAM;AACxD,QAAM,QAAQ,iBAAiB,cAAc,MAAM;AAEnD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAO,QAAQ,WAAW,OAAO,SAAS,EAAE,OAAO,CAAC,IAAI,EAAG;AAK5F,YAAU,MAAM;AACd,YAAQ,QAAQ,WAAW,OAAO,SAAS,EAAE,OAAO,CAAC,IAAI,EAAE;AAAA,EAC7D,GAAG,CAAC,OAAO,SAAS,MAAM,CAAC;AAE3B,QAAM,aAAa,CAAC,QAAgB;AAClC,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,YAAY,IAAI;AAClB,oBAAc,IAAI;AAClB;AAAA,IACF;AACA,UAAM,SAAS,MAAM,SAAS,SAAS,oBAAI,KAAK,GAAG,EAAE,OAAO,CAAC;AAC7D,QAAI,CAAC,QAAQ,MAAM,KAAK,aAAa,QAAQ,SAAS,OAAO,GAAG;AAE9D,cAAQ,QAAQ,WAAW,OAAO,SAAS,EAAE,OAAO,CAAC,IAAI,EAAE;AAC3D;AAAA,IACF;AACA,kBAAc,WAAW,MAAM,CAAC;AAAA,EAClC;AAEA,QAAM,aAAa,YAAY,QAAQ,CAAC,IAAI;AAE5C,SACE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA,aAAa,eAAe;AAAA,QAC5B,WAAU;AAAA,QACV,cAAa;AAAA,QACb,UAAU,CAAC,UAAU,QAAQ,MAAM,OAAO,KAAK;AAAA,QAC/C,QAAQ,CAAC,UAAU,WAAW,MAAM,OAAO,KAAK;AAAA,QAChD,WAAW,CAAC,UAAU;AACpB,cAAI,MAAM,QAAQ,SAAS;AACzB,kBAAM,eAAe;AACrB,uBAAW,IAAI;AAAA,UACjB;AAAA,QACF;AAAA,QACA,WAAU;AAAA,QACT,GAAG;AAAA;AAAA,IACN;AAAA,IAcA,qBAAC,SAAI,WAAU,mGACZ;AAAA,mBAAa,SAAS,CAAC,YACtB;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS,MAAM,cAAc,IAAI;AAAA,UAEjC;AAAA,gCAAC,KAAE,eAAY,QAAO,WAAU,YAAW;AAAA,YAC3C,oBAAC,UAAK,WAAU,WAAW,gBAAM,OAAM;AAAA;AAAA;AAAA,MACzC;AAAA,MAEF,qBAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,4BAAC,kBAAe,SAAO,MACrB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV;AAAA,YAEA;AAAA,kCAAC,gBAAa,eAAY,QAAO,WAAU,UAAS;AAAA,cACpD,oBAAC,UAAK,WAAU,WAAW,gBAAM,cAAa;AAAA;AAAA;AAAA,QAChD,GACF;AAAA,QACA,qBAAC,kBAAe,OAAM,OAAM,WAAU,yBACnC;AAAA,qBAAW,SAAS,KACnB,oBAAC,SAAI,WAAU,wDACZ,qBAAW,IAAI,CAAC,WACf;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS,MAAM;AACb,8BAAc,OAAO,SAAS,CAAC;AAC/B,wBAAQ,KAAK;AAAA,cACf;AAAA,cAEC,sBAAY,QAAQ,MAAM,OAAO;AAAA;AAAA,YAV7B,OAAO;AAAA,UAWd,CACD,GACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL;AAAA,cACA,WAAS;AAAA,cACT,UAAU,SAAS;AAAA,cACnB,cAAc,SAAS;AAAA,cACvB,UAAU,sBAAsB,SAAS,OAAO;AAAA,cAChD,UAAU,CAAC,SAAS;AAClB,8BAAc,OAAO,WAAW,IAAI,IAAI,IAAI;AAC5C,wBAAQ,KAAK;AAAA,cACf;AAAA;AAAA,UACF;AAAA,WACF;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;AAmCA,SAAS,YAAY,OAAqC,SAAiB,QAA+B;AACxG,MAAI,CAAC,OAAO,KAAM,QAAO;AACzB,QAAM,OAAO,WAAW,MAAM,MAAM,SAAS,EAAE,OAAO,CAAC;AACvD,MAAI,CAAC,MAAM,GAAI,QAAO;AACtB,SAAO,GAAG,IAAI,WAAM,WAAW,MAAM,IAAI,SAAS,EAAE,OAAO,CAAC,CAAC;AAC/D;AAQO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,EAAE,cAAc,IAAI,iBAAiB;AAC3C,QAAM,SAAS,cAAc;AAC7B,QAAM,UAAU,cAAc,oBAAoB,MAAM;AACxD,QAAM,OAAO,iBAAiB,cAAc,MAAM;AAClD,QAAM,cAAc,mBAAmB,KAAK;AAE5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,QAAQ,YAAY,OAAO,SAAS,MAAM;AAChD,QAAM,aAAa,YAAY,QAAQ,CAAC,IAAI;AAE5C,QAAM,YAAY,aAAa,QAAQ,KAAK,KAAK,CAAC;AAElD;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAI,WAAW,GAAG,mBAAmB,SAAS,GAC7C;AAAA,2BAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,4BAAC,kBAAe,SAAO,MACrB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL;AAAA,YACA,SAAQ;AAAA,YACR;AAAA,YACA,WAAW;AAAA;AAAA;AAAA;AAAA,cAIT;AAAA,cACA,CAAC,SAAS;AAAA,cACV,aAAa;AAAA,YACf;AAAA,YACC,GAAG;AAAA,YAEJ;AAAA,kCAAC,gBAAa,eAAY,QAAO,WAAU,yCAAwC;AAAA,cACnF,oBAAC,UAAK,WAAU,YAAY,mBAAS,aAAY;AAAA;AAAA;AAAA,QACnD,GACF;AAAA,QACA,qBAAC,kBAAe,OAAM,SAAQ,WAAU,8CACrC;AAAA,qBAAW,SAAS,KACnB,oBAAC,SAAI,WAAU,0FACZ,qBAAW,IAAI,CAAC,WACf;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS,MAAM;AACb,8BAAc,OAAO,SAAS,CAAC;AAC/B,wBAAQ,KAAK;AAAA,cACf;AAAA,cAEC,sBAAY,QAAQ,KAAK,OAAO;AAAA;AAAA,YAV5B,OAAO;AAAA,UAWd,CACD,GACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL;AAAA,cACA,WAAS;AAAA,cACT;AAAA,cACA,UAAU,SAAS;AAAA,cACnB,cAAc,OAAO,QAAQ;AAAA,cAC7B,UAAU,sBAAsB,SAAS,OAAO;AAAA,cAChD,UAAU,CAAC,UAAU,cAAc,SAAS,IAAI;AAAA;AAAA,UAClD;AAAA,WACF;AAAA,SACF;AAAA,MAEC;AAAA;AAAA,MAGC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS,MAAM,cAAc,IAAI;AAAA,UAEjC;AAAA,gCAAC,KAAE,eAAY,QAAO,WAAU,YAAW;AAAA,YAC3C,oBAAC,UAAK,WAAU,WAAW,eAAK,YAAW;AAAA;AAAA;AAAA,MAC7C;AAAA,OAEJ;AAAA;AAEJ;","names":[]}
@@ -45,7 +45,7 @@ function CommandInput({ className, ...props }) {
45
45
  {
46
46
  "data-slot": "command-input",
47
47
  className: cn(
48
- "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
48
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-base md:text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
49
49
  className
50
50
  ),
51
51
  ...props
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/command.tsx"],"sourcesContent":["\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog.js\";\n\n/**\n * Command — danh sách lệnh có lọc theo phím gõ (cmdk).\n *\n * Đây là primitive trần: composite `CommandPalette` và `Combobox` (Tier 2) dựng lên\n * từ đây. `CommandDialog` bọc sẵn trong `Dialog` cho bảng lệnh Cmd/Ctrl+K.\n *\n * Lưu ý a11y: `CommandDialog` bắt buộc có tiêu đề + mô tả cho trình đọc màn hình\n * (đặt trong `DialogHeader` ẩn `sr-only`) — mặc định theo `ComitorLocaleProvider` (namespace\n * `command`), đổi riêng bằng prop `title`/`description`.\n */\n\nexport type CommandProps = React.ComponentProps<typeof CommandPrimitive>;\n\nfunction Command({ className, ...props }: CommandProps) {\n return (\n <CommandPrimitive\n data-slot=\"command\"\n className={cn(\n \"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandDialogProps = React.ComponentProps<typeof Dialog> & {\n /** Tiêu đề cho trình đọc màn hình. Mặc định: khoá `dialogTitle` của namespace `command`. */\n title?: string;\n /** Mô tả cho trình đọc màn hình. Mặc định: khoá `dialogDescription` của namespace `command`. */\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n};\n\nfunction CommandDialog({\n title: titleProp,\n description: descriptionProp,\n children,\n className,\n showCloseButton = true,\n ...props\n}: CommandDialogProps) {\n const t = useComitorMessages(\"command\");\n const title = titleProp ?? t.dialogTitle;\n const description = descriptionProp ?? t.dialogDescription;\n return (\n <Dialog {...props}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <DialogContent className={cn(\"overflow-hidden p-0\", className)} showCloseButton={showCloseButton}>\n <Command className=\"[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport type CommandInputProps = React.ComponentProps<typeof CommandPrimitive.Input>;\n\nfunction CommandInput({ className, ...props }: CommandInputProps) {\n return (\n <div data-slot=\"command-input-wrapper\" className=\"flex h-9 items-center gap-2 border-b px-3\">\n {/* Icon kính lúp là icon MANG NGHĨA (nói \"đây là ô tìm kiếm\") → ngưỡng 3:1.\n `opacity-50` cũ hạ màu kế thừa xuống 3,56:1 (light) / 4,99:1 (dark) — đạt nhưng sát,\n và là con số không token nào bảo vệ. `--muted-foreground` là bậc dành đúng cho vai trò\n này: 5,23:1 (light) · 5,35:1 (dark) trên nền popover. */}\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n className={cn(\n \"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </div>\n );\n}\n\nexport type CommandListProps = React.ComponentProps<typeof CommandPrimitive.List>;\n\nfunction CommandList({ className, ...props }: CommandListProps) {\n return (\n <CommandPrimitive.List\n data-slot=\"command-list\"\n className={cn(\"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\", className)}\n {...props}\n />\n );\n}\n\nexport type CommandEmptyProps = React.ComponentProps<typeof CommandPrimitive.Empty>;\n\nfunction CommandEmpty({ ...props }: CommandEmptyProps) {\n return <CommandPrimitive.Empty data-slot=\"command-empty\" className=\"py-6 text-center text-sm\" {...props} />;\n}\n\nexport type CommandGroupProps = React.ComponentProps<typeof CommandPrimitive.Group>;\n\nfunction CommandGroup({ className, ...props }: CommandGroupProps) {\n return (\n <CommandPrimitive.Group\n data-slot=\"command-group\"\n className={cn(\n \"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandSeparatorProps = React.ComponentProps<typeof CommandPrimitive.Separator>;\n\nfunction CommandSeparator({ className, ...props }: CommandSeparatorProps) {\n return (\n <CommandPrimitive.Separator\n data-slot=\"command-separator\"\n className={cn(\"bg-border -mx-1 h-px\", className)}\n {...props}\n />\n );\n}\n\nexport type CommandItemProps = React.ComponentProps<typeof CommandPrimitive.Item>;\n\nfunction CommandItem({ className, ...props }: CommandItemProps) {\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\n className={cn(\n \"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 mt-1 first:mt-0 rounded-sm px-2 py-(--menu-item-py) text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandShortcutProps = React.ComponentProps<\"span\">;\n\nfunction CommandShortcut({ className, ...props }: CommandShortcutProps) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\"text-muted-foreground ml-auto text-xs tracking-widest\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut\n};\n"],"mappings":";AAwBI,cAiCE,YAjCF;AAtBJ,SAAS,WAAW,wBAAwB;AAC5C,SAAS,kBAAkB;AAE3B,SAAS,0BAA0B;AACnC,SAAS,UAAU;AACnB,SAAS,QAAQ,eAAe,mBAAmB,cAAc,mBAAmB;AAepF,SAAS,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAiB;AACtD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAWA,SAAS,cAAc;AAAA,EACrB,OAAO;AAAA,EACP,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,GAAG;AACL,GAAuB;AACrB,QAAM,IAAI,mBAAmB,SAAS;AACtC,QAAM,QAAQ,aAAa,EAAE;AAC7B,QAAM,cAAc,mBAAmB,EAAE;AACzC,SACE,qBAAC,UAAQ,GAAG,OACV;AAAA,yBAAC,gBAAa,WAAU,WACtB;AAAA,0BAAC,eAAa,iBAAM;AAAA,MACpB,oBAAC,qBAAmB,uBAAY;AAAA,OAClC;AAAA,IACA,oBAAC,iBAAc,WAAW,GAAG,uBAAuB,SAAS,GAAG,iBAC9D,8BAAC,WAAQ,WAAU,yZAChB,UACH,GACF;AAAA,KACF;AAEJ;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE,qBAAC,SAAI,aAAU,yBAAwB,WAAU,6CAK/C;AAAA,wBAAC,cAAW,WAAU,8BAA6B;AAAA,IACnD;AAAA,MAAC,iBAAiB;AAAA,MAAjB;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA,KACF;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,+DAA+D,SAAS;AAAA,MACrF,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,aAAa,EAAE,GAAG,MAAM,GAAsB;AACrD,SAAO,oBAAC,iBAAiB,OAAjB,EAAuB,aAAU,iBAAgB,WAAU,4BAA4B,GAAG,OAAO;AAC3G;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,wBAAwB,SAAS;AAAA,MAC9C,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,yDAAyD,SAAS;AAAA,MAC/E,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/command.tsx"],"sourcesContent":["\"use client\";\n\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog.js\";\n\n/**\n * Command — danh sách lệnh có lọc theo phím gõ (cmdk).\n *\n * Đây là primitive trần: composite `CommandPalette` và `Combobox` (Tier 2) dựng lên\n * từ đây. `CommandDialog` bọc sẵn trong `Dialog` cho bảng lệnh Cmd/Ctrl+K.\n *\n * Lưu ý a11y: `CommandDialog` bắt buộc có tiêu đề + mô tả cho trình đọc màn hình\n * (đặt trong `DialogHeader` ẩn `sr-only`) — mặc định theo `ComitorLocaleProvider` (namespace\n * `command`), đổi riêng bằng prop `title`/`description`.\n */\n\nexport type CommandProps = React.ComponentProps<typeof CommandPrimitive>;\n\nfunction Command({ className, ...props }: CommandProps) {\n return (\n <CommandPrimitive\n data-slot=\"command\"\n className={cn(\n \"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandDialogProps = React.ComponentProps<typeof Dialog> & {\n /** Tiêu đề cho trình đọc màn hình. Mặc định: khoá `dialogTitle` của namespace `command`. */\n title?: string;\n /** Mô tả cho trình đọc màn hình. Mặc định: khoá `dialogDescription` của namespace `command`. */\n description?: string;\n className?: string;\n showCloseButton?: boolean;\n};\n\nfunction CommandDialog({\n title: titleProp,\n description: descriptionProp,\n children,\n className,\n showCloseButton = true,\n ...props\n}: CommandDialogProps) {\n const t = useComitorMessages(\"command\");\n const title = titleProp ?? t.dialogTitle;\n const description = descriptionProp ?? t.dialogDescription;\n return (\n <Dialog {...props}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <DialogContent className={cn(\"overflow-hidden p-0\", className)} showCloseButton={showCloseButton}>\n <Command className=\"[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport type CommandInputProps = React.ComponentProps<typeof CommandPrimitive.Input>;\n\nfunction CommandInput({ className, ...props }: CommandInputProps) {\n return (\n <div data-slot=\"command-input-wrapper\" className=\"flex h-9 items-center gap-2 border-b px-3\">\n {/* Icon kính lúp là icon MANG NGHĨA (nói \"đây là ô tìm kiếm\") → ngưỡng 3:1.\n `opacity-50` cũ hạ màu kế thừa xuống 3,56:1 (light) / 4,99:1 (dark) — đạt nhưng sát,\n và là con số không token nào bảo vệ. `--muted-foreground` là bậc dành đúng cho vai trò\n này: 5,23:1 (light) · 5,35:1 (dark) trên nền popover. */}\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n className={cn(\n \"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-base md:text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </div>\n );\n}\n\nexport type CommandListProps = React.ComponentProps<typeof CommandPrimitive.List>;\n\nfunction CommandList({ className, ...props }: CommandListProps) {\n return (\n <CommandPrimitive.List\n data-slot=\"command-list\"\n className={cn(\"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\", className)}\n {...props}\n />\n );\n}\n\nexport type CommandEmptyProps = React.ComponentProps<typeof CommandPrimitive.Empty>;\n\nfunction CommandEmpty({ ...props }: CommandEmptyProps) {\n return <CommandPrimitive.Empty data-slot=\"command-empty\" className=\"py-6 text-center text-sm\" {...props} />;\n}\n\nexport type CommandGroupProps = React.ComponentProps<typeof CommandPrimitive.Group>;\n\nfunction CommandGroup({ className, ...props }: CommandGroupProps) {\n return (\n <CommandPrimitive.Group\n data-slot=\"command-group\"\n className={cn(\n \"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandSeparatorProps = React.ComponentProps<typeof CommandPrimitive.Separator>;\n\nfunction CommandSeparator({ className, ...props }: CommandSeparatorProps) {\n return (\n <CommandPrimitive.Separator\n data-slot=\"command-separator\"\n className={cn(\"bg-border -mx-1 h-px\", className)}\n {...props}\n />\n );\n}\n\nexport type CommandItemProps = React.ComponentProps<typeof CommandPrimitive.Item>;\n\nfunction CommandItem({ className, ...props }: CommandItemProps) {\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\n className={cn(\n \"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 mt-1 first:mt-0 rounded-sm px-2 py-(--menu-item-py) text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport type CommandShortcutProps = React.ComponentProps<\"span\">;\n\nfunction CommandShortcut({ className, ...props }: CommandShortcutProps) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\"text-muted-foreground ml-auto text-xs tracking-widest\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n CommandShortcut\n};\n"],"mappings":";AAwBI,cAiCE,YAjCF;AAtBJ,SAAS,WAAW,wBAAwB;AAC5C,SAAS,kBAAkB;AAE3B,SAAS,0BAA0B;AACnC,SAAS,UAAU;AACnB,SAAS,QAAQ,eAAe,mBAAmB,cAAc,mBAAmB;AAepF,SAAS,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAiB;AACtD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAWA,SAAS,cAAc;AAAA,EACrB,OAAO;AAAA,EACP,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,GAAG;AACL,GAAuB;AACrB,QAAM,IAAI,mBAAmB,SAAS;AACtC,QAAM,QAAQ,aAAa,EAAE;AAC7B,QAAM,cAAc,mBAAmB,EAAE;AACzC,SACE,qBAAC,UAAQ,GAAG,OACV;AAAA,yBAAC,gBAAa,WAAU,WACtB;AAAA,0BAAC,eAAa,iBAAM;AAAA,MACpB,oBAAC,qBAAmB,uBAAY;AAAA,OAClC;AAAA,IACA,oBAAC,iBAAc,WAAW,GAAG,uBAAuB,SAAS,GAAG,iBAC9D,8BAAC,WAAQ,WAAU,yZAChB,UACH,GACF;AAAA,KACF;AAEJ;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE,qBAAC,SAAI,aAAU,yBAAwB,WAAU,6CAK/C;AAAA,wBAAC,cAAW,WAAU,8BAA6B;AAAA,IACnD;AAAA,MAAC,iBAAiB;AAAA,MAAjB;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA,KACF;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,+DAA+D,SAAS;AAAA,MACrF,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,aAAa,EAAE,GAAG,MAAM,GAAsB;AACrD,SAAO,oBAAC,iBAAiB,OAAjB,EAAuB,aAAU,iBAAgB,WAAU,4BAA4B,GAAG,OAAO;AAC3G;AAIA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,wBAAwB,SAAS;AAAA,MAC9C,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC,iBAAiB;AAAA,IAAjB;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,gBAAgB,EAAE,WAAW,GAAG,MAAM,GAAyB;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,yDAAyD,SAAS;AAAA,MAC/E,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -27,7 +27,7 @@ function InputOTPSlot({ index, className, ...props }) {
27
27
  "data-slot": "input-otp-slot",
28
28
  "data-active": slot?.isActive,
29
29
  className: cn(
30
- "data-[active=true]:border-primary-ink data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink data-[active=true]:aria-invalid:border-destructive-ink dark:bg-input-fill border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-colors duration-150 ease-out outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
30
+ "data-[active=true]:border-primary-ink data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink data-[active=true]:aria-invalid:border-destructive-ink dark:bg-input-fill border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-base md:text-sm shadow-xs transition-colors duration-150 ease-out outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
31
31
  className
32
32
  ),
33
33
  ...props,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/input-otp.tsx"],"sourcesContent":["\"use client\";\n\nimport { OTPInput, OTPInputContext } from \"input-otp\";\nimport { MinusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * InputOTP — ô nhập mã OTP (input-otp). Dùng cho luồng 2FA / xác thực email.\n *\n * Khác bản comitor-ds: đọc slot bằng optional chaining thay vì `?? {}` — tsconfig\n * bật `noUncheckedIndexedAccess` nên `slots[index]` là `Slot | undefined`.\n */\n\nexport type InputOTPProps = React.ComponentProps<typeof OTPInput> & {\n containerClassName?: string;\n};\nexport type InputOTPGroupProps = React.ComponentProps<\"div\">;\nexport type InputOTPSlotProps = React.ComponentProps<\"div\"> & { index: number };\nexport type InputOTPSeparatorProps = React.ComponentProps<\"div\">;\n\nfunction InputOTP({ className, containerClassName, ...props }: InputOTPProps) {\n return (\n <OTPInput\n data-slot=\"input-otp\"\n containerClassName={cn(\"flex items-center gap-2 has-disabled:opacity-50\", containerClassName)}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n );\n}\n\nfunction InputOTPGroup({ className, ...props }: InputOTPGroupProps) {\n return <div data-slot=\"input-otp-group\" className={cn(\"flex items-center\", className)} {...props} />;\n}\n\nfunction InputOTPSlot({ index, className, ...props }: InputOTPSlotProps) {\n const inputOTPContext = React.useContext(OTPInputContext);\n const slot = inputOTPContext?.slots[index];\n\n return (\n <div\n data-slot=\"input-otp-slot\"\n data-active={slot?.isActive}\n className={cn(\n \"data-[active=true]:border-primary-ink data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink data-[active=true]:aria-invalid:border-destructive-ink dark:bg-input-fill border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-colors duration-150 ease-out outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]\",\n className\n )}\n {...props}\n >\n {slot?.char}\n {slot?.hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink bg-foreground h-4 w-px duration-1000\" />\n </div>\n )}\n </div>\n );\n}\n\nfunction InputOTPSeparator({ ...props }: InputOTPSeparatorProps) {\n return (\n // biome-ignore lint/a11y/useFocusableInteractive: dấu phân cách tĩnh giữa các nhóm ô, không phải splitter kéo được nên không cần focus\n // biome-ignore lint/a11y/useAriaPropsForRole: separator tĩnh không có giá trị → không cần aria-valuenow\n <div data-slot=\"input-otp-separator\" role=\"separator\" {...props}>\n <MinusIcon />\n </div>\n );\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };\n"],"mappings":";AAuBI,cAkBA,YAlBA;AArBJ,SAAS,UAAU,uBAAuB;AAC1C,SAAS,iBAAiB;AAC1B,YAAY,WAAW;AACvB,SAAS,UAAU;AAgBnB,SAAS,SAAS,EAAE,WAAW,oBAAoB,GAAG,MAAM,GAAkB;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,oBAAoB,GAAG,mDAAmD,kBAAkB;AAAA,MAC5F,WAAW,GAAG,+BAA+B,SAAS;AAAA,MACrD,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,cAAc,EAAE,WAAW,GAAG,MAAM,GAAuB;AAClE,SAAO,oBAAC,SAAI,aAAU,mBAAkB,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,OAAO;AACpG;AAEA,SAAS,aAAa,EAAE,OAAO,WAAW,GAAG,MAAM,GAAsB;AACvE,QAAM,kBAAkB,MAAM,WAAW,eAAe;AACxD,QAAM,OAAO,iBAAiB,MAAM,KAAK;AAEzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,eAAa,MAAM;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA,cAAM;AAAA,QACN,MAAM,gBACL,oBAAC,SAAI,WAAU,yEACb,8BAAC,SAAI,WAAU,4DAA2D,GAC5E;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAEA,SAAS,kBAAkB,EAAE,GAAG,MAAM,GAA2B;AAC/D;AAAA;AAAA;AAAA,IAGE,oBAAC,SAAI,aAAU,uBAAsB,MAAK,aAAa,GAAG,OACxD,8BAAC,aAAU,GACb;AAAA;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/input-otp.tsx"],"sourcesContent":["\"use client\";\n\nimport { OTPInput, OTPInputContext } from \"input-otp\";\nimport { MinusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * InputOTP — ô nhập mã OTP (input-otp). Dùng cho luồng 2FA / xác thực email.\n *\n * Khác bản comitor-ds: đọc slot bằng optional chaining thay vì `?? {}` — tsconfig\n * bật `noUncheckedIndexedAccess` nên `slots[index]` là `Slot | undefined`.\n */\n\nexport type InputOTPProps = React.ComponentProps<typeof OTPInput> & {\n containerClassName?: string;\n};\nexport type InputOTPGroupProps = React.ComponentProps<\"div\">;\nexport type InputOTPSlotProps = React.ComponentProps<\"div\"> & { index: number };\nexport type InputOTPSeparatorProps = React.ComponentProps<\"div\">;\n\nfunction InputOTP({ className, containerClassName, ...props }: InputOTPProps) {\n return (\n <OTPInput\n data-slot=\"input-otp\"\n containerClassName={cn(\"flex items-center gap-2 has-disabled:opacity-50\", containerClassName)}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n );\n}\n\nfunction InputOTPGroup({ className, ...props }: InputOTPGroupProps) {\n return <div data-slot=\"input-otp-group\" className={cn(\"flex items-center\", className)} {...props} />;\n}\n\nfunction InputOTPSlot({ index, className, ...props }: InputOTPSlotProps) {\n const inputOTPContext = React.useContext(OTPInputContext);\n const slot = inputOTPContext?.slots[index];\n\n return (\n <div\n data-slot=\"input-otp-slot\"\n data-active={slot?.isActive}\n className={cn(\n \"data-[active=true]:border-primary-ink data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive-ink data-[active=true]:aria-invalid:border-destructive-ink dark:bg-input-fill border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-base md:text-sm shadow-xs transition-colors duration-150 ease-out outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]\",\n className\n )}\n {...props}\n >\n {slot?.char}\n {slot?.hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink bg-foreground h-4 w-px duration-1000\" />\n </div>\n )}\n </div>\n );\n}\n\nfunction InputOTPSeparator({ ...props }: InputOTPSeparatorProps) {\n return (\n // biome-ignore lint/a11y/useFocusableInteractive: dấu phân cách tĩnh giữa các nhóm ô, không phải splitter kéo được nên không cần focus\n // biome-ignore lint/a11y/useAriaPropsForRole: separator tĩnh không có giá trị → không cần aria-valuenow\n <div data-slot=\"input-otp-separator\" role=\"separator\" {...props}>\n <MinusIcon />\n </div>\n );\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };\n"],"mappings":";AAuBI,cAkBA,YAlBA;AArBJ,SAAS,UAAU,uBAAuB;AAC1C,SAAS,iBAAiB;AAC1B,YAAY,WAAW;AACvB,SAAS,UAAU;AAgBnB,SAAS,SAAS,EAAE,WAAW,oBAAoB,GAAG,MAAM,GAAkB;AAC5E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,oBAAoB,GAAG,mDAAmD,kBAAkB;AAAA,MAC5F,WAAW,GAAG,+BAA+B,SAAS;AAAA,MACrD,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,cAAc,EAAE,WAAW,GAAG,MAAM,GAAuB;AAClE,SAAO,oBAAC,SAAI,aAAU,mBAAkB,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,OAAO;AACpG;AAEA,SAAS,aAAa,EAAE,OAAO,WAAW,GAAG,MAAM,GAAsB;AACvE,QAAM,kBAAkB,MAAM,WAAW,eAAe;AACxD,QAAM,OAAO,iBAAiB,MAAM,KAAK;AAEzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,eAAa,MAAM;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA,cAAM;AAAA,QACN,MAAM,gBACL,oBAAC,SAAI,WAAU,yEACb,8BAAC,SAAI,WAAU,4DAA2D,GAC5E;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAEA,SAAS,kBAAkB,EAAE,GAAG,MAAM,GAA2B;AAC/D;AAAA;AAAA;AAAA,IAGE,oBAAC,SAAI,aAAU,uBAAsB,MAAK,aAAa,GAAG,OACxD,8BAAC,aAAU,GACb;AAAA;AAEJ;","names":[]}
@@ -30,7 +30,10 @@ function Input({
30
30
  "aria-invalid": hasError || void 0,
31
31
  "aria-describedby": error ? `${inputId}-error` : hint ? `${inputId}-hint` : void 0,
32
32
  className: cn(
33
- "w-full h-9 px-3 rounded-md border bg-background text-sm text-foreground placeholder:text-muted-foreground",
33
+ // `text-base md:text-sm` — LUẬT CỠ CHỮ CHUNG của mọi ô nhập và nút dạng ô (Textarea, Select,
34
+ // Combobox, DatePicker, TimePicker, TagsInput, Mention, MaskInput, InputOTP, Command): 16px dưới
35
+ // `md`, 14px từ `md`. Đổi ở đây thì đổi cả bộ, không thì các ô cạnh nhau lệch cỡ trên điện thoại.
36
+ "w-full h-9 px-3 rounded-md border bg-background text-base md:text-sm text-foreground placeholder:text-muted-foreground",
34
37
  "transition-colors duration-150 ease-out",
35
38
  "focus:outline-none focus:ring-2 focus:ring-ring/50 focus:border-primary-ink",
36
39
  "disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-muted",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/input.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\nexport interface InputProps extends React.ComponentProps<\"input\"> {\n /** Nhãn hiển thị phía trên ô nhập. */\n label?: string;\n /** Thông báo lỗi — có giá trị là ô nhập chuyển sang trạng thái lỗi. */\n error?: string;\n /** Gợi ý phụ dưới ô nhập; bị `error` che khi có lỗi. */\n hint?: string;\n /** Nội dung chèn bên trái trong lòng ô nhập (icon, đơn vị…). */\n leftAddon?: React.ReactNode;\n /** Nội dung chèn bên phải trong lòng ô nhập. */\n rightAddon?: React.ReactNode;\n /** Alias của `leftAddon`. */\n leftIcon?: React.ReactNode;\n /** Alias của `rightAddon`. */\n rightIcon?: React.ReactNode;\n}\n\n/*\n * Style port nguyên trạng từ comitor-ds. Các prop label / error / hint / addon là phần\n * BỔ SUNG: không truyền cái nào thì component render đúng một thẻ <input> trần đã style.\n * (Đổi gray-* → ash-* theo namespace thang xám của package — xem styles.css.)\n *\n * ─ TRẠNG THÁI LỖI: BẬC `-ink`, KHÔNG PHẢI `--red` CŨNG KHÔNG PHẢI `--destructive` ─\n *\n * `--red` là thang màu phụ THÔ, vai trò TÔ NỀN: light #D64545 (chữ 12px trên nền trắng chỉ\n * 4,38:1 ✗), dark #F87171. `--destructive` là màu NỀN của nút xoá, ghim một giá trị cho cả hai\n * theme. Cả hai đều không phải bậc dành cho chữ/viền đứng trên nền TRANG — bậc đó là\n * `--destructive-ink` (= `--red-ink`), và nó gánh cả chữ lẫn viền:\n *\n * chữ lỗi `--destructive-ink` #9A3537 trên nền trắng = 7,15:1 ✓ · #F87171 trên #0E1116 = 6,84:1 ✓\n * viền lỗi cùng token : 7,15:1 (light) / 6,84:1 (dark) ✓ (ngưỡng viền control 3:1)\n *\n * Bản trước dùng `border-destructive` cho viền (5,03:1 light nhưng chỉ **3,76:1** ở dark — sát\n * ngưỡng) và một chuỗi `color-mix(--destructive, --ink 30%)` + `dark:text-red` cho chữ. Số thì\n * đạt, nhưng nhánh `dark:text-red` là token NỀN đem làm màu CHỮ — đúng cái lỗi mà hợp đồng màu\n * cấm; và chuỗi color-mix chép tay ở đây dễ lệch % so với `Badge`. Nay cả hai đi qua một token.\n *\n * ─ VIỀN LÚC NGHỈ / HOVER / FOCUS ─\n * `border-input` (= `--control-edge`) 3,83:1 (light) · 4,94:1 (dark) ✓. Hover phải ĐẬM HƠN lúc\n * nghỉ: `hover:border-ash-300 dark:hover:border-ash-500` cũ ở light chỉ **1,62:1** — rê chuột\n * vào là ô nhập gần như biến mất. Nay `--control-edge-strong`: 10,55:1 (light) · 11,65:1 (dark).\n * Focus cũng vậy — `focus:border-gold-300` (#E8B824) chỉ **1,86:1** trên nền trắng, tức lúc focus\n * ô nhập MẤT ranh giới; `--primary-ink` giữ đúng sắc gold mà đạt 6,51:1 (light) · 12,67:1 (dark).\n */\nfunction Input({\n className,\n type,\n label,\n error,\n hint,\n leftAddon,\n rightAddon,\n leftIcon,\n rightIcon,\n id,\n \"aria-invalid\": ariaInvalid,\n ...props\n}: InputProps) {\n const reactId = React.useId();\n const inputId = id ?? reactId;\n const hasError = Boolean(error) || ariaInvalid === true || ariaInvalid === \"true\";\n const left = leftAddon ?? leftIcon;\n const right = rightAddon ?? rightIcon;\n\n const inputEl = (\n <input\n id={inputId}\n type={type}\n data-slot=\"input\"\n aria-invalid={hasError || undefined}\n aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}\n className={cn(\n \"w-full h-9 px-3 rounded-md border bg-background text-sm text-foreground placeholder:text-muted-foreground\",\n \"transition-colors duration-150 ease-out\",\n \"focus:outline-none focus:ring-2 focus:ring-ring/50 focus:border-primary-ink\",\n \"disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-muted\",\n hasError\n ? \"border-destructive-ink focus:ring-destructive/30 focus:border-destructive-ink\"\n : \"border-input hover:border-control-edge-strong\",\n left && \"pl-9\",\n right && \"pr-9\",\n className\n )}\n {...props}\n />\n );\n\n if (!label && !error && !hint && !left && !right) {\n return inputEl;\n }\n\n return (\n <div className=\"w-full\">\n {label && (\n <label htmlFor={inputId} className=\"mb-1.5 block text-sm font-medium text-foreground\">\n {label}\n </label>\n )}\n <div className=\"relative flex items-center\">\n {left && (\n <div className=\"pointer-events-none absolute left-3 flex items-center text-muted-foreground\">{left}</div>\n )}\n {inputEl}\n {right && (\n <div className=\"pointer-events-none absolute right-3 flex items-center text-muted-foreground\">{right}</div>\n )}\n </div>\n {error && (\n <p id={`${inputId}-error`} className=\"mt-1.5 text-xs text-destructive-ink\">\n {error}\n </p>\n )}\n {hint && !error && (\n <p id={`${inputId}-hint`} className=\"mt-1.5 text-xs text-muted-foreground\">\n {hint}\n </p>\n )}\n </div>\n );\n}\n\nexport { Input };\n"],"mappings":";AAsEI,cAiCE,YAjCF;AApEJ,YAAY,WAAW;AACvB,SAAS,UAAU;AA8CnB,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,GAAG;AACL,GAAe;AACb,QAAM,UAAU,MAAM,MAAM;AAC5B,QAAM,UAAU,MAAM;AACtB,QAAM,WAAW,QAAQ,KAAK,KAAK,gBAAgB,QAAQ,gBAAgB;AAC3E,QAAM,OAAO,aAAa;AAC1B,QAAM,QAAQ,cAAc;AAE5B,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MACC,IAAI;AAAA,MACJ;AAAA,MACA,aAAU;AAAA,MACV,gBAAc,YAAY;AAAA,MAC1B,oBAAkB,QAAQ,GAAG,OAAO,WAAW,OAAO,GAAG,OAAO,UAAU;AAAA,MAC1E,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WACI,kFACA;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS;AAAA,QACT;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAGF,MAAI,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO;AAChD,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,SAAI,WAAU,UACZ;AAAA,aACC,oBAAC,WAAM,SAAS,SAAS,WAAU,oDAChC,iBACH;AAAA,IAEF,qBAAC,SAAI,WAAU,8BACZ;AAAA,cACC,oBAAC,SAAI,WAAU,+EAA+E,gBAAK;AAAA,MAEpG;AAAA,MACA,SACC,oBAAC,SAAI,WAAU,gFAAgF,iBAAM;AAAA,OAEzG;AAAA,IACC,SACC,oBAAC,OAAE,IAAI,GAAG,OAAO,UAAU,WAAU,uCAClC,iBACH;AAAA,IAED,QAAQ,CAAC,SACR,oBAAC,OAAE,IAAI,GAAG,OAAO,SAAS,WAAU,wCACjC,gBACH;AAAA,KAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/input.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\nexport interface InputProps extends React.ComponentProps<\"input\"> {\n /** Nhãn hiển thị phía trên ô nhập. */\n label?: string;\n /** Thông báo lỗi — có giá trị là ô nhập chuyển sang trạng thái lỗi. */\n error?: string;\n /** Gợi ý phụ dưới ô nhập; bị `error` che khi có lỗi. */\n hint?: string;\n /** Nội dung chèn bên trái trong lòng ô nhập (icon, đơn vị…). */\n leftAddon?: React.ReactNode;\n /** Nội dung chèn bên phải trong lòng ô nhập. */\n rightAddon?: React.ReactNode;\n /** Alias của `leftAddon`. */\n leftIcon?: React.ReactNode;\n /** Alias của `rightAddon`. */\n rightIcon?: React.ReactNode;\n}\n\n/*\n * Style port nguyên trạng từ comitor-ds. Các prop label / error / hint / addon là phần\n * BỔ SUNG: không truyền cái nào thì component render đúng một thẻ <input> trần đã style.\n * (Đổi gray-* → ash-* theo namespace thang xám của package — xem styles.css.)\n *\n * ─ TRẠNG THÁI LỖI: BẬC `-ink`, KHÔNG PHẢI `--red` CŨNG KHÔNG PHẢI `--destructive` ─\n *\n * `--red` là thang màu phụ THÔ, vai trò TÔ NỀN: light #D64545 (chữ 12px trên nền trắng chỉ\n * 4,38:1 ✗), dark #F87171. `--destructive` là màu NỀN của nút xoá, ghim một giá trị cho cả hai\n * theme. Cả hai đều không phải bậc dành cho chữ/viền đứng trên nền TRANG — bậc đó là\n * `--destructive-ink` (= `--red-ink`), và nó gánh cả chữ lẫn viền:\n *\n * chữ lỗi `--destructive-ink` #9A3537 trên nền trắng = 7,15:1 ✓ · #F87171 trên #0E1116 = 6,84:1 ✓\n * viền lỗi cùng token : 7,15:1 (light) / 6,84:1 (dark) ✓ (ngưỡng viền control 3:1)\n *\n * Bản trước dùng `border-destructive` cho viền (5,03:1 light nhưng chỉ **3,76:1** ở dark — sát\n * ngưỡng) và một chuỗi `color-mix(--destructive, --ink 30%)` + `dark:text-red` cho chữ. Số thì\n * đạt, nhưng nhánh `dark:text-red` là token NỀN đem làm màu CHỮ — đúng cái lỗi mà hợp đồng màu\n * cấm; và chuỗi color-mix chép tay ở đây dễ lệch % so với `Badge`. Nay cả hai đi qua một token.\n *\n * ─ VIỀN LÚC NGHỈ / HOVER / FOCUS ─\n * `border-input` (= `--control-edge`) 3,83:1 (light) · 4,94:1 (dark) ✓. Hover phải ĐẬM HƠN lúc\n * nghỉ: `hover:border-ash-300 dark:hover:border-ash-500` cũ ở light chỉ **1,62:1** — rê chuột\n * vào là ô nhập gần như biến mất. Nay `--control-edge-strong`: 10,55:1 (light) · 11,65:1 (dark).\n * Focus cũng vậy — `focus:border-gold-300` (#E8B824) chỉ **1,86:1** trên nền trắng, tức lúc focus\n * ô nhập MẤT ranh giới; `--primary-ink` giữ đúng sắc gold mà đạt 6,51:1 (light) · 12,67:1 (dark).\n */\nfunction Input({\n className,\n type,\n label,\n error,\n hint,\n leftAddon,\n rightAddon,\n leftIcon,\n rightIcon,\n id,\n \"aria-invalid\": ariaInvalid,\n ...props\n}: InputProps) {\n const reactId = React.useId();\n const inputId = id ?? reactId;\n const hasError = Boolean(error) || ariaInvalid === true || ariaInvalid === \"true\";\n const left = leftAddon ?? leftIcon;\n const right = rightAddon ?? rightIcon;\n\n const inputEl = (\n <input\n id={inputId}\n type={type}\n data-slot=\"input\"\n aria-invalid={hasError || undefined}\n aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}\n className={cn(\n // `text-base md:text-sm` — LUẬT CỠ CHỮ CHUNG của mọi ô nhập và nút dạng ô (Textarea, Select,\n // Combobox, DatePicker, TimePicker, TagsInput, Mention, MaskInput, InputOTP, Command): 16px dưới\n // `md`, 14px từ `md`. Đổi ở đây thì đổi cả bộ, không thì các ô cạnh nhau lệch cỡ trên điện thoại.\n \"w-full h-9 px-3 rounded-md border bg-background text-base md:text-sm text-foreground placeholder:text-muted-foreground\",\n \"transition-colors duration-150 ease-out\",\n \"focus:outline-none focus:ring-2 focus:ring-ring/50 focus:border-primary-ink\",\n \"disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-muted\",\n hasError\n ? \"border-destructive-ink focus:ring-destructive/30 focus:border-destructive-ink\"\n : \"border-input hover:border-control-edge-strong\",\n left && \"pl-9\",\n right && \"pr-9\",\n className\n )}\n {...props}\n />\n );\n\n if (!label && !error && !hint && !left && !right) {\n return inputEl;\n }\n\n return (\n <div className=\"w-full\">\n {label && (\n <label htmlFor={inputId} className=\"mb-1.5 block text-sm font-medium text-foreground\">\n {label}\n </label>\n )}\n <div className=\"relative flex items-center\">\n {left && (\n <div className=\"pointer-events-none absolute left-3 flex items-center text-muted-foreground\">{left}</div>\n )}\n {inputEl}\n {right && (\n <div className=\"pointer-events-none absolute right-3 flex items-center text-muted-foreground\">{right}</div>\n )}\n </div>\n {error && (\n <p id={`${inputId}-error`} className=\"mt-1.5 text-xs text-destructive-ink\">\n {error}\n </p>\n )}\n {hint && !error && (\n <p id={`${inputId}-hint`} className=\"mt-1.5 text-xs text-muted-foreground\">\n {hint}\n </p>\n )}\n </div>\n );\n}\n\nexport { Input };\n"],"mappings":";AAsEI,cAoCE,YApCF;AApEJ,YAAY,WAAW;AACvB,SAAS,UAAU;AA8CnB,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,GAAG;AACL,GAAe;AACb,QAAM,UAAU,MAAM,MAAM;AAC5B,QAAM,UAAU,MAAM;AACtB,QAAM,WAAW,QAAQ,KAAK,KAAK,gBAAgB,QAAQ,gBAAgB;AAC3E,QAAM,OAAO,aAAa;AAC1B,QAAM,QAAQ,cAAc;AAE5B,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MACC,IAAI;AAAA,MACJ;AAAA,MACA,aAAU;AAAA,MACV,gBAAc,YAAY;AAAA,MAC1B,oBAAkB,QAAQ,GAAG,OAAO,WAAW,OAAO,GAAG,OAAO,UAAU;AAAA,MAC1E,WAAW;AAAA;AAAA;AAAA;AAAA,QAIT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WACI,kFACA;AAAA,QACJ,QAAQ;AAAA,QACR,SAAS;AAAA,QACT;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAGF,MAAI,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO;AAChD,WAAO;AAAA,EACT;AAEA,SACE,qBAAC,SAAI,WAAU,UACZ;AAAA,aACC,oBAAC,WAAM,SAAS,SAAS,WAAU,oDAChC,iBACH;AAAA,IAEF,qBAAC,SAAI,WAAU,8BACZ;AAAA,cACC,oBAAC,SAAI,WAAU,+EAA+E,gBAAK;AAAA,MAEpG;AAAA,MACA,SACC,oBAAC,SAAI,WAAU,gFAAgF,iBAAM;AAAA,OAEzG;AAAA,IACC,SACC,oBAAC,OAAE,IAAI,GAAG,OAAO,UAAU,WAAU,uCAClC,iBACH;AAAA,IAED,QAAQ,CAAC,SACR,oBAAC,OAAE,IAAI,GAAG,OAAO,SAAS,WAAU,wCACjC,gBACH;AAAA,KAEJ;AAEJ;","names":[]}
@@ -480,12 +480,12 @@ function KeyValueValueInput(props) {
480
480
  * ⚠ Khác thượng nguồn: ô giá trị phải trông như ô khoá (`Input`) đứng ngay cạnh khi còn MỘT
481
481
  * dòng. `Textarea` của gói theo comitor-ds: `text-base md:text-sm` + `py-2` (cao 38px ở
482
482
  * desktop, 42px và chữ 16px ở mobile), nền `bg-transparent dark:bg-input-fill`, có
483
- * `shadow-xs`; còn `Input` là `h-9 text-sm bg-background`, không bóng. Ghi đè ở ĐÂY chứ
484
- * không sửa hai primitive: `text-sm` ở mọi cỡ màn, `min-h-9` + `py-1.5` (dòng chữ 20px cách
483
+ * `shadow-xs`; còn `Input` là `h-9 bg-background`, không bóng. Ghi đè ở ĐÂY chứ không sửa
484
+ * hai primitive: cùng cỡ chữ với mọi ô nhập (`text-base md:text-sm`), `min-h-9` + `py-1.5` (dòng chữ 20px cách
485
485
  * mép trên 7px — đúng chỗ `Input` căn giữa dòng chữ trong 36px), `bg-background` ở cả hai
486
486
  * theme, bỏ bóng. Nhiều dòng thì `field-sizing-content` vẫn cho ô cao dần như cũ.
487
487
  */
488
- "field-sizing-content min-h-9 resize-none py-1.5 text-sm md:text-sm bg-background dark:bg-background shadow-none",
488
+ "field-sizing-content min-h-9 resize-none py-1.5 text-base md:text-sm bg-background dark:bg-background shadow-none",
489
489
  maxRows && "overflow-y-auto",
490
490
  className
491
491
  ),