@comitor/ui 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -10
- package/dist/chart/chart-colors.d.ts +56 -0
- package/dist/chart/chart-colors.js +21 -0
- package/dist/chart/chart-colors.js.map +1 -0
- package/dist/chart/chart.d.ts +40 -0
- package/dist/chart/chart.js.map +1 -1
- package/dist/chart/comitor-chart.d.ts +21 -43
- package/dist/chart/comitor-chart.js +13 -24
- package/dist/chart/comitor-chart.js.map +1 -1
- package/dist/chart/index.d.ts +2 -1
- package/dist/chart/index.js +1 -0
- package/dist/chart/index.js.map +1 -1
- package/dist/components/composite/data-table-constants.d.ts +40 -0
- package/dist/components/composite/data-table-constants.js +13 -0
- package/dist/components/composite/data-table-constants.js.map +1 -0
- package/dist/components/composite/data-table.d.ts +2 -27
- package/dist/components/composite/data-table.js +1 -10
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/date-picker-constants.d.ts +21 -0
- package/dist/components/composite/date-picker-constants.js +39 -0
- package/dist/components/composite/date-picker-constants.js.map +1 -0
- package/dist/components/composite/date-picker.d.ts +2 -18
- package/dist/components/composite/date-picker.js +6 -48
- package/dist/components/composite/date-picker.js.map +1 -1
- package/dist/components/composite/form-field-constants.d.ts +26 -0
- package/dist/components/composite/form-field-constants.js +13 -0
- package/dist/components/composite/form-field-constants.js.map +1 -0
- package/dist/components/composite/form-field.d.ts +1 -13
- package/dist/components/composite/form-field.js +1 -11
- package/dist/components/composite/form-field.js.map +1 -1
- package/dist/components/composite/search-filter-bar-constants.d.ts +52 -0
- package/dist/components/composite/search-filter-bar-constants.js +18 -0
- package/dist/components/composite/search-filter-bar-constants.js.map +1 -0
- package/dist/components/composite/search-filter-bar.d.ts +3 -37
- package/dist/components/composite/search-filter-bar.js +2 -15
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/table-pagination-constants.d.ts +56 -0
- package/dist/components/composite/table-pagination-constants.js +18 -0
- package/dist/components/composite/table-pagination-constants.js.map +1 -0
- package/dist/components/composite/table-pagination.d.ts +2 -43
- package/dist/components/composite/table-pagination.js +4 -15
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/shell/shell-context.js +3 -2
- package/dist/shell/shell-context.js.map +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/date-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n endOfMonth,\n format as formatDate,\n isAfter,\n isBefore,\n isValid,\n parse,\n startOfDay,\n startOfMonth,\n subDays,\n subMonths\n} from \"date-fns\";\nimport { vi } from \"date-fns/locale\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\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\";\n\nexport type { DateRange };\n\n/** Định dạng ngày của Việt Nam — dùng thống nhất ở mọi app Comitor. */\nexport const VI_DATE_FORMAT = \"dd/MM/yyyy\";\n\nexport interface DatePreset {\n id: string;\n label: string;\n /** Trả về ngày (DatePicker) hoặc khoảng ngày (DateRangePicker) tại thời điểm bấm */\n getValue: () => Date;\n}\n\nexport interface DateRangePreset {\n id: string;\n label: string;\n getValue: () => DateRange;\n}\n\n/** Preset mặc định cho ô chọn một ngày. */\nexport const DEFAULT_DATE_PRESETS: DatePreset[] = [\n { id: \"today\", label: \"Hôm nay\", getValue: () => startOfDay(new Date()) },\n { id: \"yesterday\", label: \"Hôm qua\", getValue: () => startOfDay(subDays(new Date(), 1)) },\n { id: \"week-ago\", label: \"7 ngày trước\", getValue: () => startOfDay(subDays(new Date(), 7)) }\n];\n\n/** Preset mặc định cho ô chọn khoảng ngày (báo cáo, bộ lọc danh sách). */\nexport const DEFAULT_DATE_RANGE_PRESETS: DateRangePreset[] = [\n { id: \"today\", label: \"Hôm nay\", getValue: () => ({ from: startOfDay(new Date()), to: startOfDay(new Date()) }) },\n {\n id: \"last-7\",\n label: \"7 ngày qua\",\n getValue: () => ({ from: startOfDay(subDays(new Date(), 6)), to: startOfDay(new Date()) })\n },\n {\n id: \"last-30\",\n label: \"30 ngày qua\",\n getValue: () => ({ from: startOfDay(subDays(new Date(), 29)), to: startOfDay(new Date()) })\n },\n {\n id: \"this-month\",\n label: \"Tháng này\",\n getValue: () => ({ from: startOfMonth(new Date()), to: startOfDay(new Date()) })\n },\n {\n id: \"last-month\",\n label: \"Tháng trước\",\n getValue: () => {\n const previous = subMonths(new Date(), 1);\n return { from: startOfMonth(previous), to: endOfMonth(previous) };\n }\n }\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 /** Cột preset bên trái lịch (mặc định ẩn) */\n presets?: DatePreset[] | false;\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 `dd/MM/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 = VI_DATE_FORMAT,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về).\n useEffect(() => {\n setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\");\n }, [value]);\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, VI_DATE_FORMAT, new Date(), { locale: vi });\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, VI_DATE_FORMAT, { locale: vi }) : \"\");\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}\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 <div className=\"absolute right-1 top-1/2 flex -translate-y-1/2 items-center\">\n {clearable && value && !disabled && (\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" onClick={() => onValueChange(null)}>\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Xóa ngày</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" disabled={disabled}>\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Mở lịch</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 {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={vi}\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 placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /** Cột preset bên trái (mặc định DEFAULT_DATE_RANGE_PRESETS; `false` để tắt) */\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 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): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, VI_DATE_FORMAT, { locale: vi });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, VI_DATE_FORMAT, { locale: vi })}`;\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 = \"Chọn khoảng ngày\",\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const [open, setOpen] = useState(false);\n const label = formatRange(value);\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=\"relative w-full\">\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 \"w-full justify-start gap-2 font-normal\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-9\",\n className\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0\" />\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 {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={vi}\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 <button\n type=\"button\"\n aria-label=\"Xóa khoảng ngày\"\n onClick={() => onValueChange(null)}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\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, parse, startOfDay } from \"date-fns\";\nimport { vi } from \"date-fns/locale\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\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 DatePreset,\n type DateRangePreset,\n DEFAULT_DATE_RANGE_PRESETS,\n VI_DATE_FORMAT\n} from \"./date-picker-constants.js\";\n\nexport type { DateRange };\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 /** Cột preset bên trái lịch (mặc định ẩn) */\n presets?: DatePreset[] | false;\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 `dd/MM/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 = VI_DATE_FORMAT,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về).\n useEffect(() => {\n setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\");\n }, [value]);\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, VI_DATE_FORMAT, new Date(), { locale: vi });\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, VI_DATE_FORMAT, { locale: vi }) : \"\");\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}\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 <div className=\"absolute right-1 top-1/2 flex -translate-y-1/2 items-center\">\n {clearable && value && !disabled && (\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" onClick={() => onValueChange(null)}>\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Xóa ngày</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" disabled={disabled}>\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Mở lịch</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 {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={vi}\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 placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /** Cột preset bên trái (mặc định DEFAULT_DATE_RANGE_PRESETS; `false` để tắt) */\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 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): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, VI_DATE_FORMAT, { locale: vi });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, VI_DATE_FORMAT, { locale: vi })}`;\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 = \"Chọn khoảng ngày\",\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const [open, setOpen] = useState(false);\n const label = formatRange(value);\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=\"relative w-full\">\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 \"w-full justify-start gap-2 font-normal\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-9\",\n className\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0\" />\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 {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={vi}\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 <button\n type=\"button\"\n aria-label=\"Xóa khoảng ngày\"\n onClick={() => onValueChange(null)}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel `Select` + hai hàm quy đổi giá trị.
|
|
3
|
+
*
|
|
4
|
+
* Module **CỐ Ý không có `"use client"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy
|
|
5
|
+
* đủ: mọi export của một file `"use client"` đến tay Server Component dưới dạng
|
|
6
|
+
* client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần
|
|
7
|
+
* (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một
|
|
8
|
+
* khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `form-field.tsx` chỉ được `import`,
|
|
11
|
+
* KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Sentinel cho `Select` của Radix.
|
|
15
|
+
*
|
|
16
|
+
* Radix cấm `SelectItem value=""` (chuỗi rỗng dành riêng cho việc xoá lựa chọn), nên mục
|
|
17
|
+
* "— Không chọn —" phải mang một giá trị giả. Dùng chung hằng số này ở mọi app để tránh mỗi
|
|
18
|
+
* nơi tự đặt một sentinel rồi lọt xuống payload gửi server.
|
|
19
|
+
*/
|
|
20
|
+
declare const SELECT_EMPTY_VALUE = "__empty__";
|
|
21
|
+
/** Giá trị model ("" = chưa chọn) → giá trị cho Radix Select. */
|
|
22
|
+
declare function toSelectValue(value: string | null | undefined): string;
|
|
23
|
+
/** Giá trị từ Radix Select → giá trị model ("" khi người dùng chọn mục rỗng). */
|
|
24
|
+
declare function fromSelectValue(value: string): string;
|
|
25
|
+
|
|
26
|
+
export { SELECT_EMPTY_VALUE, fromSelectValue, toSelectValue };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const SELECT_EMPTY_VALUE = "__empty__";
|
|
2
|
+
function toSelectValue(value) {
|
|
3
|
+
return value == null || value === "" ? SELECT_EMPTY_VALUE : value;
|
|
4
|
+
}
|
|
5
|
+
function fromSelectValue(value) {
|
|
6
|
+
return value === SELECT_EMPTY_VALUE ? "" : value;
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
SELECT_EMPTY_VALUE,
|
|
10
|
+
fromSelectValue,
|
|
11
|
+
toSelectValue
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=form-field-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/form-field-constants.ts"],"sourcesContent":["/**\n * Sentinel `Select` + hai hàm quy đổi giá trị.\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một\n * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `form-field.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\n\n/**\n * Sentinel cho `Select` của Radix.\n *\n * Radix cấm `SelectItem value=\"\"` (chuỗi rỗng dành riêng cho việc xoá lựa chọn), nên mục\n * \"— Không chọn —\" phải mang một giá trị giả. Dùng chung hằng số này ở mọi app để tránh mỗi\n * nơi tự đặt một sentinel rồi lọt xuống payload gửi server.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\n\n/** Giá trị model (\"\" = chưa chọn) → giá trị cho Radix Select. */\nexport function toSelectValue(value: string | null | undefined): string {\n return value == null || value === \"\" ? SELECT_EMPTY_VALUE : value;\n}\n\n/** Giá trị từ Radix Select → giá trị model (\"\" khi người dùng chọn mục rỗng). */\nexport function fromSelectValue(value: string): string {\n return value === SELECT_EMPTY_VALUE ? \"\" : value;\n}\n"],"mappings":"AAoBO,MAAM,qBAAqB;AAG3B,SAAS,cAAc,OAA0C;AACtE,SAAO,SAAS,QAAQ,UAAU,KAAK,qBAAqB;AAC9D;AAGO,SAAS,gBAAgB,OAAuB;AACrD,SAAO,UAAU,qBAAqB,KAAK;AAC7C;","names":[]}
|
|
@@ -42,17 +42,5 @@ interface FormFieldProps {
|
|
|
42
42
|
* mở lại file này — một FormField "đa năng" ôm hết mọi loại control sẽ phình ra không có điểm dừng.
|
|
43
43
|
*/
|
|
44
44
|
declare function FormField({ label, required, description, error, htmlFor, colSpan, orientation, className, children }: FormFieldProps): React.JSX.Element;
|
|
45
|
-
/**
|
|
46
|
-
* Sentinel cho `Select` của Radix.
|
|
47
|
-
*
|
|
48
|
-
* Radix cấm `SelectItem value=""` (chuỗi rỗng dành riêng cho việc xoá lựa chọn), nên mục
|
|
49
|
-
* "— Không chọn —" phải mang một giá trị giả. Dùng chung hằng số này ở mọi app để tránh mỗi
|
|
50
|
-
* nơi tự đặt một sentinel rồi lọt xuống payload gửi server.
|
|
51
|
-
*/
|
|
52
|
-
declare const SELECT_EMPTY_VALUE = "__empty__";
|
|
53
|
-
/** Giá trị model ("" = chưa chọn) → giá trị cho Radix Select. */
|
|
54
|
-
declare function toSelectValue(value: string | null | undefined): string;
|
|
55
|
-
/** Giá trị từ Radix Select → giá trị model ("" khi người dùng chọn mục rỗng). */
|
|
56
|
-
declare function fromSelectValue(value: string): string;
|
|
57
45
|
|
|
58
|
-
export { FormField, type FormFieldColSpan, type FormFieldControlProps, type FormFieldProps
|
|
46
|
+
export { FormField, type FormFieldColSpan, type FormFieldControlProps, type FormFieldProps };
|
|
@@ -67,17 +67,7 @@ function FormField({
|
|
|
67
67
|
}
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
|
-
const SELECT_EMPTY_VALUE = "__empty__";
|
|
71
|
-
function toSelectValue(value) {
|
|
72
|
-
return value == null || value === "" ? SELECT_EMPTY_VALUE : value;
|
|
73
|
-
}
|
|
74
|
-
function fromSelectValue(value) {
|
|
75
|
-
return value === SELECT_EMPTY_VALUE ? "" : value;
|
|
76
|
-
}
|
|
77
70
|
export {
|
|
78
|
-
FormField
|
|
79
|
-
SELECT_EMPTY_VALUE,
|
|
80
|
-
fromSelectValue,
|
|
81
|
-
toSelectValue
|
|
71
|
+
FormField
|
|
82
72
|
};
|
|
83
73
|
//# sourceMappingURL=form-field.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/form-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\nimport { type ReactNode, useId } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from \"../ui/field.js\";\n\nexport type FormFieldColSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;\n\n/**\n * Thuộc tính a11y mà `FormField` tiêm xuống control con.\n *\n * Trải nguyên cụm này vào `<Input>`/`<Combobox>`/`<Select>` là control tự có id khớp nhãn,\n * tự báo lỗi và trạng thái bắt buộc — không phải nhớ từng thuộc tính ở mỗi trang.\n */\nexport interface FormFieldControlProps {\n id: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n \"aria-describedby\"?: string;\n}\n\nexport interface FormFieldProps {\n label?: ReactNode;\n /** Đánh dấu bắt buộc: vẽ dấu * và phát `aria-required` xuống control */\n required?: boolean;\n /** Mô tả/gợi ý dưới trường — bị ẩn khi có lỗi để không đọc chồng lời */\n description?: ReactNode;\n /** Thông báo lỗi; có giá trị là trường vào trạng thái invalid */\n error?: ReactNode;\n /** id của control — bỏ trống thì tự sinh bằng useId */\n htmlFor?: string;\n /** Số cột chiếm trong lưới 12 cột của `FormSection` (mặc định 12 = cả hàng) */\n colSpan?: FormFieldColSpan;\n orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n className?: string;\n /**\n * Control của trường. Dạng hàm sẽ nhận `FormFieldControlProps` để trải xuống —\n * dạng node thì app tự lo id/aria (dùng khi control đã tự quản, vd Checkbox có nhãn riêng).\n */\n children: ReactNode | ((control: FormFieldControlProps) => ReactNode);\n}\n\nconst colSpanClasses: Record<FormFieldColSpan, string> = {\n 1: \"col-span-12 md:col-span-1\",\n 2: \"col-span-12 md:col-span-2\",\n 3: \"col-span-12 sm:col-span-6 md:col-span-3\",\n 4: \"col-span-12 sm:col-span-6 md:col-span-4\",\n 5: \"col-span-12 sm:col-span-6 md:col-span-5\",\n 6: \"col-span-12 sm:col-span-6\",\n 7: \"col-span-12 md:col-span-7\",\n 8: \"col-span-12 md:col-span-8\",\n 9: \"col-span-12 md:col-span-9\",\n 10: \"col-span-12 md:col-span-10\",\n 11: \"col-span-12 md:col-span-11\",\n 12: \"col-span-12\"\n};\n\n/**\n * Một trường form: nhãn + control + mô tả/lỗi, đặt trong lưới 12 cột của `FormSection`.\n *\n * KHÔNG tự vẽ input. Nhãn và lỗi uỷ quyền xuống primitive `Field`, còn control do app chọn —\n * nếu composite tự dựng input thì mỗi kiểu control mới (combobox, date range, editor) lại phải\n * mở lại file này — một FormField \"đa năng\" ôm hết mọi loại control sẽ phình ra không có điểm dừng.\n */\nexport function FormField({\n label,\n required = false,\n description,\n error,\n htmlFor,\n colSpan = 12,\n orientation = \"vertical\",\n className,\n children\n}: FormFieldProps) {\n const generatedId = useId();\n const id = htmlFor ?? generatedId;\n const hasError = Boolean(error);\n const errorId = `${id}-error`;\n const descriptionId = `${id}-description`;\n\n const control: FormFieldControlProps = {\n id,\n \"aria-invalid\": hasError ? true : undefined,\n \"aria-required\": required ? true : undefined,\n \"aria-describedby\": hasError ? errorId : description ? descriptionId : undefined\n };\n\n return (\n <Field\n data-slot=\"form-field\"\n data-invalid={hasError || undefined}\n orientation={orientation}\n className={cn(colSpanClasses[colSpan], className)}\n >\n {label && (\n <FieldLabel htmlFor={id} className=\"text-sm\">\n {label}\n {required && (\n // Dấu * chỉ là tín hiệu thị giác — trạng thái bắt buộc đã nằm ở aria-required trên\n // control, để trình đọc màn hình khỏi đọc thừa \"sao\".\n // Bậc ink (7,15:1 light / 6,42:1 dark trên `--card`) chứ không `text-destructive`\n // (bậc NỀN, chỉ 3,53:1 trên `--card` ở dark ✗).\n <span aria-hidden=\"true\" className=\"text-destructive-ink\">\n *\n </span>\n )}\n </FieldLabel>\n )}\n\n <FieldContent>\n {typeof children === \"function\" ? children(control) : children}\n\n {hasError ? (\n <FieldError id={errorId} className=\"flex items-center gap-1.5 text-xs animate-in fade-in slide-in-from-top-1\">\n <AlertCircle aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n {error}\n </FieldError>\n ) : (\n description && (\n <FieldDescription id={descriptionId} className=\"text-xs\">\n {description}\n </FieldDescription>\n )\n )}\n </FieldContent>\n </Field>\n );\n}\n
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/form-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\nimport { type ReactNode, useId } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from \"../ui/field.js\";\n\nexport type FormFieldColSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;\n\n/**\n * Thuộc tính a11y mà `FormField` tiêm xuống control con.\n *\n * Trải nguyên cụm này vào `<Input>`/`<Combobox>`/`<Select>` là control tự có id khớp nhãn,\n * tự báo lỗi và trạng thái bắt buộc — không phải nhớ từng thuộc tính ở mỗi trang.\n */\nexport interface FormFieldControlProps {\n id: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n \"aria-describedby\"?: string;\n}\n\nexport interface FormFieldProps {\n label?: ReactNode;\n /** Đánh dấu bắt buộc: vẽ dấu * và phát `aria-required` xuống control */\n required?: boolean;\n /** Mô tả/gợi ý dưới trường — bị ẩn khi có lỗi để không đọc chồng lời */\n description?: ReactNode;\n /** Thông báo lỗi; có giá trị là trường vào trạng thái invalid */\n error?: ReactNode;\n /** id của control — bỏ trống thì tự sinh bằng useId */\n htmlFor?: string;\n /** Số cột chiếm trong lưới 12 cột của `FormSection` (mặc định 12 = cả hàng) */\n colSpan?: FormFieldColSpan;\n orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n className?: string;\n /**\n * Control của trường. Dạng hàm sẽ nhận `FormFieldControlProps` để trải xuống —\n * dạng node thì app tự lo id/aria (dùng khi control đã tự quản, vd Checkbox có nhãn riêng).\n */\n children: ReactNode | ((control: FormFieldControlProps) => ReactNode);\n}\n\nconst colSpanClasses: Record<FormFieldColSpan, string> = {\n 1: \"col-span-12 md:col-span-1\",\n 2: \"col-span-12 md:col-span-2\",\n 3: \"col-span-12 sm:col-span-6 md:col-span-3\",\n 4: \"col-span-12 sm:col-span-6 md:col-span-4\",\n 5: \"col-span-12 sm:col-span-6 md:col-span-5\",\n 6: \"col-span-12 sm:col-span-6\",\n 7: \"col-span-12 md:col-span-7\",\n 8: \"col-span-12 md:col-span-8\",\n 9: \"col-span-12 md:col-span-9\",\n 10: \"col-span-12 md:col-span-10\",\n 11: \"col-span-12 md:col-span-11\",\n 12: \"col-span-12\"\n};\n\n/**\n * Một trường form: nhãn + control + mô tả/lỗi, đặt trong lưới 12 cột của `FormSection`.\n *\n * KHÔNG tự vẽ input. Nhãn và lỗi uỷ quyền xuống primitive `Field`, còn control do app chọn —\n * nếu composite tự dựng input thì mỗi kiểu control mới (combobox, date range, editor) lại phải\n * mở lại file này — một FormField \"đa năng\" ôm hết mọi loại control sẽ phình ra không có điểm dừng.\n */\nexport function FormField({\n label,\n required = false,\n description,\n error,\n htmlFor,\n colSpan = 12,\n orientation = \"vertical\",\n className,\n children\n}: FormFieldProps) {\n const generatedId = useId();\n const id = htmlFor ?? generatedId;\n const hasError = Boolean(error);\n const errorId = `${id}-error`;\n const descriptionId = `${id}-description`;\n\n const control: FormFieldControlProps = {\n id,\n \"aria-invalid\": hasError ? true : undefined,\n \"aria-required\": required ? true : undefined,\n \"aria-describedby\": hasError ? errorId : description ? descriptionId : undefined\n };\n\n return (\n <Field\n data-slot=\"form-field\"\n data-invalid={hasError || undefined}\n orientation={orientation}\n className={cn(colSpanClasses[colSpan], className)}\n >\n {label && (\n <FieldLabel htmlFor={id} className=\"text-sm\">\n {label}\n {required && (\n // Dấu * chỉ là tín hiệu thị giác — trạng thái bắt buộc đã nằm ở aria-required trên\n // control, để trình đọc màn hình khỏi đọc thừa \"sao\".\n // Bậc ink (7,15:1 light / 6,42:1 dark trên `--card`) chứ không `text-destructive`\n // (bậc NỀN, chỉ 3,53:1 trên `--card` ở dark ✗).\n <span aria-hidden=\"true\" className=\"text-destructive-ink\">\n *\n </span>\n )}\n </FieldLabel>\n )}\n\n <FieldContent>\n {typeof children === \"function\" ? children(control) : children}\n\n {hasError ? (\n <FieldError id={errorId} className=\"flex items-center gap-1.5 text-xs animate-in fade-in slide-in-from-top-1\">\n <AlertCircle aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n {error}\n </FieldError>\n ) : (\n description && (\n <FieldDescription id={descriptionId} className=\"text-xs\">\n {description}\n </FieldDescription>\n )\n )}\n </FieldContent>\n </Field>\n );\n}\n"],"mappings":";AAiGQ,SAOI,KAPJ;AA/FR,SAAS,mBAAmB;AAC5B,SAAyB,aAAa;AACtC,SAAS,UAAU;AACnB,SAAS,OAAO,cAAc,kBAAkB,YAAY,kBAAkB;AAsC9E,MAAM,iBAAmD;AAAA,EACvD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AASO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAmB;AACjB,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,WAAW;AACtB,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,gBAAgB,GAAG,EAAE;AAE3B,QAAM,UAAiC;AAAA,IACrC;AAAA,IACA,gBAAgB,WAAW,OAAO;AAAA,IAClC,iBAAiB,WAAW,OAAO;AAAA,IACnC,oBAAoB,WAAW,UAAU,cAAc,gBAAgB;AAAA,EACzE;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc,YAAY;AAAA,MAC1B;AAAA,MACA,WAAW,GAAG,eAAe,OAAO,GAAG,SAAS;AAAA,MAE/C;AAAA,iBACC,qBAAC,cAAW,SAAS,IAAI,WAAU,WAChC;AAAA;AAAA,UACA;AAAA;AAAA;AAAA;AAAA,UAKC,oBAAC,UAAK,eAAY,QAAO,WAAU,wBAAuB,eAE1D;AAAA,WAEJ;AAAA,QAGF,qBAAC,gBACE;AAAA,iBAAO,aAAa,aAAa,SAAS,OAAO,IAAI;AAAA,UAErD,WACC,qBAAC,cAAW,IAAI,SAAS,WAAU,4EACjC;AAAA,gCAAC,eAAY,eAAY,QAAO,WAAU,qBAAoB;AAAA,YAC7D;AAAA,aACH,IAEA,eACE,oBAAC,oBAAiB,IAAI,eAAe,WAAU,WAC5C,uBACH;AAAA,WAGN;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nhãn mặc định của `SearchFilterBar` (`.tsx` vì `resultSummary` trả JSX).
|
|
5
|
+
*
|
|
6
|
+
* Module **CỐ Ý không có `"use client"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy
|
|
7
|
+
* đủ: mọi export của một file `"use client"` đến tay Server Component dưới dạng
|
|
8
|
+
* client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần
|
|
9
|
+
* (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một
|
|
10
|
+
* khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.
|
|
11
|
+
*
|
|
12
|
+
* ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `search-filter-bar.tsx` chỉ được `import`,
|
|
13
|
+
* KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Chuỗi hiển thị + chuỗi a11y của `SearchFilterBar` — cùng khuôn `DataTableLabels`.
|
|
18
|
+
*
|
|
19
|
+
* `searchPlaceholder` và `unitLabel` vốn đã là prop rời và vẫn được giữ (prop thắng `labels`);
|
|
20
|
+
* các khoá còn lại trước đây chôn cứng, trong đó có `sr-only` và nội dung nút mà KHÔNG prop nào
|
|
21
|
+
* chạm tới — app tiếng Anh ghép thanh này với `DataTable` đã i18n sẽ ra giao diện nửa Việt nửa Anh.
|
|
22
|
+
*/
|
|
23
|
+
interface SearchFilterBarLabels {
|
|
24
|
+
/** Placeholder + `aria-label` của ô tìm */
|
|
25
|
+
searchPlaceholder: string;
|
|
26
|
+
/**
|
|
27
|
+
* Đơn vị đếm mặc định ("mục" / "items") — prop `unitLabel` vẫn ghi đè được riêng.
|
|
28
|
+
*
|
|
29
|
+
* Cùng lý do như `TablePaginationLabels.unitLabel`: mặc định là chuỗi theo NGÔN NGỮ nên
|
|
30
|
+
* phải override được qua `labels`, còn prop `unitLabel` là chuỗi theo NỘI DUNG của từng
|
|
31
|
+
* bảng ("việc", "liên hệ") nên vẫn là prop rời và vẫn thắng `labels`.
|
|
32
|
+
*/
|
|
33
|
+
unitLabel: string;
|
|
34
|
+
/** `sr-only` của nút xoá từ khoá trong ô tìm */
|
|
35
|
+
clearSearch: string;
|
|
36
|
+
/** Huy hiệu đếm bộ lọc đang bật: "3 bộ lọc" */
|
|
37
|
+
filterCount: (count: number) => string;
|
|
38
|
+
/** Nhãn nút xoá toàn bộ bộ lọc */
|
|
39
|
+
clearFilters: string;
|
|
40
|
+
/**
|
|
41
|
+
* Dòng đếm kết quả sau lọc: "**3** / 57 mục" (`total` bỏ trống thì chỉ "**3** mục").
|
|
42
|
+
*
|
|
43
|
+
* Trả `ReactNode` chứ không phải chuỗi vì con số cần thẻ riêng (`tabular-nums` để số không
|
|
44
|
+
* nhảy cột, `text-foreground` để nổi trên nền chữ mờ) — ép về chuỗi là mất phần định dạng đó.
|
|
45
|
+
* Là hàm vì trật tự số/đơn vị không giống nhau giữa các ngôn ngữ.
|
|
46
|
+
*/
|
|
47
|
+
resultSummary: (resultCount: number, total: number | undefined, unitLabel: string) => ReactNode;
|
|
48
|
+
}
|
|
49
|
+
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
50
|
+
declare const DEFAULT_SEARCH_FILTER_BAR_LABELS: SearchFilterBarLabels;
|
|
51
|
+
|
|
52
|
+
export { DEFAULT_SEARCH_FILTER_BAR_LABELS, type SearchFilterBarLabels };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
const DEFAULT_SEARCH_FILTER_BAR_LABELS = {
|
|
3
|
+
searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
|
|
4
|
+
unitLabel: "m\u1EE5c",
|
|
5
|
+
clearSearch: "X\xF3a t\u1EEB kh\xF3a",
|
|
6
|
+
filterCount: (count) => `${count} b\u1ED9 l\u1ECDc`,
|
|
7
|
+
clearFilters: "X\xF3a l\u1ECDc",
|
|
8
|
+
resultSummary: (resultCount, total, unitLabel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9
|
+
/* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: resultCount }),
|
|
10
|
+
total !== void 0 ? ` / ${total}` : "",
|
|
11
|
+
" ",
|
|
12
|
+
unitLabel
|
|
13
|
+
] })
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_SEARCH_FILTER_BAR_LABELS
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=search-filter-bar-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/search-filter-bar-constants.tsx"],"sourcesContent":["/**\n * Nhãn mặc định của `SearchFilterBar` (`.tsx` vì `resultSummary` trả JSX).\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một\n * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `search-filter-bar.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\nimport type { ReactNode } from \"react\";\n\n/**\n * Chuỗi hiển thị + chuỗi a11y của `SearchFilterBar` — cùng khuôn `DataTableLabels`.\n *\n * `searchPlaceholder` và `unitLabel` vốn đã là prop rời và vẫn được giữ (prop thắng `labels`);\n * các khoá còn lại trước đây chôn cứng, trong đó có `sr-only` và nội dung nút mà KHÔNG prop nào\n * chạm tới — app tiếng Anh ghép thanh này với `DataTable` đã i18n sẽ ra giao diện nửa Việt nửa Anh.\n */\nexport interface SearchFilterBarLabels {\n /** Placeholder + `aria-label` của ô tìm */\n searchPlaceholder: string;\n /**\n * Đơn vị đếm mặc định (\"mục\" / \"items\") — prop `unitLabel` vẫn ghi đè được riêng.\n *\n * Cùng lý do như `TablePaginationLabels.unitLabel`: mặc định là chuỗi theo NGÔN NGỮ nên\n * phải override được qua `labels`, còn prop `unitLabel` là chuỗi theo NỘI DUNG của từng\n * bảng (\"việc\", \"liên hệ\") nên vẫn là prop rời và vẫn thắng `labels`.\n */\n unitLabel: string;\n /** `sr-only` của nút xoá từ khoá trong ô tìm */\n clearSearch: string;\n /** Huy hiệu đếm bộ lọc đang bật: \"3 bộ lọc\" */\n filterCount: (count: number) => string;\n /** Nhãn nút xoá toàn bộ bộ lọc */\n clearFilters: string;\n /**\n * Dòng đếm kết quả sau lọc: \"**3** / 57 mục\" (`total` bỏ trống thì chỉ \"**3** mục\").\n *\n * Trả `ReactNode` chứ không phải chuỗi vì con số cần thẻ riêng (`tabular-nums` để số không\n * nhảy cột, `text-foreground` để nổi trên nền chữ mờ) — ép về chuỗi là mất phần định dạng đó.\n * Là hàm vì trật tự số/đơn vị không giống nhau giữa các ngôn ngữ.\n */\n resultSummary: (resultCount: number, total: number | undefined, unitLabel: string) => ReactNode;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_SEARCH_FILTER_BAR_LABELS: SearchFilterBarLabels = {\n searchPlaceholder: \"Tìm kiếm…\",\n unitLabel: \"mục\",\n clearSearch: \"Xóa từ khóa\",\n filterCount: (count) => `${count} bộ lọc`,\n clearFilters: \"Xóa lọc\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{resultCount}</strong>\n {total !== undefined ? ` / ${total}` : \"\"} {unitLabel}\n </>\n )\n};\n"],"mappings":"AAwDI,mBACE,KADF;AAPG,MAAM,mCAA0D;AAAA,EACrE,mBAAmB;AAAA,EACnB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa,CAAC,UAAU,GAAG,KAAK;AAAA,EAChC,cAAc;AAAA,EACd,eAAe,CAAC,aAAa,OAAO,cAClC,iCACE;AAAA,wBAAC,YAAO,WAAU,4CAA4C,uBAAY;AAAA,IACzE,UAAU,SAAY,MAAM,KAAK,KAAK;AAAA,IAAG;AAAA,IAAE;AAAA,KAC9C;AAEJ;","names":[]}
|
|
@@ -1,41 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Ref, ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import { SearchFilterBarLabels } from './search-filter-bar-constants.js';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* Chuỗi hiển thị + chuỗi a11y của `SearchFilterBar` — cùng khuôn `DataTableLabels`.
|
|
6
|
-
*
|
|
7
|
-
* `searchPlaceholder` và `unitLabel` vốn đã là prop rời và vẫn được giữ (prop thắng `labels`);
|
|
8
|
-
* các khoá còn lại trước đây chôn cứng, trong đó có `sr-only` và nội dung nút mà KHÔNG prop nào
|
|
9
|
-
* chạm tới — app tiếng Anh ghép thanh này với `DataTable` đã i18n sẽ ra giao diện nửa Việt nửa Anh.
|
|
10
|
-
*/
|
|
11
|
-
interface SearchFilterBarLabels {
|
|
12
|
-
/** Placeholder + `aria-label` của ô tìm */
|
|
13
|
-
searchPlaceholder: string;
|
|
14
|
-
/**
|
|
15
|
-
* Đơn vị đếm mặc định ("mục" / "items") — prop `unitLabel` vẫn ghi đè được riêng.
|
|
16
|
-
*
|
|
17
|
-
* Cùng lý do như `TablePaginationLabels.unitLabel`: mặc định là chuỗi theo NGÔN NGỮ nên
|
|
18
|
-
* phải override được qua `labels`, còn prop `unitLabel` là chuỗi theo NỘI DUNG của từng
|
|
19
|
-
* bảng ("việc", "liên hệ") nên vẫn là prop rời và vẫn thắng `labels`.
|
|
20
|
-
*/
|
|
21
|
-
unitLabel: string;
|
|
22
|
-
/** `sr-only` của nút xoá từ khoá trong ô tìm */
|
|
23
|
-
clearSearch: string;
|
|
24
|
-
/** Huy hiệu đếm bộ lọc đang bật: "3 bộ lọc" */
|
|
25
|
-
filterCount: (count: number) => string;
|
|
26
|
-
/** Nhãn nút xoá toàn bộ bộ lọc */
|
|
27
|
-
clearFilters: string;
|
|
28
|
-
/**
|
|
29
|
-
* Dòng đếm kết quả sau lọc: "**3** / 57 mục" (`total` bỏ trống thì chỉ "**3** mục").
|
|
30
|
-
*
|
|
31
|
-
* Trả `ReactNode` chứ không phải chuỗi vì con số cần thẻ riêng (`tabular-nums` để số không
|
|
32
|
-
* nhảy cột, `text-foreground` để nổi trên nền chữ mờ) — ép về chuỗi là mất phần định dạng đó.
|
|
33
|
-
* Là hàm vì trật tự số/đơn vị không giống nhau giữa các ngôn ngữ.
|
|
34
|
-
*/
|
|
35
|
-
resultSummary: (resultCount: number, total: number | undefined, unitLabel: string) => ReactNode;
|
|
36
|
-
}
|
|
37
|
-
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
38
|
-
declare const DEFAULT_SEARCH_FILTER_BAR_LABELS: SearchFilterBarLabels;
|
|
39
5
|
interface SearchFilterBarProps {
|
|
40
6
|
/** Giá trị ô tìm kiếm (controlled) */
|
|
41
7
|
searchValue: string;
|
|
@@ -73,4 +39,4 @@ interface SearchFilterBarProps {
|
|
|
73
39
|
*/
|
|
74
40
|
declare function SearchFilterBar({ searchValue, onSearchChange, searchPlaceholder, debounceMs, searchInputRef, searchInputProps, children, actions, activeFilterCount, onClearFilters, resultCount, total, unitLabel: unitLabelProp, labels: labelsProp, className }: SearchFilterBarProps): React.JSX.Element;
|
|
75
41
|
|
|
76
|
-
export {
|
|
42
|
+
export { SearchFilterBar, type SearchFilterBarProps };
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Search, SlidersHorizontal, X } from "lucide-react";
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { cn } from "../../lib/cn.js";
|
|
6
6
|
import { Badge } from "../ui/badge.js";
|
|
7
7
|
import { Button } from "../ui/button.js";
|
|
8
8
|
import { Input } from "../ui/input.js";
|
|
9
|
-
|
|
10
|
-
searchPlaceholder: "T\xECm ki\u1EBFm\u2026",
|
|
11
|
-
unitLabel: "m\u1EE5c",
|
|
12
|
-
clearSearch: "X\xF3a t\u1EEB kh\xF3a",
|
|
13
|
-
filterCount: (count) => `${count} b\u1ED9 l\u1ECDc`,
|
|
14
|
-
clearFilters: "X\xF3a l\u1ECDc",
|
|
15
|
-
resultSummary: (resultCount, total, unitLabel) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
16
|
-
/* @__PURE__ */ jsx("strong", { className: "font-medium text-foreground tabular-nums", children: resultCount }),
|
|
17
|
-
total !== void 0 ? ` / ${total}` : "",
|
|
18
|
-
" ",
|
|
19
|
-
unitLabel
|
|
20
|
-
] })
|
|
21
|
-
};
|
|
9
|
+
import { DEFAULT_SEARCH_FILTER_BAR_LABELS } from "./search-filter-bar-constants.js";
|
|
22
10
|
function SearchFilterBar({
|
|
23
11
|
searchValue,
|
|
24
12
|
onSearchChange,
|
|
@@ -141,7 +129,6 @@ function SearchFilterBar({
|
|
|
141
129
|
] });
|
|
142
130
|
}
|
|
143
131
|
export {
|
|
144
|
-
DEFAULT_SEARCH_FILTER_BAR_LABELS,
|
|
145
132
|
SearchFilterBar
|
|
146
133
|
};
|
|
147
134
|
//# sourceMappingURL=search-filter-bar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/search-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Search, SlidersHorizontal, X } from \"lucide-react\";\nimport { type ComponentPropsWithoutRef, type ReactNode, type Ref, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Input } from \"../ui/input.js\";\n\n/**\n * Chuỗi hiển thị + chuỗi a11y của `SearchFilterBar` — cùng khuôn `DataTableLabels`.\n *\n * `searchPlaceholder` và `unitLabel` vốn đã là prop rời và vẫn được giữ (prop thắng `labels`);\n * các khoá còn lại trước đây chôn cứng, trong đó có `sr-only` và nội dung nút mà KHÔNG prop nào\n * chạm tới — app tiếng Anh ghép thanh này với `DataTable` đã i18n sẽ ra giao diện nửa Việt nửa Anh.\n */\nexport interface SearchFilterBarLabels {\n /** Placeholder + `aria-label` của ô tìm */\n searchPlaceholder: string;\n /**\n * Đơn vị đếm mặc định (\"mục\" / \"items\") — prop `unitLabel` vẫn ghi đè được riêng.\n *\n * Cùng lý do như `TablePaginationLabels.unitLabel`: mặc định là chuỗi theo NGÔN NGỮ nên\n * phải override được qua `labels`, còn prop `unitLabel` là chuỗi theo NỘI DUNG của từng\n * bảng (\"việc\", \"liên hệ\") nên vẫn là prop rời và vẫn thắng `labels`.\n */\n unitLabel: string;\n /** `sr-only` của nút xoá từ khoá trong ô tìm */\n clearSearch: string;\n /** Huy hiệu đếm bộ lọc đang bật: \"3 bộ lọc\" */\n filterCount: (count: number) => string;\n /** Nhãn nút xoá toàn bộ bộ lọc */\n clearFilters: string;\n /**\n * Dòng đếm kết quả sau lọc: \"**3** / 57 mục\" (`total` bỏ trống thì chỉ \"**3** mục\").\n *\n * Trả `ReactNode` chứ không phải chuỗi vì con số cần thẻ riêng (`tabular-nums` để số không\n * nhảy cột, `text-foreground` để nổi trên nền chữ mờ) — ép về chuỗi là mất phần định dạng đó.\n * Là hàm vì trật tự số/đơn vị không giống nhau giữa các ngôn ngữ.\n */\n resultSummary: (resultCount: number, total: number | undefined, unitLabel: string) => ReactNode;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_SEARCH_FILTER_BAR_LABELS: SearchFilterBarLabels = {\n searchPlaceholder: \"Tìm kiếm…\",\n unitLabel: \"mục\",\n clearSearch: \"Xóa từ khóa\",\n filterCount: (count) => `${count} bộ lọc`,\n clearFilters: \"Xóa lọc\",\n resultSummary: (resultCount, total, unitLabel) => (\n <>\n <strong className=\"font-medium text-foreground tabular-nums\">{resultCount}</strong>\n {total !== undefined ? ` / ${total}` : \"\"} {unitLabel}\n </>\n )\n};\n\nexport interface SearchFilterBarProps {\n /** Giá trị ô tìm kiếm (controlled) */\n searchValue: string;\n onSearchChange: (value: string) => void;\n searchPlaceholder?: string;\n /** Trễ trước khi gọi `onSearchChange` (ms). 0 = gọi ngay. Gõ tìm gọi API thì đặt 250–350. */\n debounceMs?: number;\n searchInputRef?: Ref<HTMLInputElement>;\n /** Props đặt thẳng lên ô nhập (id, name, aria-label, enterKeyHint…) */\n searchInputProps?: ComponentPropsWithoutRef<\"input\">;\n\n /** Slot bộ lọc cạnh ô tìm (Select, Combobox, DatePicker…) */\n children?: ReactNode;\n /** Slot hành động bên phải (nút Tạo mới, menu cột…) */\n actions?: ReactNode;\n\n /** Số bộ lọc đang bật — hiện huy hiệu và mở dòng đếm kết quả */\n activeFilterCount?: number;\n onClearFilters?: () => void;\n /** Số bản ghi sau lọc */\n resultCount?: number;\n /** Tổng số bản ghi khi chưa lọc */\n total?: number;\n /** Đơn vị đếm theo nội dung bảng, vd \"việc\", \"liên hệ\" — mặc định `labels.unitLabel` (\"mục\") */\n unitLabel?: string;\n /** Ghi đè chuỗi hiển thị/a11y (mặc định tiếng Việt) — xem `SearchFilterBarLabels` */\n labels?: Partial<SearchFilterBarLabels>;\n className?: string;\n}\n\n/**\n * Thanh tìm kiếm + lọc chuẩn cho trang danh sách.\n *\n * Ô tìm giữ giá trị cục bộ và chỉ đẩy lên sau `debounceMs` — nếu bind thẳng vào state của trang,\n * mỗi phím gõ là một lần render cả bảng (và thường là một request).\n *\n * Dòng đếm kết quả có `min-h-7` cố định để việc bật/tắt lọc không làm bảng nhảy lên xuống.\n */\nexport function SearchFilterBar({\n searchValue,\n onSearchChange,\n searchPlaceholder,\n debounceMs = 250,\n searchInputRef,\n searchInputProps,\n children,\n actions,\n activeFilterCount = 0,\n onClearFilters,\n resultCount,\n total,\n unitLabel: unitLabelProp,\n labels: labelsProp,\n className\n}: SearchFilterBarProps) {\n const labels = { ...DEFAULT_SEARCH_FILTER_BAR_LABELS, ...labelsProp };\n const placeholder = searchPlaceholder ?? labels.searchPlaceholder;\n const unitLabel = unitLabelProp ?? labels.unitLabel;\n const [localValue, setLocalValue] = useState(searchValue);\n const lastEmittedRef = useRef(searchValue);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const onSearchChangeRef = useRef(onSearchChange);\n\n useEffect(() => {\n onSearchChangeRef.current = onSearchChange;\n });\n\n // Đồng bộ khi giá trị bị đổi từ bên ngoài (nút \"Xóa lọc\", điều hướng lại trang) — nhưng\n // không đè lên những gì người dùng đang gõ dở.\n useEffect(() => {\n if (searchValue !== lastEmittedRef.current) {\n lastEmittedRef.current = searchValue;\n setLocalValue(searchValue);\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }\n }, [searchValue]);\n\n useEffect(\n () => () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n },\n []\n );\n\n const emit = (value: string) => {\n setLocalValue(value);\n if (timerRef.current) clearTimeout(timerRef.current);\n if (!debounceMs) {\n lastEmittedRef.current = value;\n onSearchChangeRef.current(value);\n return;\n }\n timerRef.current = setTimeout(() => {\n lastEmittedRef.current = value;\n onSearchChangeRef.current(value);\n }, debounceMs);\n };\n\n const clearSearch = () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setLocalValue(\"\");\n lastEmittedRef.current = \"\";\n onSearchChangeRef.current(\"\");\n };\n\n const hasFilters = activeFilterCount > 0;\n const showCount = hasFilters || Boolean(localValue);\n\n return (\n <div data-slot=\"search-filter-bar\" className={cn(\"flex flex-col gap-2\", className)}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative min-w-[12rem] flex-1 sm:max-w-sm\">\n <Search\n aria-hidden=\"true\"\n className=\"absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n />\n <Input\n ref={searchInputRef}\n type=\"search\"\n value={localValue}\n placeholder={placeholder}\n aria-label={placeholder}\n onChange={(event) => emit(event.target.value)}\n className=\"pl-9 pr-8 [&::-webkit-search-cancel-button]:hidden\"\n {...searchInputProps}\n />\n {localValue && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={clearSearch}\n className=\"absolute right-1 top-1/2 size-6 -translate-y-1/2\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{labels.clearSearch}</span>\n </Button>\n )}\n </div>\n\n {children}\n\n {hasFilters && (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <SlidersHorizontal aria-hidden=\"true\" />\n {labels.filterCount(activeFilterCount)}\n </Badge>\n )}\n\n {actions && <div className=\"ml-auto flex items-center gap-2\">{actions}</div>}\n </div>\n\n {showCount && (\n <div role=\"status\" className=\"flex min-h-7 items-center gap-2 text-sm text-muted-foreground\">\n {resultCount !== undefined && <span>{labels.resultSummary(resultCount, total, unitLabel)}</span>}\n {hasFilters && onClearFilters && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={onClearFilters} className=\"h-7 gap-1.5 px-2\">\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n {labels.clearFilters}\n </Button>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/search-filter-bar.tsx"],"sourcesContent":["\"use client\";\n\nimport { Search, SlidersHorizontal, X } from \"lucide-react\";\nimport { type ComponentPropsWithoutRef, type ReactNode, type Ref, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Input } from \"../ui/input.js\";\nimport { DEFAULT_SEARCH_FILTER_BAR_LABELS, type SearchFilterBarLabels } from \"./search-filter-bar-constants.js\";\n\nexport interface SearchFilterBarProps {\n /** Giá trị ô tìm kiếm (controlled) */\n searchValue: string;\n onSearchChange: (value: string) => void;\n searchPlaceholder?: string;\n /** Trễ trước khi gọi `onSearchChange` (ms). 0 = gọi ngay. Gõ tìm gọi API thì đặt 250–350. */\n debounceMs?: number;\n searchInputRef?: Ref<HTMLInputElement>;\n /** Props đặt thẳng lên ô nhập (id, name, aria-label, enterKeyHint…) */\n searchInputProps?: ComponentPropsWithoutRef<\"input\">;\n\n /** Slot bộ lọc cạnh ô tìm (Select, Combobox, DatePicker…) */\n children?: ReactNode;\n /** Slot hành động bên phải (nút Tạo mới, menu cột…) */\n actions?: ReactNode;\n\n /** Số bộ lọc đang bật — hiện huy hiệu và mở dòng đếm kết quả */\n activeFilterCount?: number;\n onClearFilters?: () => void;\n /** Số bản ghi sau lọc */\n resultCount?: number;\n /** Tổng số bản ghi khi chưa lọc */\n total?: number;\n /** Đơn vị đếm theo nội dung bảng, vd \"việc\", \"liên hệ\" — mặc định `labels.unitLabel` (\"mục\") */\n unitLabel?: string;\n /** Ghi đè chuỗi hiển thị/a11y (mặc định tiếng Việt) — xem `SearchFilterBarLabels` */\n labels?: Partial<SearchFilterBarLabels>;\n className?: string;\n}\n\n/**\n * Thanh tìm kiếm + lọc chuẩn cho trang danh sách.\n *\n * Ô tìm giữ giá trị cục bộ và chỉ đẩy lên sau `debounceMs` — nếu bind thẳng vào state của trang,\n * mỗi phím gõ là một lần render cả bảng (và thường là một request).\n *\n * Dòng đếm kết quả có `min-h-7` cố định để việc bật/tắt lọc không làm bảng nhảy lên xuống.\n */\nexport function SearchFilterBar({\n searchValue,\n onSearchChange,\n searchPlaceholder,\n debounceMs = 250,\n searchInputRef,\n searchInputProps,\n children,\n actions,\n activeFilterCount = 0,\n onClearFilters,\n resultCount,\n total,\n unitLabel: unitLabelProp,\n labels: labelsProp,\n className\n}: SearchFilterBarProps) {\n const labels = { ...DEFAULT_SEARCH_FILTER_BAR_LABELS, ...labelsProp };\n const placeholder = searchPlaceholder ?? labels.searchPlaceholder;\n const unitLabel = unitLabelProp ?? labels.unitLabel;\n const [localValue, setLocalValue] = useState(searchValue);\n const lastEmittedRef = useRef(searchValue);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const onSearchChangeRef = useRef(onSearchChange);\n\n useEffect(() => {\n onSearchChangeRef.current = onSearchChange;\n });\n\n // Đồng bộ khi giá trị bị đổi từ bên ngoài (nút \"Xóa lọc\", điều hướng lại trang) — nhưng\n // không đè lên những gì người dùng đang gõ dở.\n useEffect(() => {\n if (searchValue !== lastEmittedRef.current) {\n lastEmittedRef.current = searchValue;\n setLocalValue(searchValue);\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }\n }, [searchValue]);\n\n useEffect(\n () => () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n },\n []\n );\n\n const emit = (value: string) => {\n setLocalValue(value);\n if (timerRef.current) clearTimeout(timerRef.current);\n if (!debounceMs) {\n lastEmittedRef.current = value;\n onSearchChangeRef.current(value);\n return;\n }\n timerRef.current = setTimeout(() => {\n lastEmittedRef.current = value;\n onSearchChangeRef.current(value);\n }, debounceMs);\n };\n\n const clearSearch = () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setLocalValue(\"\");\n lastEmittedRef.current = \"\";\n onSearchChangeRef.current(\"\");\n };\n\n const hasFilters = activeFilterCount > 0;\n const showCount = hasFilters || Boolean(localValue);\n\n return (\n <div data-slot=\"search-filter-bar\" className={cn(\"flex flex-col gap-2\", className)}>\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative min-w-[12rem] flex-1 sm:max-w-sm\">\n <Search\n aria-hidden=\"true\"\n className=\"absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n />\n <Input\n ref={searchInputRef}\n type=\"search\"\n value={localValue}\n placeholder={placeholder}\n aria-label={placeholder}\n onChange={(event) => emit(event.target.value)}\n className=\"pl-9 pr-8 [&::-webkit-search-cancel-button]:hidden\"\n {...searchInputProps}\n />\n {localValue && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={clearSearch}\n className=\"absolute right-1 top-1/2 size-6 -translate-y-1/2\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{labels.clearSearch}</span>\n </Button>\n )}\n </div>\n\n {children}\n\n {hasFilters && (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <SlidersHorizontal aria-hidden=\"true\" />\n {labels.filterCount(activeFilterCount)}\n </Badge>\n )}\n\n {actions && <div className=\"ml-auto flex items-center gap-2\">{actions}</div>}\n </div>\n\n {showCount && (\n <div role=\"status\" className=\"flex min-h-7 items-center gap-2 text-sm text-muted-foreground\">\n {resultCount !== undefined && <span>{labels.resultSummary(resultCount, total, unitLabel)}</span>}\n {hasFilters && onClearFilters && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={onClearFilters} className=\"h-7 gap-1.5 px-2\">\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n {labels.clearFilters}\n </Button>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cỡ trang và nhãn mặc định của `TablePagination`.
|
|
3
|
+
*
|
|
4
|
+
* Module **CỐ Ý không có `"use client"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy
|
|
5
|
+
* đủ: mọi export của một file `"use client"` đến tay Server Component dưới dạng
|
|
6
|
+
* client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần
|
|
7
|
+
* (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một
|
|
8
|
+
* khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `table-pagination.tsx` chỉ được `import`,
|
|
11
|
+
* KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).
|
|
12
|
+
*/
|
|
13
|
+
/** Cỡ trang mặc định — chốt ở 3 mức để mọi bảng trong hệ sinh thái giống nhau. */
|
|
14
|
+
declare const DEFAULT_PAGE_SIZE_OPTIONS: readonly [20, 50, 100];
|
|
15
|
+
/**
|
|
16
|
+
* Toàn bộ chuỗi hiển thị và chuỗi a11y của `TablePagination` — cùng khuôn `DataTableLabels`.
|
|
17
|
+
*
|
|
18
|
+
* Vì sao phải có, dù component đã nhận `label`/`unitLabel` qua prop rời: 4 nhãn `sr-only` của
|
|
19
|
+
* cụm nút ("Trang đầu/trước/sau/cuối"), `aria-label` của ô chọn cỡ trang và hai dòng đếm
|
|
20
|
+
* (`range`, `pageStatus`) KHÔNG có prop nào chạm tới được, nên app tiếng Anh dùng `DataTable`
|
|
21
|
+
* (đã i18n) kèm `TablePagination` (chưa) sẽ ra một cái bảng dịch NỬA VỜI — tệ hơn 0.1.0 vốn
|
|
22
|
+
* nhất quán tiếng Việt.
|
|
23
|
+
*
|
|
24
|
+
* Khoá nào có số/đơn vị chèn vào thì là HÀM: nối chuỗi bằng dấu cộng ở phía app là cách chắc
|
|
25
|
+
* chắn sai trật tự từ với các ngôn ngữ khác.
|
|
26
|
+
*/
|
|
27
|
+
interface TablePaginationLabels {
|
|
28
|
+
/** `aria-label` của vùng phân trang (`<nav>`) — prop `label` vẫn ghi đè được riêng */
|
|
29
|
+
region: string;
|
|
30
|
+
/**
|
|
31
|
+
* Đơn vị đếm mặc định ("mục" / "items") — prop `unitLabel` vẫn ghi đè được riêng.
|
|
32
|
+
*
|
|
33
|
+
* Nằm trong `labels` vì đây là chuỗi theo NGÔN NGỮ: app tiếng Anh override cả bộ `labels`
|
|
34
|
+
* mà vẫn nhận "mục" thì đúng là cái giao diện nửa vời mà cơ chế này sinh ra để chặn. Còn
|
|
35
|
+
* prop `unitLabel` là chuỗi theo NỘI DUNG ("việc", "liên hệ") — mỗi bảng một khác, nên vẫn
|
|
36
|
+
* là prop rời và vẫn thắng `labels`, đúng cặp `label`/`labels.region` ở trên.
|
|
37
|
+
*/
|
|
38
|
+
unitLabel: string;
|
|
39
|
+
/** Chữ đứng trước ô chọn cỡ trang (ẩn ở màn hình hẹp) */
|
|
40
|
+
pageSizePrefix: string;
|
|
41
|
+
/** `aria-label` của ô chọn cỡ trang */
|
|
42
|
+
pageSizeSelect: (unitLabel: string) => string;
|
|
43
|
+
/** Dòng đếm bản ghi: "1–20 trên tổng 57 mục" */
|
|
44
|
+
range: (start: number, end: number, total: number, unitLabel: string) => string;
|
|
45
|
+
/** Dòng đếm trang: "Trang 2 / 5" */
|
|
46
|
+
pageStatus: (page: number, pageCount: number) => string;
|
|
47
|
+
/** `sr-only` của 4 nút điều hướng */
|
|
48
|
+
firstPage: string;
|
|
49
|
+
previousPage: string;
|
|
50
|
+
nextPage: string;
|
|
51
|
+
lastPage: string;
|
|
52
|
+
}
|
|
53
|
+
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
54
|
+
declare const DEFAULT_TABLE_PAGINATION_LABELS: TablePaginationLabels;
|
|
55
|
+
|
|
56
|
+
export { DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_TABLE_PAGINATION_LABELS, type TablePaginationLabels };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const DEFAULT_PAGE_SIZE_OPTIONS = [20, 50, 100];
|
|
2
|
+
const DEFAULT_TABLE_PAGINATION_LABELS = {
|
|
3
|
+
region: "Ph\xE2n trang",
|
|
4
|
+
unitLabel: "m\u1EE5c",
|
|
5
|
+
pageSizePrefix: "Hi\u1EC3n th\u1ECB",
|
|
6
|
+
pageSizeSelect: (unitLabel) => `S\u1ED1 ${unitLabel} m\u1ED7i trang`,
|
|
7
|
+
range: (start, end, total, unitLabel) => `${start}\u2013${end} tr\xEAn t\u1ED5ng ${total} ${unitLabel}`,
|
|
8
|
+
pageStatus: (page, pageCount) => `Trang ${page} / ${pageCount}`,
|
|
9
|
+
firstPage: "Trang \u0111\u1EA7u",
|
|
10
|
+
previousPage: "Trang tr\u01B0\u1EDBc",
|
|
11
|
+
nextPage: "Trang sau",
|
|
12
|
+
lastPage: "Trang cu\u1ED1i"
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
DEFAULT_PAGE_SIZE_OPTIONS,
|
|
16
|
+
DEFAULT_TABLE_PAGINATION_LABELS
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=table-pagination-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/table-pagination-constants.ts"],"sourcesContent":["/**\n * Cỡ trang và nhãn mặc định của `TablePagination`.\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một\n * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `table-pagination.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\n\n/** Cỡ trang mặc định — chốt ở 3 mức để mọi bảng trong hệ sinh thái giống nhau. */\nexport const DEFAULT_PAGE_SIZE_OPTIONS = [20, 50, 100] as const;\n\n/**\n * Toàn bộ chuỗi hiển thị và chuỗi a11y của `TablePagination` — cùng khuôn `DataTableLabels`.\n *\n * Vì sao phải có, dù component đã nhận `label`/`unitLabel` qua prop rời: 4 nhãn `sr-only` của\n * cụm nút (\"Trang đầu/trước/sau/cuối\"), `aria-label` của ô chọn cỡ trang và hai dòng đếm\n * (`range`, `pageStatus`) KHÔNG có prop nào chạm tới được, nên app tiếng Anh dùng `DataTable`\n * (đã i18n) kèm `TablePagination` (chưa) sẽ ra một cái bảng dịch NỬA VỜI — tệ hơn 0.1.0 vốn\n * nhất quán tiếng Việt.\n *\n * Khoá nào có số/đơn vị chèn vào thì là HÀM: nối chuỗi bằng dấu cộng ở phía app là cách chắc\n * chắn sai trật tự từ với các ngôn ngữ khác.\n */\nexport interface TablePaginationLabels {\n /** `aria-label` của vùng phân trang (`<nav>`) — prop `label` vẫn ghi đè được riêng */\n region: string;\n /**\n * Đơn vị đếm mặc định (\"mục\" / \"items\") — prop `unitLabel` vẫn ghi đè được riêng.\n *\n * Nằm trong `labels` vì đây là chuỗi theo NGÔN NGỮ: app tiếng Anh override cả bộ `labels`\n * mà vẫn nhận \"mục\" thì đúng là cái giao diện nửa vời mà cơ chế này sinh ra để chặn. Còn\n * prop `unitLabel` là chuỗi theo NỘI DUNG (\"việc\", \"liên hệ\") — mỗi bảng một khác, nên vẫn\n * là prop rời và vẫn thắng `labels`, đúng cặp `label`/`labels.region` ở trên.\n */\n unitLabel: string;\n /** Chữ đứng trước ô chọn cỡ trang (ẩn ở màn hình hẹp) */\n pageSizePrefix: string;\n /** `aria-label` của ô chọn cỡ trang */\n pageSizeSelect: (unitLabel: string) => string;\n /** Dòng đếm bản ghi: \"1–20 trên tổng 57 mục\" */\n range: (start: number, end: number, total: number, unitLabel: string) => string;\n /** Dòng đếm trang: \"Trang 2 / 5\" */\n pageStatus: (page: number, pageCount: number) => string;\n /** `sr-only` của 4 nút điều hướng */\n firstPage: string;\n previousPage: string;\n nextPage: string;\n lastPage: string;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_TABLE_PAGINATION_LABELS: TablePaginationLabels = {\n region: \"Phân trang\",\n unitLabel: \"mục\",\n pageSizePrefix: \"Hiển thị\",\n pageSizeSelect: (unitLabel) => `Số ${unitLabel} mỗi trang`,\n range: (start, end, total, unitLabel) => `${start}–${end} trên tổng ${total} ${unitLabel}`,\n pageStatus: (page, pageCount) => `Trang ${page} / ${pageCount}`,\n firstPage: \"Trang đầu\",\n previousPage: \"Trang trước\",\n nextPage: \"Trang sau\",\n lastPage: \"Trang cuối\"\n};\n"],"mappings":"AAcO,MAAM,4BAA4B,CAAC,IAAI,IAAI,GAAG;AA0C9C,MAAM,kCAAyD;AAAA,EACpE,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,gBAAgB,CAAC,cAAc,WAAM,SAAS;AAAA,EAC9C,OAAO,CAAC,OAAO,KAAK,OAAO,cAAc,GAAG,KAAK,SAAI,GAAG,sBAAc,KAAK,IAAI,SAAS;AAAA,EACxF,YAAY,CAAC,MAAM,cAAc,SAAS,IAAI,MAAM,SAAS;AAAA,EAC7D,WAAW;AAAA,EACX,cAAc;AAAA,EACd,UAAU;AAAA,EACV,UAAU;AACZ;","names":[]}
|
|
@@ -1,47 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { TablePaginationLabels } from './table-pagination-constants.js';
|
|
2
3
|
|
|
3
|
-
/** Cỡ trang mặc định — chốt ở 3 mức để mọi bảng trong hệ sinh thái giống nhau. */
|
|
4
|
-
declare const DEFAULT_PAGE_SIZE_OPTIONS: readonly [20, 50, 100];
|
|
5
|
-
/**
|
|
6
|
-
* Toàn bộ chuỗi hiển thị và chuỗi a11y của `TablePagination` — cùng khuôn `DataTableLabels`.
|
|
7
|
-
*
|
|
8
|
-
* Vì sao phải có, dù component đã nhận `label`/`unitLabel` qua prop rời: 4 nhãn `sr-only` của
|
|
9
|
-
* cụm nút ("Trang đầu/trước/sau/cuối"), `aria-label` của ô chọn cỡ trang và hai dòng đếm
|
|
10
|
-
* (`range`, `pageStatus`) KHÔNG có prop nào chạm tới được, nên app tiếng Anh dùng `DataTable`
|
|
11
|
-
* (đã i18n) kèm `TablePagination` (chưa) sẽ ra một cái bảng dịch NỬA VỜI — tệ hơn 0.1.0 vốn
|
|
12
|
-
* nhất quán tiếng Việt.
|
|
13
|
-
*
|
|
14
|
-
* Khoá nào có số/đơn vị chèn vào thì là HÀM: nối chuỗi bằng dấu cộng ở phía app là cách chắc
|
|
15
|
-
* chắn sai trật tự từ với các ngôn ngữ khác.
|
|
16
|
-
*/
|
|
17
|
-
interface TablePaginationLabels {
|
|
18
|
-
/** `aria-label` của vùng phân trang (`<nav>`) — prop `label` vẫn ghi đè được riêng */
|
|
19
|
-
region: string;
|
|
20
|
-
/**
|
|
21
|
-
* Đơn vị đếm mặc định ("mục" / "items") — prop `unitLabel` vẫn ghi đè được riêng.
|
|
22
|
-
*
|
|
23
|
-
* Nằm trong `labels` vì đây là chuỗi theo NGÔN NGỮ: app tiếng Anh override cả bộ `labels`
|
|
24
|
-
* mà vẫn nhận "mục" thì đúng là cái giao diện nửa vời mà cơ chế này sinh ra để chặn. Còn
|
|
25
|
-
* prop `unitLabel` là chuỗi theo NỘI DUNG ("việc", "liên hệ") — mỗi bảng một khác, nên vẫn
|
|
26
|
-
* là prop rời và vẫn thắng `labels`, đúng cặp `label`/`labels.region` ở trên.
|
|
27
|
-
*/
|
|
28
|
-
unitLabel: string;
|
|
29
|
-
/** Chữ đứng trước ô chọn cỡ trang (ẩn ở màn hình hẹp) */
|
|
30
|
-
pageSizePrefix: string;
|
|
31
|
-
/** `aria-label` của ô chọn cỡ trang */
|
|
32
|
-
pageSizeSelect: (unitLabel: string) => string;
|
|
33
|
-
/** Dòng đếm bản ghi: "1–20 trên tổng 57 mục" */
|
|
34
|
-
range: (start: number, end: number, total: number, unitLabel: string) => string;
|
|
35
|
-
/** Dòng đếm trang: "Trang 2 / 5" */
|
|
36
|
-
pageStatus: (page: number, pageCount: number) => string;
|
|
37
|
-
/** `sr-only` của 4 nút điều hướng */
|
|
38
|
-
firstPage: string;
|
|
39
|
-
previousPage: string;
|
|
40
|
-
nextPage: string;
|
|
41
|
-
lastPage: string;
|
|
42
|
-
}
|
|
43
|
-
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
44
|
-
declare const DEFAULT_TABLE_PAGINATION_LABELS: TablePaginationLabels;
|
|
45
4
|
interface TablePaginationProps {
|
|
46
5
|
/** Trang hiện tại, đếm từ 1 */
|
|
47
6
|
page: number;
|
|
@@ -70,4 +29,4 @@ interface TablePaginationProps {
|
|
|
70
29
|
*/
|
|
71
30
|
declare function TablePagination({ page, pageSize, total, pageSizeOptions, onPageChange, onPageSizeChange, unitLabel: unitLabelProp, hidePageSize, label, labels: labelsProp, className }: TablePaginationProps): React.JSX.Element;
|
|
72
31
|
|
|
73
|
-
export {
|
|
32
|
+
export { TablePagination, type TablePaginationProps };
|
|
@@ -4,19 +4,10 @@ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-r
|
|
|
4
4
|
import { cn } from "../../lib/cn.js";
|
|
5
5
|
import { Button } from "../ui/button.js";
|
|
6
6
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
pageSizePrefix: "Hi\u1EC3n th\u1ECB",
|
|
12
|
-
pageSizeSelect: (unitLabel) => `S\u1ED1 ${unitLabel} m\u1ED7i trang`,
|
|
13
|
-
range: (start, end, total, unitLabel) => `${start}\u2013${end} tr\xEAn t\u1ED5ng ${total} ${unitLabel}`,
|
|
14
|
-
pageStatus: (page, pageCount) => `Trang ${page} / ${pageCount}`,
|
|
15
|
-
firstPage: "Trang \u0111\u1EA7u",
|
|
16
|
-
previousPage: "Trang tr\u01B0\u1EDBc",
|
|
17
|
-
nextPage: "Trang sau",
|
|
18
|
-
lastPage: "Trang cu\u1ED1i"
|
|
19
|
-
};
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_PAGE_SIZE_OPTIONS,
|
|
9
|
+
DEFAULT_TABLE_PAGINATION_LABELS
|
|
10
|
+
} from "./table-pagination-constants.js";
|
|
20
11
|
function TablePagination({
|
|
21
12
|
page,
|
|
22
13
|
pageSize,
|
|
@@ -99,8 +90,6 @@ function TablePagination({
|
|
|
99
90
|
);
|
|
100
91
|
}
|
|
101
92
|
export {
|
|
102
|
-
DEFAULT_PAGE_SIZE_OPTIONS,
|
|
103
|
-
DEFAULT_TABLE_PAGINATION_LABELS,
|
|
104
93
|
TablePagination
|
|
105
94
|
};
|
|
106
95
|
//# sourceMappingURL=table-pagination.js.map
|