@comitor/ui 3.12.1 → 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/composite/data-table.d.ts +2 -5
- package/dist/components/composite/data-table.js +9 -33
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/form-section.js +5 -5
- package/dist/components/composite/form-section.js.map +1 -1
- package/dist/components/composite/gantt.js +47 -28
- package/dist/components/composite/gantt.js.map +1 -1
- package/dist/components/composite/inline-edit.js +33 -38
- package/dist/components/composite/inline-edit.js.map +1 -1
- package/dist/components/composite/search-filter-bar.js +2 -1
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +17 -15
- package/dist/components/composite/status-config.js +6 -2
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/ui/breadcrumb.js +1 -4
- package/dist/components/ui/breadcrumb.js.map +1 -1
- package/dist/components/ui/button.js +7 -7
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/circular-progress.d.ts +9 -4
- package/dist/components/ui/circular-progress.js +58 -24
- package/dist/components/ui/circular-progress.js.map +1 -1
- package/dist/components/ui/progress.d.ts +6 -0
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/table.d.ts +7 -13
- package/dist/components/ui/table.js +1 -1
- package/dist/components/ui/table.js.map +1 -1
- package/dist/components/ui/tabs-variants.js +2 -2
- package/dist/components/ui/tabs-variants.js.map +1 -1
- package/dist/components/ui/timeline.js +19 -14
- package/dist/components/ui/timeline.js.map +1 -1
- package/dist/i18n/namespaces/file-upload.js +5 -1
- package/dist/i18n/namespaces/file-upload.js.map +1 -1
- package/dist/i18n/namespaces/image-upload-field.js +10 -0
- package/dist/i18n/namespaces/image-upload-field.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/uploader/file-upload-constants.d.ts +26 -2
- package/dist/uploader/file-upload-constants.js.map +1 -1
- package/dist/uploader/file-upload.d.ts +18 -1
- package/dist/uploader/file-upload.js +50 -5
- package/dist/uploader/file-upload.js.map +1 -1
- package/dist/uploader/image-upload-field.d.ts +31 -2
- package/dist/uploader/image-upload-field.js +139 -8
- package/dist/uploader/image-upload-field.js.map +1 -1
- package/dist/uploader/index.d.ts +2 -2
- package/dist/uploader/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/inline-edit.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, X } from \"lucide-react\";\nimport { type KeyboardEvent, useEffect, useRef, useState } from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\n\nexport interface InlineEditProps {\n /** Giá trị hiện tại (controlled từ phía dữ liệu) */\n value: string;\n /**\n * Lưu giá trị mới. Trả Promise thì component hiện trạng thái đang lưu và chỉ thoát chế độ\n * sửa khi lưu xong — thoát sớm sẽ nháy về giá trị cũ rồi mới nhảy sang giá trị mới.\n */\n onSave: (value: string) => void | Promise<void>;\n /**\n * Chữ hiện khi giá trị rỗng, kiêm placeholder của ô nhập (khoá `placeholder` của namespace\n * `inlineEdit` — `ComitorLocaleProvider`, mặc định tiếng Anh).\n */\n placeholder?: string;\n /** Ô nhập nhiều dòng (mô tả task) — Enter xuống dòng, Cmd/Ctrl+Enter mới lưu */\n multiline?: boolean;\n /**\n * Chỉ áp khi `multiline` tắt: phần HIỂN THỊ được xuống dòng thay vì cắt bằng \"…\" (mặc định false).\n * Ô sửa vẫn là MỘT dòng — Enter lưu, giá trị không bao giờ mang `\\n`. Dùng cho tiêu đề dài ở trang\n * chi tiết: `multiline` thì Enter thành ký tự xuống dòng và dữ liệu lưu được `\\n`.\n */\n wrap?: boolean;\n maxLength?: number;\n disabled?: boolean;\n /** Bắt buộc có nội dung: chuỗi rỗng bị coi là huỷ, không lưu (mặc định true) */\n required?: boolean;\n /** Rời ô là lưu (mặc định true) */\n saveOnBlur?: boolean;\n /**\n * Hiện cặp nút ✓/✕ khi đang sửa (mặc định false — thao tác bằng phím). Tên sr-only của hai nút\n * lấy khoá `save` / `cancel` của namespace `inlineEdit`; không có prop rời — đè qua `messages`\n * của `ComitorLocaleProvider`.\n */\n showActions?: boolean;\n /**\n * Nhãn cho trình đọc màn hình, vd \"Sửa tên công việc\" (không truyền thì lấy khoá `ariaLabel` của\n * namespace `inlineEdit`)\n */\n ariaLabel?: string;\n onError?: (error: unknown) => void;\n className?: string;\n /** className riêng cho phần hiển thị (khi chưa sửa) — vd chỉnh cỡ chữ tiêu đề */\n displayClassName?: string;\n}\n\n/**\n * Sửa tại chỗ: bấm vào chữ → thành ô nhập → Enter lưu, Esc huỷ.\n *\n * Vì sao không dùng `<input>` luôn cho gọn: ô nhập luôn hiện làm danh sách trông như một cái\n * form khổng lồ và người dùng dễ sửa nhầm khi chỉ định bấm chọn. Hiển thị dạng chữ cho tới khi\n * người dùng thật sự muốn sửa là ranh giới an toàn.\n */\nexport function InlineEdit({\n value,\n onSave,\n placeholder: placeholderProp,\n multiline = false,\n wrap = false,\n maxLength,\n disabled = false,\n required = true,\n saveOnBlur = true,\n showActions = false,\n ariaLabel: ariaLabelProp,\n onError,\n className,\n displayClassName\n}: InlineEditProps) {\n const t = useComitorMessages(\"inlineEdit\");\n const placeholder = placeholderProp ?? t.placeholder;\n const ariaLabel = ariaLabelProp ?? t.ariaLabel;\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(value);\n const [saving, setSaving] = useState(false);\n const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);\n\n // Giá trị đổi từ ngoài (người khác sửa qua realtime) — chỉ đồng bộ khi không sửa dở.\n useEffect(() => {\n if (!editing) setDraft(value);\n }, [value, editing]);\n\n useEffect(() => {\n if (!editing) return;\n const element = inputRef.current;\n if (!element) return;\n element.focus();\n element.setSelectionRange(element.value.length, element.value.length);\n }, [editing]);\n\n const cancel = () => {\n setDraft(value);\n setEditing(false);\n };\n\n const commit = async () => {\n const next = draft.trim();\n if (next === value.trim() || (required && next === \"\")) {\n cancel();\n return;\n }\n\n try {\n setSaving(true);\n await onSave(next);\n setSaving(false);\n setEditing(false);\n } catch (error) {\n setSaving(false);\n // Giữ nguyên chế độ sửa: người dùng không mất chữ vừa gõ khi mạng lỗi.\n if (onError) onError(error);\n else console.error(\"[InlineEdit] onSave thất bại:\", error);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n cancel();\n return;\n }\n if (event.key === \"Enter\" && (!multiline || event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n void commit();\n }\n };\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n disabled={disabled}\n aria-label={ariaLabel}\n onClick={() => setEditing(true)}\n className={cn(\n \"-mx-1 w-full rounded-md px-1 py-0.5 text-left transition-colors duration-150 ease-out\",\n \"hover:bg-muted disabled:pointer-events-none disabled:opacity-60\",\n !value && \"text-muted-foreground\",\n className,\n displayClassName\n )}\n >\n <span\n className={cn(\n multiline ? \"whitespace-pre-wrap\" : wrap ? \"block whitespace-normal break-words\" : \"block truncate\"\n )}\n >\n {value || placeholder}\n </span>\n </button>\n );\n }\n\n const sharedProps = {\n value: draft,\n maxLength,\n placeholder,\n disabled: saving,\n \"aria-label\": ariaLabel,\n onKeyDown: handleKeyDown,\n onBlur: saveOnBlur ? () => void commit() : undefined,\n className: cn(\n \"w-full rounded-md border border-input bg-background px-1.5 py-0.5 text-sm text-foreground\",\n \"focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\",\n \"disabled:opacity-60\",\n className\n )\n };\n\n return (\n <div className=\"flex items-start gap-1\">\n {multiline ? (\n <textarea\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n rows={3}\n onChange={(event) => setDraft(event.target.value)}\n />\n ) : (\n <input\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n type=\"text\"\n onChange={(event) => setDraft(event.target.value)}\n />\n )}\n\n {showActions && (\n <div className=\"flex shrink-0 items-center gap-0.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={saving}\n // onMouseDown: chạy TRƯỚC blur của ô nhập, nếu để onClick thì blur đã huỷ/lưu mất rồi.\n onMouseDown={(event) => {\n event.preventDefault();\n void commit();\n }}\n >\n <Check aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{t.save}</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={saving}\n onMouseDown={(event) => {\n event.preventDefault();\n cancel();\n }}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{t.cancel}</span>\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/inline-edit.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, X } from \"lucide-react\";\nimport { type KeyboardEvent, useEffect, useRef, useState } from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\n\nexport interface InlineEditProps {\n /** Giá trị hiện tại (controlled từ phía dữ liệu) */\n value: string;\n /**\n * Lưu giá trị mới. Trả Promise thì component hiện trạng thái đang lưu và chỉ thoát chế độ\n * sửa khi lưu xong — thoát sớm sẽ nháy về giá trị cũ rồi mới nhảy sang giá trị mới.\n */\n onSave: (value: string) => void | Promise<void>;\n /**\n * Chữ hiện khi giá trị rỗng, kiêm placeholder của ô nhập (khoá `placeholder` của namespace\n * `inlineEdit` — `ComitorLocaleProvider`, mặc định tiếng Anh).\n */\n placeholder?: string;\n /** Ô nhập nhiều dòng (mô tả task) — Enter xuống dòng, Cmd/Ctrl+Enter mới lưu */\n multiline?: boolean;\n /**\n * Chỉ áp khi `multiline` tắt: phần HIỂN THỊ được xuống dòng thay vì cắt bằng \"…\" (mặc định false).\n * Ô sửa vẫn là MỘT dòng — Enter lưu, giá trị không bao giờ mang `\\n`. Dùng cho tiêu đề dài ở trang\n * chi tiết: `multiline` thì Enter thành ký tự xuống dòng và dữ liệu lưu được `\\n`.\n */\n wrap?: boolean;\n maxLength?: number;\n disabled?: boolean;\n /** Bắt buộc có nội dung: chuỗi rỗng bị coi là huỷ, không lưu (mặc định true) */\n required?: boolean;\n /** Rời ô là lưu (mặc định true) */\n saveOnBlur?: boolean;\n /**\n * Hiện cặp nút ✓/✕ khi đang sửa (mặc định false — thao tác bằng phím). Tên sr-only của hai nút\n * lấy khoá `save` / `cancel` của namespace `inlineEdit`; không có prop rời — đè qua `messages`\n * của `ComitorLocaleProvider`.\n */\n showActions?: boolean;\n /**\n * Nhãn cho trình đọc màn hình, vd \"Sửa tên công việc\" (không truyền thì lấy khoá `ariaLabel` của\n * namespace `inlineEdit`)\n */\n ariaLabel?: string;\n onError?: (error: unknown) => void;\n className?: string;\n /** className riêng cho phần hiển thị (khi chưa sửa) — vd chỉnh cỡ chữ tiêu đề */\n displayClassName?: string;\n}\n\n/**\n * Sửa tại chỗ: bấm vào chữ → thành ô nhập → Enter lưu, Esc huỷ.\n *\n * Vì sao không dùng `<input>` luôn cho gọn: ô nhập luôn hiện làm danh sách trông như một cái\n * form khổng lồ và người dùng dễ sửa nhầm khi chỉ định bấm chọn. Hiển thị dạng chữ cho tới khi\n * người dùng thật sự muốn sửa là ranh giới an toàn.\n */\nexport function InlineEdit({\n value,\n onSave,\n placeholder: placeholderProp,\n multiline = false,\n wrap = false,\n maxLength,\n disabled = false,\n required = true,\n saveOnBlur = true,\n showActions = false,\n ariaLabel: ariaLabelProp,\n onError,\n className,\n displayClassName\n}: InlineEditProps) {\n const t = useComitorMessages(\"inlineEdit\");\n const placeholder = placeholderProp ?? t.placeholder;\n const ariaLabel = ariaLabelProp ?? t.ariaLabel;\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(value);\n const [saving, setSaving] = useState(false);\n const inputRef = useRef<HTMLTextAreaElement | null>(null);\n\n // Giá trị đổi từ ngoài (người khác sửa qua realtime) — chỉ đồng bộ khi không sửa dở.\n useEffect(() => {\n if (!editing) setDraft(value);\n }, [value, editing]);\n\n useEffect(() => {\n if (!editing) return;\n const element = inputRef.current;\n if (!element) return;\n element.focus();\n element.setSelectionRange(element.value.length, element.value.length);\n }, [editing]);\n\n const cancel = () => {\n setDraft(value);\n setEditing(false);\n };\n\n const commit = async () => {\n const next = draft.trim();\n if (next === value.trim() || (required && next === \"\")) {\n cancel();\n return;\n }\n\n try {\n setSaving(true);\n await onSave(next);\n setSaving(false);\n setEditing(false);\n } catch (error) {\n setSaving(false);\n // Giữ nguyên chế độ sửa: người dùng không mất chữ vừa gõ khi mạng lỗi.\n if (onError) onError(error);\n else console.error(\"[InlineEdit] onSave thất bại:\", error);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n cancel();\n return;\n }\n if (event.key === \"Enter\" && (!multiline || event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n void commit();\n }\n };\n\n // Cách chữ chảy — DÙNG CHUNG cho phần hiển thị và khối giữ chỗ lúc sửa, để hai bên đo ra cùng một kích thước.\n const textFlow = multiline\n ? \"block whitespace-pre-wrap\"\n : wrap\n ? \"block whitespace-normal break-words\"\n : \"block truncate\";\n // Khung của phần hiển thị — khối giữ chỗ lúc sửa mượn y nguyên.\n const box = cn(\"-mx-1 block w-full rounded-md px-1 py-0.5 text-left\", className, displayClassName);\n\n if (!editing) {\n return (\n <button\n type=\"button\"\n disabled={disabled}\n aria-label={ariaLabel}\n onClick={() => setEditing(true)}\n className={cn(\n // `block`, không để `<button>` mặc định `inline-block`: nút inline nằm trong một dòng chữ, và dòng đó cao\n // hơn nút (chừa chỗ chân chữ) — khối cao thêm ~4,5px so với ô sửa, nên bấm vào sửa là mọi thứ bên dưới nhảy lên.\n box,\n \"transition-colors duration-150 ease-out hover:bg-muted disabled:pointer-events-none disabled:opacity-60\",\n !value && \"text-muted-foreground\"\n )}\n >\n <span className={textFlow}>{value || placeholder}</span>\n </button>\n );\n }\n\n const sharedProps = {\n value: draft,\n maxLength,\n placeholder,\n disabled: saving,\n \"aria-label\": ariaLabel,\n onKeyDown: handleKeyDown,\n onBlur: saveOnBlur ? () => void commit() : undefined,\n /*\n * Cùng `px-1 py-0.5`, cùng cỡ/dòng chữ (thừa hưởng, không ép `text-sm`), cùng `className`/`displayClassName` với\n * phần hiển thị. Viền vẽ bằng `ring` (bóng, không chiếm chỗ) thay `border`, vì viền 1px đẩy chữ lệch 1px.\n */\n className: cn(\n \"absolute inset-y-0 -left-1 right-1 resize-none overflow-hidden rounded-md bg-background px-1 py-0.5 text-foreground\",\n \"ring-1 ring-input ring-inset focus:outline-none focus:ring-2 focus:ring-primary-ink\",\n \"disabled:opacity-60\",\n className,\n displayClassName\n )\n };\n\n return (\n <div className=\"flex items-start gap-1\">\n {/*\n KHÔNG để ô sửa tự quyết kích thước. Một khối GIỮ CHỖ vô hình — y nguyên khung và cách chảy chữ của phần hiển\n thị, chứa bản nháp — nằm trong luồng; `<textarea>` đè `absolute` lên đúng khung đó. Bấm vào sửa vì thế không\n đổi kích thước gì: không cao/thấp đi (nút hiển thị `inline-block`, `rows`, `wrap`), không rộng/hẹp đi (bảng\n `table-layout: auto` chia cột theo bề rộng tự nhiên — của `<textarea>` là ~20 ký tự, của chữ là chính nó). Khi\n gõ, khối giữ chỗ lớn theo bản nháp, như chữ thật.\n\n Cả một dòng cũng là `<textarea>`, không `<input>`: trình duyệt căn dọc chữ trong `<input>` theo cách riêng và\n làm tròn tới pixel, nên chữ nhích ~0,5px CSS (1 điểm ảnh thật ở màn 2x) khi bấm vào sửa.\n */}\n <div className=\"relative min-w-0 flex-1\">\n <span aria-hidden=\"true\" className={cn(box, \"invisible\")}>\n {/* `\\u200b`: dòng trống cuối của bản nháp nhiều dòng vẫn được tính chiều cao. */}\n <span className={textFlow}>{draft ? `${draft}\\u200b` : placeholder}</span>\n </span>\n <textarea\n {...sharedProps}\n ref={(element) => {\n inputRef.current = element;\n }}\n rows={1}\n {...(multiline\n ? {}\n : wrap\n ? { \"aria-multiline\": false }\n : { \"aria-multiline\": false, wrap: \"off\", className: cn(sharedProps.className, \"whitespace-pre\") })}\n onChange={(event) => setDraft(multiline ? event.target.value : event.target.value.replace(/[\\r\\n]+/g, \" \"))}\n />\n </div>\n\n {showActions && (\n <div className=\"flex shrink-0 items-center gap-0.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n // 28px = đúng chiều cao ô sửa một dòng; nút 32px mặc định làm hàng cao thêm 4px khi vào chế độ sửa.\n className=\"size-7\"\n disabled={saving}\n // onMouseDown: chạy TRƯỚC blur của ô nhập, nếu để onClick thì blur đã huỷ/lưu mất rồi.\n onMouseDown={(event) => {\n event.preventDefault();\n void commit();\n }}\n >\n <Check aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{t.save}</span>\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n // 28px = đúng chiều cao ô sửa một dòng; nút 32px mặc định làm hàng cao thêm 4px khi vào chế độ sửa.\n className=\"size-7\"\n disabled={saving}\n onMouseDown={(event) => {\n event.preventDefault();\n cancel();\n }}\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">{t.cancel}</span>\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -113,7 +113,8 @@ function SearchFilterBar({
|
|
|
113
113
|
)
|
|
114
114
|
] }),
|
|
115
115
|
children,
|
|
116
|
-
hasFilters &&
|
|
116
|
+
hasFilters && // Cao `h-9` như ô tìm, Select và Button `md` cùng dòng — Badge `md` mặc định chỉ cao 20px.
|
|
117
|
+
/* @__PURE__ */ jsxs(Badge, { variant: "secondary", className: "h-9 gap-1.5 px-3 text-sm [&>svg]:size-3.5", children: [
|
|
117
118
|
/* @__PURE__ */ jsx(SlidersHorizontal, { "aria-hidden": "true" }),
|
|
118
119
|
labels.filterCount(activeFilterCount)
|
|
119
120
|
] }),
|
|
@@ -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 { useComitorLabels } from \"../../i18n/locale-provider.js\";\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 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 \"tasks\", \"contacts\" — mặc định `labels.unitLabel` (\"items\") */\n unitLabel?: string;\n /**\n * Ghi đè chuỗi hiển thị/a11y — không truyền thì theo namespace `searchFilterBar` của\n * `ComitorLocaleProvider` (mặc định tiếng Anh). Xem `SearchFilterBarLabels`.\n */\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 = useComitorLabels(\"searchFilterBar\", 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 xoá bộ 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":";AAoIU,cAeE,YAfF;AAlIV,SAAS,QAAQ,mBAAmB,SAAS;AAC7C,SAAkE,WAAW,QAAQ,gBAAgB;AACrG,SAAS,wBAAwB;AACjC,SAAS,UAAU;AACnB,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,aAAa;AA4Cf,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,QAAQ;AAAA,EACR;AACF,GAAyB;AACvB,QAAM,SAAS,iBAAiB,mBAAmB,UAAU;AAC7D,QAAM,cAAc,qBAAqB,OAAO;AAChD,QAAM,YAAY,iBAAiB,OAAO;AAC1C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,WAAW;AACxD,QAAM,iBAAiB,OAAO,WAAW;AACzC,QAAM,WAAW,OAA6C,IAAI;AAClE,QAAM,oBAAoB,OAAO,cAAc;AAE/C,YAAU,MAAM;AACd,sBAAkB,UAAU;AAAA,EAC9B,CAAC;AAID,YAAU,MAAM;AACd,QAAI,gBAAgB,eAAe,SAAS;AAC1C,qBAAe,UAAU;AACzB,oBAAc,WAAW;AACzB,UAAI,SAAS,SAAS;AACpB,qBAAa,SAAS,OAAO;AAC7B,iBAAS,UAAU;AAAA,MACrB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB;AAAA,IACE,MAAM,MAAM;AACV,UAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AAAA,IACrD;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,OAAO,CAAC,UAAkB;AAC9B,kBAAc,KAAK;AACnB,QAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AACnD,QAAI,CAAC,YAAY;AACf,qBAAe,UAAU;AACzB,wBAAkB,QAAQ,KAAK;AAC/B;AAAA,IACF;AACA,aAAS,UAAU,WAAW,MAAM;AAClC,qBAAe,UAAU;AACzB,wBAAkB,QAAQ,KAAK;AAAA,IACjC,GAAG,UAAU;AAAA,EACf;AAEA,QAAM,cAAc,MAAM;AACxB,QAAI,SAAS,SAAS;AACpB,mBAAa,SAAS,OAAO;AAC7B,eAAS,UAAU;AAAA,IACrB;AACA,kBAAc,EAAE;AAChB,mBAAe,UAAU;AACzB,sBAAkB,QAAQ,EAAE;AAAA,EAC9B;AAEA,QAAM,aAAa,oBAAoB;AACvC,QAAM,YAAY,cAAc,QAAQ,UAAU;AAElD,SACE,qBAAC,SAAI,aAAU,qBAAoB,WAAW,GAAG,uBAAuB,SAAS,GAC/E;AAAA,yBAAC,SAAI,WAAU,qCACb;AAAA,2BAAC,SAAI,WAAU,6CACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAU;AAAA;AAAA,QACZ;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,OAAO;AAAA,YACP;AAAA,YACA,cAAY;AAAA,YACZ,UAAU,CAAC,UAAU,KAAK,MAAM,OAAO,KAAK;AAAA,YAC5C,WAAU;AAAA,YACT,GAAG;AAAA;AAAA,QACN;AAAA,QACC,cACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAU;AAAA,YAEV;AAAA,kCAAC,KAAE,eAAY,QAAO,WAAU,YAAW;AAAA,cAC3C,oBAAC,UAAK,WAAU,WAAW,iBAAO,aAAY;AAAA;AAAA;AAAA,QAChD;AAAA,SAEJ;AAAA,MAEC;AAAA,MAEA,
|
|
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 { useComitorLabels } from \"../../i18n/locale-provider.js\";\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 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 \"tasks\", \"contacts\" — mặc định `labels.unitLabel` (\"items\") */\n unitLabel?: string;\n /**\n * Ghi đè chuỗi hiển thị/a11y — không truyền thì theo namespace `searchFilterBar` của\n * `ComitorLocaleProvider` (mặc định tiếng Anh). Xem `SearchFilterBarLabels`.\n */\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 = useComitorLabels(\"searchFilterBar\", 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 xoá bộ 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 // Cao `h-9` như ô tìm, Select và Button `md` cùng dòng — Badge `md` mặc định chỉ cao 20px.\n <Badge variant=\"secondary\" className=\"h-9 gap-1.5 px-3 text-sm [&>svg]:size-3.5\">\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":[]}
|
|
@@ -59,8 +59,8 @@ interface StatusConfig<T extends string = string> extends StatusTone {
|
|
|
59
59
|
* ⚠ ĐỪNG để đây một lớp nền có alpha (`bg-destructive/5`…) nếu bảng có cột GHIM hoặc có
|
|
60
60
|
* `striped`. Ô cột ghim lấy nền bằng `bg-inherit` từ hàng nên nó thừa hưởng luôn alpha và
|
|
61
61
|
* trở nên trong mờ; còn một class nền trần thì thua sọc của `striped` về độ đặc tả nên mất
|
|
62
|
-
* hẳn ở hàng chẵn. Dùng
|
|
63
|
-
* thái bằng chip + màu chữ.
|
|
62
|
+
* hẳn ở hàng chẵn. Dùng nền ĐỤC tô trên từng ô (như `isRowActive` của `DataTable`:
|
|
63
|
+
* `[&>td]:bg-accent`) hoặc nói trạng thái bằng chip + màu chữ.
|
|
64
64
|
*/
|
|
65
65
|
rowClassName?: string;
|
|
66
66
|
}
|
|
@@ -90,7 +90,7 @@ interface StatusConfig<T extends string = string> extends StatusTone {
|
|
|
90
90
|
* destructive 5,76:1 (L) · 5,71:1 (D) 7,15:1 (L) · 6,84:1 (D)
|
|
91
91
|
* warning 5,93:1 (L) · 7,19:1 (D) 6,51:1 (L) · 12,35:1 (D)
|
|
92
92
|
* severe 5,55:1 (L) · 6,09:1 (D) 6,66:1 (L) · 8,36:1 (D) ← chỉ outline; chấm: dưới
|
|
93
|
-
* neutral
|
|
93
|
+
* neutral (chữ `--foreground` — xem mục `neutral` bên dưới)
|
|
94
94
|
* primary 5,77:1 (L) · 7,26:1 (D) 6,51:1 (L) · 12,67:1 (D)
|
|
95
95
|
*
|
|
96
96
|
* ─ TONE `severe` (cam) — nấc giữa `warning` và `destructive` ─
|
|
@@ -111,15 +111,17 @@ interface StatusConfig<T extends string = string> extends StatusTone {
|
|
|
111
111
|
* `warning` không còn cần cặp `text-gold-500 dark:text-warning`: `--warning-ink` ĐÚNG BẰNG
|
|
112
112
|
* `--gold-500` ở light và `--yellow` ở dark — cùng một màu, ít hơn một class và một nơi để lệch.
|
|
113
113
|
*
|
|
114
|
-
* ─ TONE `primary
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
114
|
+
* ─ TONE `primary`: NỀN theo `--primary`, CHỮ và VIỀN theo thang `gold-*` ─
|
|
115
|
+
* Nền là `bg-primary/20` (alpha `/20`, không `/15`, để tint vàng nổi ngang `warning`: 1,13:1 với nền trang).
|
|
116
|
+
* Chữ và viền KHÔNG dùng `--primary-ink`: ở bảng màu MẶC ĐỊNH (bản duyệt comitor-ds) `--primary-ink` chính là
|
|
117
|
+
* vàng thương hiệu `#E8B824`, nên chữ vàng trên tint vàng chỉ ~1,55:1 — chip "primary" gần như không đọc được.
|
|
118
|
+
* Dùng đúng cặp của tone `gold` ở avatar (`lib/avatar-tone.ts`): `text-gold-600` light (7,77:1 trên tint),
|
|
119
|
+
* `text-gold-100` dark (11,85:1). Cái giá: app đặt `--primary` khác vàng thì nền chip đổi theo còn chữ vẫn vàng —
|
|
120
|
+
* đã cân và chọn chữ đọc được. `Gantt` đi cùng cặp này.
|
|
121
|
+
*
|
|
122
|
+
* ─ TONE `neutral`: chữ `--foreground`, không `--muted-foreground` ─
|
|
123
|
+
* `--muted-foreground` trên `--muted` ở bảng mặc định chỉ ~3,58:1 — chip "Draft" nhạt hơn mọi tone khác. Viền
|
|
124
|
+
* (`outline`) giữ `--muted-foreground`: nó chỉ cần 3:1 với nền trang, và viền đen sẽ nặng hơn mọi viền tone.
|
|
123
125
|
*
|
|
124
126
|
* ─ VIỀN: VÌ SAO BỎ HẲN ALPHA ─
|
|
125
127
|
* Ở `variant="outline"` nền trong suốt nên viền là ranh giới DUY NHẤT của chip → ngưỡng 3:1 của
|
|
@@ -136,13 +138,13 @@ interface StatusConfig<T extends string = string> extends StatusTone {
|
|
|
136
138
|
declare const STATUS_TONES: {
|
|
137
139
|
readonly neutral: {
|
|
138
140
|
readonly bgColor: "bg-muted";
|
|
139
|
-
readonly textColor: "text-
|
|
141
|
+
readonly textColor: "text-foreground";
|
|
140
142
|
readonly borderColor: "border-muted-foreground";
|
|
141
143
|
};
|
|
142
144
|
readonly primary: {
|
|
143
145
|
readonly bgColor: "bg-primary/20";
|
|
144
|
-
readonly textColor: "text-
|
|
145
|
-
readonly borderColor: "border-
|
|
146
|
+
readonly textColor: "text-gold-600 dark:text-gold-100";
|
|
147
|
+
readonly borderColor: "border-gold-600 dark:border-gold-100";
|
|
146
148
|
};
|
|
147
149
|
readonly success: {
|
|
148
150
|
readonly bgColor: "bg-success/15";
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
const STATUS_TONES = {
|
|
2
2
|
neutral: {
|
|
3
3
|
bgColor: "bg-muted",
|
|
4
|
-
textColor: "text-
|
|
4
|
+
textColor: "text-foreground",
|
|
5
5
|
borderColor: "border-muted-foreground"
|
|
6
6
|
},
|
|
7
|
-
primary: {
|
|
7
|
+
primary: {
|
|
8
|
+
bgColor: "bg-primary/20",
|
|
9
|
+
textColor: "text-gold-600 dark:text-gold-100",
|
|
10
|
+
borderColor: "border-gold-600 dark:border-gold-100"
|
|
11
|
+
},
|
|
8
12
|
success: { bgColor: "bg-success/15", textColor: "text-success-ink", borderColor: "border-success-ink" },
|
|
9
13
|
warning: { bgColor: "bg-warning/20", textColor: "text-warning-ink", borderColor: "border-warning-ink" },
|
|
10
14
|
severe: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/status-config.ts"],"sourcesContent":["/**\n * Cơ chế StatusConfig generic — app khai map \"trạng thái → lớp màu TOKEN\".\n *\n * Vì sao generic thay vì liệt kê sẵn trạng thái: mỗi app Comitor có bộ trạng thái riêng\n * (Tasks: todo/doing/done; CRM: lead/qualified/won/lost; HR: nộp/duyệt/từ chối). Package\n * chỉ cấp KIỂU và bảng tone, còn nhãn tiếng Việt là chuyện của app.\n *\n * Bắt buộc dùng class token (`bg-success/15 text-success-ink`), KHÔNG hex — hex sẽ chết ở dark mode.\n *\n * @example\n * const TASK_STATUSES: StatusConfig<TaskStatus>[] = [\n * { value: \"todo\", label: \"Cần làm\", shortLabel: \"CL\", ...STATUS_TONES.neutral },\n * { value: \"doing\", label: \"Đang làm\", shortLabel: \"ĐL\", ...STATUS_TONES.info },\n * { value: \"done\", label: \"Hoàn thành\", shortLabel: \"HT\", ...STATUS_TONES.success }\n * ];\n */\n\nexport interface StatusTone {\n /** Lớp nền tint — token TÔ NỀN, vd \"bg-success/15\" */\n bgColor: string;\n /**\n * Lớp chữ — phải là bậc **ink** (`text-success-ink`), KHÔNG phải token tô nền (`text-success`).\n * Chữ chip đứng trên tint của chính màu đó nên đo theo AA 4,5:1; bậc tô nền không đạt ở light.\n */\n textColor: string;\n /**\n * Lớp viền (tuỳ chọn) — dùng cho biến thể `outline`. Ở biến thể đó nền trong suốt nên viền là\n * thứ DUY NHẤT khoanh vùng chip → chịu ngưỡng 3:1 của WCAG 1.4.11. Dùng cùng bậc ink với\n * `textColor` (`border-success-ink`), đừng hạ alpha xuống `/40`.\n */\n borderColor?: string;\n /**\n * Lớp tô CHẤM của `StatusPill variant=\"dot\"` (tuỳ chọn) — một token TÔ NỀN viết thành lớp nền\n * TĨNH (`\"bg-severe\"`), sơn làm `background-color` của chấm 6px. Không khai thì chấm giữ cách\n * cũ: lấy `currentColor` của bậc ink trong `textColor`.\n *\n * Vì sao cần: bậc ink được chọn để đủ tối cho CHỮ, và ở cỡ chấm một bậc tối đọc ra một đốm xỉn\n * — `--warning-ink` #7A5800 là \"ô-liu\" chứ không phải \"gấp\". Chấm là hình, không phải chữ, nên\n * nó chỉ chịu ngưỡng 3:1 của WCAG 1.4.11 và dùng được chính bậc tô nền nếu bậc đó đủ đậm.\n *\n * ⚠ Token phải đạt **3:1 với nền trang ở CẢ hai theme** (--background/--card, và nên cả\n * --muted vì chấm hay nằm trong dòng bảng). Bậc tô nền của các tone khác KHÔNG đạt: `--warning`\n * 1,59 · `--primary` 1,86:1 trên nền trắng ✗; `--success` qua nền trắng (3,13) nhưng tụt trên\n * --muted (2,92:1 ✗) — nên đừng thêm trường này cho tone nào chưa đo. Cổng tương phản KHÔNG\n * thấy được cặp này: chuỗi lớp nằm ở bảng tone, còn phần tử chấm nằm ở `status-pill.tsx` — số đo\n * ghi tay ở bảng dưới là bằng chứng duy nhất. Như mọi trường ở đây: lớp TĨNH, không `bg-${tone}`.\n */\n dotColor?: string;\n}\n\nexport interface StatusConfig<T extends string = string> extends StatusTone {\n value: T;\n label: string;\n /** Nhãn rút gọn cho bảng/chip hẹp (vd \"HT\" thay \"Hoàn thành\") */\n shortLabel?: string;\n /** Ưu tiên sắp xếp (nhỏ = lên trước) */\n priority?: number;\n /**\n * Class thêm cho DÒNG bảng khi trạng thái cần gây chú ý (vd quá hạn).\n *\n * ⚠ ĐỪNG để đây một lớp nền có alpha (`bg-destructive/5`…) nếu bảng có cột GHIM hoặc có\n * `striped`. Ô cột ghim lấy nền bằng `bg-inherit` từ hàng nên nó thừa hưởng luôn alpha và\n * trở nên trong mờ; còn một class nền trần thì thua sọc của `striped` về độ đặc tả nên mất\n * hẳn ở hàng chẵn. Dùng `outline` (như `isRowActive` của `DataTable` đang làm) hoặc nói trạng\n * thái bằng chip + màu chữ.\n */\n rowClassName?: string;\n}\n\n/**\n * Bảng tone dựng sẵn theo token semantic — trải (`...STATUS_TONES.success`) vào StatusConfig.\n *\n * Mọi chuỗi ở đây là class TĨNH để Tailwind quét được từ `dist`; tuyệt đối không sinh động\n * kiểu `bg-${tone}/15` (class động bị loại khỏi bundle của app).\n *\n * ─ VÌ SAO CHỮ VÀ VIỀN ĐỀU LÀ BẬC `--x-ink`, KHÔNG PHẢI `--x` ─\n *\n * `--success/--warning/--info/--destructive` là token TÔ NỀN. Đặt chúng làm màu CHỮ thì bản light\n * hỏng ở cả ba biến thể của `StatusPill` (đo theo WCAG, nền tint đã trộn với `--card` trắng):\n *\n * text-success trên bg-success/15 (#E0F2E9) = 2,69:1 ✗ (dưới cả ngưỡng 3:1)\n * text-info trên bg-info/15 (#DDEDEE) = 3,44:1 ✗\n * text-destructive trên bg-destructive/15 = 4,06:1 ✗\n * text-success / text-info trên nền TRANG (outline, dot) = 3,13:1 / 4,14:1 ✗\n *\n * Đi qua bậc ink thì đạt ở CẢ hai theme (`--x-ink` ở `.dark` vốn trỏ về chính `--x`, nên không\n * cần biến thể `dark:` nào). Số của bảng TƯƠNG PHẢN CAO — bảng mà cổng chặn, như mọi số trong\n * docs/hop-dong-mau.md:\n *\n * soft (trên tint của chính nó) outline · dot (trên nền trang)\n * success 5,60:1 (L) · 7,44:1 (D) 6,52:1 (L) · 10,85:1 (D)\n * info 5,54:1 (L) · 7,12:1 (D) 6,68:1 (L) · 10,16:1 (D)\n * destructive 5,76:1 (L) · 5,71:1 (D) 7,15:1 (L) · 6,84:1 (D)\n * warning 5,93:1 (L) · 7,19:1 (D) 6,51:1 (L) · 12,35:1 (D)\n * severe 5,55:1 (L) · 6,09:1 (D) 6,66:1 (L) · 8,36:1 (D) ← chỉ outline; chấm: dưới\n * neutral 4,89:1 (L) · 4,86:1 (D) 5,23:1 (L) · 5,70:1 (D)\n * primary 5,77:1 (L) · 7,26:1 (D) 6,51:1 (L) · 12,67:1 (D)\n *\n * ─ TONE `severe` (cam) — nấc giữa `warning` và `destructive` ─\n * Ưu tiên cao, SLA sắp trễ — tên \"severe\" là tên quen dùng cho nấc này. Có vì `warning` tô chấm bằng\n * `--warning-ink` #7A5800: đạt chuẩn (6,51:1) nhưng ở cỡ chấm đọc ra ô-liu xỉn, không ra \"gấp\".\n * Nó là tone DUY NHẤT có `dotColor`: chấm tô bằng chính bậc tô nền `--severe` (ngưỡng 3:1, vì chấm\n * là hình) thay cho bậc ink tối. Đo ở CẢ HAI bảng màu — ở tone này bảng mặc định cũng đạt hết:\n *\n * soft (ink trên /15) outline (ink trên nền trang) dot (FILL trên nền trang)\n * mặc định L #AC440F 4,87:1 5,85:1 (--muted 5,47) #EA580C 3,56:1 (--muted 3,32)\n * tương phản L #9D3F10 5,55:1 6,66:1 (--muted 6,22) #EA580C 3,56:1 (--muted 3,32)\n * cả hai D #FB923C 6,09:1 8,36:1 (--card 7,85) #FB923C 8,36:1 (--card 7,85)\n *\n * `/15` như `success`/`info`/`destructive` (không cần nấc `/20` của `warning`/`primary` — đó là\n * cho vàng): tint cam đã nổi 1,20:1 so với nền trắng, hơn cả `success/15` (1,16:1). Trần tint của\n * `text-severe-ink` là `/21` ở bảng mặc định (light) — vẫn giữ trần chung `/20` của hợp đồng.\n *\n * `warning` không còn cần cặp `text-gold-500 dark:text-warning`: `--warning-ink` ĐÚNG BẰNG\n * `--gold-500` ở light và `--yellow` ở dark — cùng một màu, ít hơn một class và một nơi để lệch.\n *\n * ─ TONE `primary` ĐI THEO `--primary`, KHÔNG PHẢI THANG `gold-*` ─\n * Trước đây tone này chôn cứng `bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100`.\n * Tương phản thì ĐẠT (7,77:1 L · 11,85:1 D soft; 9,58:1 L · 15,33:1 D outline — bảng cũ ghi\n * 11,90:1 cho ô dark, đó là số của `--primary-ink` trên `--card`, chép nhầm sang), nhưng SAI VAI\n * TRÒ: thang `gold-*` không có bản dark nên phải viết hai lớp `dark:`, và app đổi `--primary` cho\n * sản phẩm của mình thì chip \"primary\" vẫn cứ vàng. `bg-primary/20` + `text-primary-ink` đi theo\n * token ở cả hai theme bằng MỘT lớp — hợp đồng §5. Alpha `/20` (không phải `/15` như các tone\n * khác) để giữ đúng độ nổi của `bg-gold-100` cũ: nền chip ⇄ nền trang 1,13:1 thay vì 1,10:1,\n * cùng mức với `warning` — vàng luôn cần thêm một nấc mới thấy được.\n *\n * ─ VIỀN: VÌ SAO BỎ HẲN ALPHA ─\n * Ở `variant=\"outline\"` nền trong suốt nên viền là ranh giới DUY NHẤT của chip → ngưỡng 3:1 của\n * WCAG 1.4.11. Không cách hạ alpha nào qua được; đo trên nền trang, cả hai cơ sở đều trượt:\n *\n * border-<tone>/40 (token NỀN) light 1,21–1,84:1 · dark 1,51–2,86:1 ✗\n * border-<tone>/70 (token NỀN) light 1,38–3,09:1 · dark 2,38–6,45:1 ✗\n * border-<tone>-ink/40 (bậc ink) light 1,88–1,99:1 · dark 2,02–2,86:1 ✗\n * border-<tone>-ink/70 (bậc ink) light 3,34–3,69:1 · dark 3,87–6,45:1 — hết biên an toàn\n *\n * (Bảng cũ trộn hai cơ sở: dải `/40` là của token NỀN, còn con số `/70 = 3,34:1` là của bậc ink.)\n * Viền full-strength bằng đúng màu chữ: 6,51–7,15:1 (light) · 6,84–12,67:1 (dark) ✓.\n */\nexport const STATUS_TONES = {\n neutral: {\n bgColor: \"bg-muted\",\n textColor: \"text-muted-foreground\",\n borderColor: \"border-muted-foreground\"\n },\n primary: { bgColor: \"bg-primary/20\", textColor: \"text-primary-ink\", borderColor: \"border-primary-ink\" },\n success: { bgColor: \"bg-success/15\", textColor: \"text-success-ink\", borderColor: \"border-success-ink\" },\n warning: { bgColor: \"bg-warning/20\", textColor: \"text-warning-ink\", borderColor: \"border-warning-ink\" },\n severe: {\n bgColor: \"bg-severe/15\",\n textColor: \"text-severe-ink\",\n borderColor: \"border-severe-ink\",\n dotColor: \"bg-severe\"\n },\n destructive: {\n bgColor: \"bg-destructive/15\",\n textColor: \"text-destructive-ink\",\n borderColor: \"border-destructive-ink\"\n },\n info: { bgColor: \"bg-info/15\", textColor: \"text-info-ink\", borderColor: \"border-info-ink\" }\n} as const satisfies Record<string, StatusTone>;\n\nexport type StatusToneName = keyof typeof STATUS_TONES;\n\n/** Tìm config theo value trong danh sách app cung cấp. */\nexport function getStatusConfig<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): StatusConfig<T> | undefined {\n if (value == null) return undefined;\n return configs.find((config) => config.value === value);\n}\n\n/** Lớp màu (nền tint + chữ) của một trạng thái; không tìm thấy → chuỗi rỗng. */\nexport function getStatusClasses<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return \"\";\n return `${config.bgColor} ${config.textColor}`;\n}\n\n/** Nhãn hiển thị của trạng thái; không tìm thấy → trả về chính `value` để không mất dữ liệu. */\nexport function getStatusLabel<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined,\n options: { short?: boolean } = {}\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return value ?? \"\";\n return options.short ? (config.shortLabel ?? config.label) : config.label;\n}\n\n/** Sắp xếp theo `priority` (thiếu priority thì xuống cuối, giữ thứ tự khai báo). */\nexport function sortByStatusPriority<T extends string>(configs: readonly StatusConfig<T>[]): StatusConfig<T>[] {\n return [...configs].sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));\n}\n"],"mappings":"AA0IO,MAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,EACZ;AAAA,EACA,aAAa;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,MAAM,EAAE,SAAS,cAAc,WAAW,iBAAiB,aAAa,kBAAkB;AAC5F;AAKO,SAAS,gBACd,SACA,OAC6B;AAC7B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACxD;AAGO,SAAS,iBACd,SACA,OACQ;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,GAAG,OAAO,OAAO,IAAI,OAAO,SAAS;AAC9C;AAGO,SAAS,eACd,SACA,OACA,UAA+B,CAAC,GACxB;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO,SAAS;AAC7B,SAAO,QAAQ,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO;AACtE;AAGO,SAAS,qBAAuC,SAAwD;AAC7G,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,qBAAqB,EAAE,YAAY,OAAO,iBAAiB;AACtH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/status-config.ts"],"sourcesContent":["/**\n * Cơ chế StatusConfig generic — app khai map \"trạng thái → lớp màu TOKEN\".\n *\n * Vì sao generic thay vì liệt kê sẵn trạng thái: mỗi app Comitor có bộ trạng thái riêng\n * (Tasks: todo/doing/done; CRM: lead/qualified/won/lost; HR: nộp/duyệt/từ chối). Package\n * chỉ cấp KIỂU và bảng tone, còn nhãn tiếng Việt là chuyện của app.\n *\n * Bắt buộc dùng class token (`bg-success/15 text-success-ink`), KHÔNG hex — hex sẽ chết ở dark mode.\n *\n * @example\n * const TASK_STATUSES: StatusConfig<TaskStatus>[] = [\n * { value: \"todo\", label: \"Cần làm\", shortLabel: \"CL\", ...STATUS_TONES.neutral },\n * { value: \"doing\", label: \"Đang làm\", shortLabel: \"ĐL\", ...STATUS_TONES.info },\n * { value: \"done\", label: \"Hoàn thành\", shortLabel: \"HT\", ...STATUS_TONES.success }\n * ];\n */\n\nexport interface StatusTone {\n /** Lớp nền tint — token TÔ NỀN, vd \"bg-success/15\" */\n bgColor: string;\n /**\n * Lớp chữ — phải là bậc **ink** (`text-success-ink`), KHÔNG phải token tô nền (`text-success`).\n * Chữ chip đứng trên tint của chính màu đó nên đo theo AA 4,5:1; bậc tô nền không đạt ở light.\n */\n textColor: string;\n /**\n * Lớp viền (tuỳ chọn) — dùng cho biến thể `outline`. Ở biến thể đó nền trong suốt nên viền là\n * thứ DUY NHẤT khoanh vùng chip → chịu ngưỡng 3:1 của WCAG 1.4.11. Dùng cùng bậc ink với\n * `textColor` (`border-success-ink`), đừng hạ alpha xuống `/40`.\n */\n borderColor?: string;\n /**\n * Lớp tô CHẤM của `StatusPill variant=\"dot\"` (tuỳ chọn) — một token TÔ NỀN viết thành lớp nền\n * TĨNH (`\"bg-severe\"`), sơn làm `background-color` của chấm 6px. Không khai thì chấm giữ cách\n * cũ: lấy `currentColor` của bậc ink trong `textColor`.\n *\n * Vì sao cần: bậc ink được chọn để đủ tối cho CHỮ, và ở cỡ chấm một bậc tối đọc ra một đốm xỉn\n * — `--warning-ink` #7A5800 là \"ô-liu\" chứ không phải \"gấp\". Chấm là hình, không phải chữ, nên\n * nó chỉ chịu ngưỡng 3:1 của WCAG 1.4.11 và dùng được chính bậc tô nền nếu bậc đó đủ đậm.\n *\n * ⚠ Token phải đạt **3:1 với nền trang ở CẢ hai theme** (--background/--card, và nên cả\n * --muted vì chấm hay nằm trong dòng bảng). Bậc tô nền của các tone khác KHÔNG đạt: `--warning`\n * 1,59 · `--primary` 1,86:1 trên nền trắng ✗; `--success` qua nền trắng (3,13) nhưng tụt trên\n * --muted (2,92:1 ✗) — nên đừng thêm trường này cho tone nào chưa đo. Cổng tương phản KHÔNG\n * thấy được cặp này: chuỗi lớp nằm ở bảng tone, còn phần tử chấm nằm ở `status-pill.tsx` — số đo\n * ghi tay ở bảng dưới là bằng chứng duy nhất. Như mọi trường ở đây: lớp TĨNH, không `bg-${tone}`.\n */\n dotColor?: string;\n}\n\nexport interface StatusConfig<T extends string = string> extends StatusTone {\n value: T;\n label: string;\n /** Nhãn rút gọn cho bảng/chip hẹp (vd \"HT\" thay \"Hoàn thành\") */\n shortLabel?: string;\n /** Ưu tiên sắp xếp (nhỏ = lên trước) */\n priority?: number;\n /**\n * Class thêm cho DÒNG bảng khi trạng thái cần gây chú ý (vd quá hạn).\n *\n * ⚠ ĐỪNG để đây một lớp nền có alpha (`bg-destructive/5`…) nếu bảng có cột GHIM hoặc có\n * `striped`. Ô cột ghim lấy nền bằng `bg-inherit` từ hàng nên nó thừa hưởng luôn alpha và\n * trở nên trong mờ; còn một class nền trần thì thua sọc của `striped` về độ đặc tả nên mất\n * hẳn ở hàng chẵn. Dùng nền ĐỤC tô trên từng ô (như `isRowActive` của `DataTable`:\n * `[&>td]:bg-accent`) hoặc nói trạng thái bằng chip + màu chữ.\n */\n rowClassName?: string;\n}\n\n/**\n * Bảng tone dựng sẵn theo token semantic — trải (`...STATUS_TONES.success`) vào StatusConfig.\n *\n * Mọi chuỗi ở đây là class TĨNH để Tailwind quét được từ `dist`; tuyệt đối không sinh động\n * kiểu `bg-${tone}/15` (class động bị loại khỏi bundle của app).\n *\n * ─ VÌ SAO CHỮ VÀ VIỀN ĐỀU LÀ BẬC `--x-ink`, KHÔNG PHẢI `--x` ─\n *\n * `--success/--warning/--info/--destructive` là token TÔ NỀN. Đặt chúng làm màu CHỮ thì bản light\n * hỏng ở cả ba biến thể của `StatusPill` (đo theo WCAG, nền tint đã trộn với `--card` trắng):\n *\n * text-success trên bg-success/15 (#E0F2E9) = 2,69:1 ✗ (dưới cả ngưỡng 3:1)\n * text-info trên bg-info/15 (#DDEDEE) = 3,44:1 ✗\n * text-destructive trên bg-destructive/15 = 4,06:1 ✗\n * text-success / text-info trên nền TRANG (outline, dot) = 3,13:1 / 4,14:1 ✗\n *\n * Đi qua bậc ink thì đạt ở CẢ hai theme (`--x-ink` ở `.dark` vốn trỏ về chính `--x`, nên không\n * cần biến thể `dark:` nào). Số của bảng TƯƠNG PHẢN CAO — bảng mà cổng chặn, như mọi số trong\n * docs/hop-dong-mau.md:\n *\n * soft (trên tint của chính nó) outline · dot (trên nền trang)\n * success 5,60:1 (L) · 7,44:1 (D) 6,52:1 (L) · 10,85:1 (D)\n * info 5,54:1 (L) · 7,12:1 (D) 6,68:1 (L) · 10,16:1 (D)\n * destructive 5,76:1 (L) · 5,71:1 (D) 7,15:1 (L) · 6,84:1 (D)\n * warning 5,93:1 (L) · 7,19:1 (D) 6,51:1 (L) · 12,35:1 (D)\n * severe 5,55:1 (L) · 6,09:1 (D) 6,66:1 (L) · 8,36:1 (D) ← chỉ outline; chấm: dưới\n * neutral (chữ `--foreground` — xem mục `neutral` bên dưới)\n * primary 5,77:1 (L) · 7,26:1 (D) 6,51:1 (L) · 12,67:1 (D)\n *\n * ─ TONE `severe` (cam) — nấc giữa `warning` và `destructive` ─\n * Ưu tiên cao, SLA sắp trễ — tên \"severe\" là tên quen dùng cho nấc này. Có vì `warning` tô chấm bằng\n * `--warning-ink` #7A5800: đạt chuẩn (6,51:1) nhưng ở cỡ chấm đọc ra ô-liu xỉn, không ra \"gấp\".\n * Nó là tone DUY NHẤT có `dotColor`: chấm tô bằng chính bậc tô nền `--severe` (ngưỡng 3:1, vì chấm\n * là hình) thay cho bậc ink tối. Đo ở CẢ HAI bảng màu — ở tone này bảng mặc định cũng đạt hết:\n *\n * soft (ink trên /15) outline (ink trên nền trang) dot (FILL trên nền trang)\n * mặc định L #AC440F 4,87:1 5,85:1 (--muted 5,47) #EA580C 3,56:1 (--muted 3,32)\n * tương phản L #9D3F10 5,55:1 6,66:1 (--muted 6,22) #EA580C 3,56:1 (--muted 3,32)\n * cả hai D #FB923C 6,09:1 8,36:1 (--card 7,85) #FB923C 8,36:1 (--card 7,85)\n *\n * `/15` như `success`/`info`/`destructive` (không cần nấc `/20` của `warning`/`primary` — đó là\n * cho vàng): tint cam đã nổi 1,20:1 so với nền trắng, hơn cả `success/15` (1,16:1). Trần tint của\n * `text-severe-ink` là `/21` ở bảng mặc định (light) — vẫn giữ trần chung `/20` của hợp đồng.\n *\n * `warning` không còn cần cặp `text-gold-500 dark:text-warning`: `--warning-ink` ĐÚNG BẰNG\n * `--gold-500` ở light và `--yellow` ở dark — cùng một màu, ít hơn một class và một nơi để lệch.\n *\n * ─ TONE `primary`: NỀN theo `--primary`, CHỮ và VIỀN theo thang `gold-*` ─\n * Nền là `bg-primary/20` (alpha `/20`, không `/15`, để tint vàng nổi ngang `warning`: 1,13:1 với nền trang).\n * Chữ và viền KHÔNG dùng `--primary-ink`: ở bảng màu MẶC ĐỊNH (bản duyệt comitor-ds) `--primary-ink` chính là\n * vàng thương hiệu `#E8B824`, nên chữ vàng trên tint vàng chỉ ~1,55:1 — chip \"primary\" gần như không đọc được.\n * Dùng đúng cặp của tone `gold` ở avatar (`lib/avatar-tone.ts`): `text-gold-600` light (7,77:1 trên tint),\n * `text-gold-100` dark (11,85:1). Cái giá: app đặt `--primary` khác vàng thì nền chip đổi theo còn chữ vẫn vàng —\n * đã cân và chọn chữ đọc được. `Gantt` đi cùng cặp này.\n *\n * ─ TONE `neutral`: chữ `--foreground`, không `--muted-foreground` ─\n * `--muted-foreground` trên `--muted` ở bảng mặc định chỉ ~3,58:1 — chip \"Draft\" nhạt hơn mọi tone khác. Viền\n * (`outline`) giữ `--muted-foreground`: nó chỉ cần 3:1 với nền trang, và viền đen sẽ nặng hơn mọi viền tone.\n *\n * ─ VIỀN: VÌ SAO BỎ HẲN ALPHA ─\n * Ở `variant=\"outline\"` nền trong suốt nên viền là ranh giới DUY NHẤT của chip → ngưỡng 3:1 của\n * WCAG 1.4.11. Không cách hạ alpha nào qua được; đo trên nền trang, cả hai cơ sở đều trượt:\n *\n * border-<tone>/40 (token NỀN) light 1,21–1,84:1 · dark 1,51–2,86:1 ✗\n * border-<tone>/70 (token NỀN) light 1,38–3,09:1 · dark 2,38–6,45:1 ✗\n * border-<tone>-ink/40 (bậc ink) light 1,88–1,99:1 · dark 2,02–2,86:1 ✗\n * border-<tone>-ink/70 (bậc ink) light 3,34–3,69:1 · dark 3,87–6,45:1 — hết biên an toàn\n *\n * (Bảng cũ trộn hai cơ sở: dải `/40` là của token NỀN, còn con số `/70 = 3,34:1` là của bậc ink.)\n * Viền full-strength bằng đúng màu chữ: 6,51–7,15:1 (light) · 6,84–12,67:1 (dark) ✓.\n */\nexport const STATUS_TONES = {\n neutral: {\n bgColor: \"bg-muted\",\n textColor: \"text-foreground\",\n borderColor: \"border-muted-foreground\"\n },\n primary: {\n bgColor: \"bg-primary/20\",\n textColor: \"text-gold-600 dark:text-gold-100\",\n borderColor: \"border-gold-600 dark:border-gold-100\"\n },\n success: { bgColor: \"bg-success/15\", textColor: \"text-success-ink\", borderColor: \"border-success-ink\" },\n warning: { bgColor: \"bg-warning/20\", textColor: \"text-warning-ink\", borderColor: \"border-warning-ink\" },\n severe: {\n bgColor: \"bg-severe/15\",\n textColor: \"text-severe-ink\",\n borderColor: \"border-severe-ink\",\n dotColor: \"bg-severe\"\n },\n destructive: {\n bgColor: \"bg-destructive/15\",\n textColor: \"text-destructive-ink\",\n borderColor: \"border-destructive-ink\"\n },\n info: { bgColor: \"bg-info/15\", textColor: \"text-info-ink\", borderColor: \"border-info-ink\" }\n} as const satisfies Record<string, StatusTone>;\n\nexport type StatusToneName = keyof typeof STATUS_TONES;\n\n/** Tìm config theo value trong danh sách app cung cấp. */\nexport function getStatusConfig<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): StatusConfig<T> | undefined {\n if (value == null) return undefined;\n return configs.find((config) => config.value === value);\n}\n\n/** Lớp màu (nền tint + chữ) của một trạng thái; không tìm thấy → chuỗi rỗng. */\nexport function getStatusClasses<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return \"\";\n return `${config.bgColor} ${config.textColor}`;\n}\n\n/** Nhãn hiển thị của trạng thái; không tìm thấy → trả về chính `value` để không mất dữ liệu. */\nexport function getStatusLabel<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined,\n options: { short?: boolean } = {}\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return value ?? \"\";\n return options.short ? (config.shortLabel ?? config.label) : config.label;\n}\n\n/** Sắp xếp theo `priority` (thiếu priority thì xuống cuối, giữ thứ tự khai báo). */\nexport function sortByStatusPriority<T extends string>(configs: readonly StatusConfig<T>[]): StatusConfig<T>[] {\n return [...configs].sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));\n}\n"],"mappings":"AA4IO,MAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,EACZ;AAAA,EACA,aAAa;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,MAAM,EAAE,SAAS,cAAc,WAAW,iBAAiB,aAAa,kBAAkB;AAC5F;AAKO,SAAS,gBACd,SACA,OAC6B;AAC7B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACxD;AAGO,SAAS,iBACd,SACA,OACQ;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,GAAG,OAAO,OAAO,IAAI,OAAO,SAAS;AAC9C;AAGO,SAAS,eACd,SACA,OACA,UAA+B,CAAC,GACxB;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO,SAAS;AAC7B,SAAO,QAAQ,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO;AACtE;AAGO,SAAS,qBAAuC,SAAwD;AAC7G,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,qBAAqB,EAAE,YAAY,OAAO,iBAAiB;AACtH;","names":[]}
|
|
@@ -13,10 +13,7 @@ function BreadcrumbList({ className, ...props }) {
|
|
|
13
13
|
"ol",
|
|
14
14
|
{
|
|
15
15
|
"data-slot": "breadcrumb-list",
|
|
16
|
-
className: cn(
|
|
17
|
-
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
|
18
|
-
className
|
|
19
|
-
),
|
|
16
|
+
className: cn("text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words", className),
|
|
20
17
|
...props
|
|
21
18
|
}
|
|
22
19
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/breadcrumb.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Breadcrumb — primitive dựng đường dẫn phân cấp.\n * Chuỗi cho trình đọc màn hình theo `ComitorLocaleProvider` (namespace `breadcrumb`, mặc định\n * tiếng Anh); override từng chỗ qua `aria-label` / `srLabel`.\n *\n * `\"use client\"` từ `3.0.0`: tên landmark của `<nav>` và nhãn của `BreadcrumbEllipsis` đọc từ\n * provider, mà context chỉ đọc được ở client. Trước đó file này tĩnh và ghim cứng tiếng Việt.\n * Không prop nào là hàm/component nên Server Component (vd `PageHeader`) vẫn render được nó.\n * Con của `BreadcrumbLink asChild` nay đi qua ranh giới RSC — tới dạng element lười thì `Slot`\n * (`@radix-ui/react-slot` 1.2.4) tự mở bằng `use()`.\n *\n * Điều hướng: bọc `next/link` bằng `<BreadcrumbLink asChild><Link …/></BreadcrumbLink>`\n * để primitive không phụ thuộc `next/*`.\n */\n\nexport type BreadcrumbProps = React.ComponentProps<\"nav\">;\nexport type BreadcrumbListProps = React.ComponentProps<\"ol\">;\nexport type BreadcrumbItemProps = React.ComponentProps<\"li\">;\nexport type BreadcrumbLinkProps = React.ComponentProps<\"a\"> & { asChild?: boolean };\nexport type BreadcrumbPageProps = React.ComponentProps<\"span\">;\nexport type BreadcrumbSeparatorProps = React.ComponentProps<\"li\">;\nexport type BreadcrumbEllipsisProps = React.ComponentProps<\"span\"> & {\n /** Nhãn chỉ dành cho trình đọc màn hình. Mặc định: khoá `ellipsisLabel` của namespace `breadcrumb`. */\n srLabel?: string;\n};\n\nfunction Breadcrumb({ ...props }: BreadcrumbProps) {\n const t = useComitorMessages(\"breadcrumb\");\n return <nav aria-label={t.navLabel} data-slot=\"breadcrumb\" {...props} />;\n}\n\nfunction BreadcrumbList({ className, ...props }: BreadcrumbListProps) {\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn(\
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/breadcrumb.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Breadcrumb — primitive dựng đường dẫn phân cấp.\n * Chuỗi cho trình đọc màn hình theo `ComitorLocaleProvider` (namespace `breadcrumb`, mặc định\n * tiếng Anh); override từng chỗ qua `aria-label` / `srLabel`.\n *\n * `\"use client\"` từ `3.0.0`: tên landmark của `<nav>` và nhãn của `BreadcrumbEllipsis` đọc từ\n * provider, mà context chỉ đọc được ở client. Trước đó file này tĩnh và ghim cứng tiếng Việt.\n * Không prop nào là hàm/component nên Server Component (vd `PageHeader`) vẫn render được nó.\n * Con của `BreadcrumbLink asChild` nay đi qua ranh giới RSC — tới dạng element lười thì `Slot`\n * (`@radix-ui/react-slot` 1.2.4) tự mở bằng `use()`.\n *\n * Điều hướng: bọc `next/link` bằng `<BreadcrumbLink asChild><Link …/></BreadcrumbLink>`\n * để primitive không phụ thuộc `next/*`.\n */\n\nexport type BreadcrumbProps = React.ComponentProps<\"nav\">;\nexport type BreadcrumbListProps = React.ComponentProps<\"ol\">;\nexport type BreadcrumbItemProps = React.ComponentProps<\"li\">;\nexport type BreadcrumbLinkProps = React.ComponentProps<\"a\"> & { asChild?: boolean };\nexport type BreadcrumbPageProps = React.ComponentProps<\"span\">;\nexport type BreadcrumbSeparatorProps = React.ComponentProps<\"li\">;\nexport type BreadcrumbEllipsisProps = React.ComponentProps<\"span\"> & {\n /** Nhãn chỉ dành cho trình đọc màn hình. Mặc định: khoá `ellipsisLabel` của namespace `breadcrumb`. */\n srLabel?: string;\n};\n\nfunction Breadcrumb({ ...props }: BreadcrumbProps) {\n const t = useComitorMessages(\"breadcrumb\");\n return <nav aria-label={t.navLabel} data-slot=\"breadcrumb\" {...props} />;\n}\n\nfunction BreadcrumbList({ className, ...props }: BreadcrumbListProps) {\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn(\"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words\", className)}\n {...props}\n />\n );\n}\n\nfunction BreadcrumbItem({ className, ...props }: BreadcrumbItemProps) {\n return <li data-slot=\"breadcrumb-item\" className={cn(\"inline-flex items-center gap-1.5\", className)} {...props} />;\n}\n\nfunction BreadcrumbLink({ asChild, className, ...props }: BreadcrumbLinkProps) {\n const Comp = asChild ? Slot : \"a\";\n\n return (\n <Comp\n data-slot=\"breadcrumb-link\"\n className={cn(\"hover:text-foreground transition-colors duration-150 ease-out\", className)}\n {...props}\n />\n );\n}\n\nfunction BreadcrumbPage({ className, ...props }: BreadcrumbPageProps) {\n return (\n // biome-ignore lint/a11y/useFocusableInteractive: giữ nguyên markup shadcn — mục hiện tại cố tình KHÔNG focus được (aria-disabled), chỉ mượn role=\"link\" để trình đọc màn hình đọc đúng ngữ cảnh\n <span\n data-slot=\"breadcrumb-page\"\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn(\"text-foreground font-normal\", className)}\n {...props}\n />\n );\n}\n\nfunction BreadcrumbSeparator({ children, className, ...props }: BreadcrumbSeparatorProps) {\n return (\n <li\n data-slot=\"breadcrumb-separator\"\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn(\"[&>svg]:size-3.5\", className)}\n {...props}\n >\n {children ?? <ChevronRight />}\n </li>\n );\n}\n\nfunction BreadcrumbEllipsis({ className, srLabel: srLabelProp, ...props }: BreadcrumbEllipsisProps) {\n const t = useComitorMessages(\"breadcrumb\");\n const srLabel = srLabelProp ?? t.ellipsisLabel;\n return (\n <span\n data-slot=\"breadcrumb-ellipsis\"\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn(\"flex size-9 items-center justify-center\", className)}\n {...props}\n >\n <MoreHorizontal className=\"size-4\" />\n <span className=\"sr-only\">{srLabel}</span>\n </span>\n );\n}\n\nexport {\n Breadcrumb,\n BreadcrumbEllipsis,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator\n};\n"],"mappings":";AAoCS,cA6DL,YA7DK;AAlCT,SAAS,YAAY;AACrB,SAAS,cAAc,sBAAsB;AAE7C,SAAS,0BAA0B;AACnC,SAAS,UAAU;AA4BnB,SAAS,WAAW,EAAE,GAAG,MAAM,GAAoB;AACjD,QAAM,IAAI,mBAAmB,YAAY;AACzC,SAAO,oBAAC,SAAI,cAAY,EAAE,UAAU,aAAU,cAAc,GAAG,OAAO;AACxE;AAEA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iFAAiF,SAAS;AAAA,MACvG,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE,SAAO,oBAAC,QAAG,aAAU,mBAAkB,WAAW,GAAG,oCAAoC,SAAS,GAAI,GAAG,OAAO;AAClH;AAEA,SAAS,eAAe,EAAE,SAAS,WAAW,GAAG,MAAM,GAAwB;AAC7E,QAAM,OAAO,UAAU,OAAO;AAE9B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,iEAAiE,SAAS;AAAA,MACvF,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,eAAe,EAAE,WAAW,GAAG,MAAM,GAAwB;AACpE;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA;AAAA,QACC,aAAU;AAAA,QACV,MAAK;AAAA,QACL,iBAAc;AAAA,QACd,gBAAa;AAAA,QACb,WAAW,GAAG,+BAA+B,SAAS;AAAA,QACrD,GAAG;AAAA;AAAA,IACN;AAAA;AAEJ;AAEA,SAAS,oBAAoB,EAAE,UAAU,WAAW,GAAG,MAAM,GAA6B;AACxF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,eAAY;AAAA,MACZ,WAAW,GAAG,oBAAoB,SAAS;AAAA,MAC1C,GAAG;AAAA,MAEH,sBAAY,oBAAC,gBAAa;AAAA;AAAA,EAC7B;AAEJ;AAEA,SAAS,mBAAmB,EAAE,WAAW,SAAS,aAAa,GAAG,MAAM,GAA4B;AAClG,QAAM,IAAI,mBAAmB,YAAY;AACzC,QAAM,UAAU,eAAe,EAAE;AACjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,eAAY;AAAA,MACZ,WAAW,GAAG,2CAA2C,SAAS;AAAA,MACjE,GAAG;AAAA,MAEJ;AAAA,4BAAC,kBAAe,WAAU,UAAS;AAAA,QACnC,oBAAC,UAAK,WAAU,WAAW,mBAAQ;AAAA;AAAA;AAAA,EACrC;AAEJ;","names":[]}
|
|
@@ -18,13 +18,13 @@ const buttonVariants = cva(
|
|
|
18
18
|
link: "bg-transparent text-link-ink hover:text-link-ink-hover underline-offset-4 hover:underline border border-transparent p-0 h-auto"
|
|
19
19
|
},
|
|
20
20
|
size: {
|
|
21
|
-
default: "h-9 px-4 text-sm rounded-md gap-2",
|
|
22
|
-
md: "h-9 px-4 text-sm rounded-md gap-2",
|
|
23
|
-
sm: "h-8 px-3 text-xs rounded-md gap-1.5",
|
|
24
|
-
lg: "h-11 px-6 text-base rounded-md gap-2",
|
|
25
|
-
icon: "h-9 w-9 rounded-md",
|
|
26
|
-
"icon-sm": "h-8 w-8 rounded-md",
|
|
27
|
-
"icon-lg": "h-11 w-11 rounded-md"
|
|
21
|
+
default: "h-9 px-4 text-sm rounded-md gap-2 [&_svg:not([class*='size-'])]:size-4",
|
|
22
|
+
md: "h-9 px-4 text-sm rounded-md gap-2 [&_svg:not([class*='size-'])]:size-4",
|
|
23
|
+
sm: "h-8 px-3 text-xs rounded-md gap-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
24
|
+
lg: "h-11 px-6 text-base rounded-md gap-2 [&_svg:not([class*='size-'])]:size-5",
|
|
25
|
+
icon: "h-9 w-9 rounded-md [&_svg:not([class*='size-'])]:size-4",
|
|
26
|
+
"icon-sm": "h-8 w-8 rounded-md [&_svg:not([class*='size-'])]:size-3.5",
|
|
27
|
+
"icon-lg": "h-11 w-11 rounded-md [&_svg:not([class*='size-'])]:size-5"
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
defaultVariants: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Loader2 } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port NGUYÊN TRẠNG từ comitor-ds — bản duyệt là nguồn sự thật (xem khối NGUỒN SỰ THẬT ở\n * đầu styles.css). API giữ tương thích shadcn (cva + asChild + data-slot); `primary` là alias của\n * `default` — đã là API công khai trong docs comitor-ds nên không bỏ.\n *\n * ⚠ 0.2.x từng đổi nhóm biến thể này sang token semantic + bậc `-ink` để đạt WCAG. 0.3.0 kéo về\n * đúng comitor-ds. Những khoản dưới ngưỡng còn tồn ở đây, ghi ra để không ai \"sửa\" lại lần nữa:\n *\n * destructive : chữ trắng trên `--red` #D64545 = 4,38:1 (thiếu 0,12 so với AA 4,5:1)\n * link : `--gold-400` #B88A00 trên nền trang = 3,15:1 (AA đòi 4,5:1)\n * outline : viền `--border` ⇄ nền trang = 1,16:1 (1.4.11 đòi 3:1)\n *\n * Vì sao `bg-gold-300` chứ không `bg-primary`: comitor-ds tô cùng một gold ở CẢ hai theme\n * (`dark:data-[state=checked]:bg-gold-300` là lối viết quen của bản duyệt), trong khi `--primary`\n * có bản dark riêng #F5D060. Dùng thang gold thô là cách duy nhất khớp bản duyệt ở dark.\n * ⚠ Đánh đổi: app ghi đè `--primary` cho sản phẩm của mình sẽ KHÔNG đổi được nền nút. Muốn nút\n * theo brand riêng thì ghi đè thang `--gold-*`.\n *\n * ─ `aria-invalid`: VIỀN ĐỎ, KHÔNG CHỈ CHỮ ĐỎ (BỔ SUNG, comitor-ds chưa có) ─\n *\n * Control dựng trên `<Button>` mà mang giá trị form — `Combobox`/`MultiCombobox` render trigger\n * bằng `<Button role=\"combobox\" aria-invalid>` — trước đây chỉ có chữ lỗi đỏ ở dưới, còn chính ô\n * thì giữ nguyên viền: người dùng thấy thông báo lỗi mà không thấy ô nào đang lỗi. `Input` đã có\n * nhánh này từ đầu; đặt ở đây để MỌI control dựng trên Button được nhất quán. Token là\n * `--destructive-ink` (= `--destructive` = #D64545), đúng thứ `Input` của comitor-ds dùng\n * (`border-red`).\n */\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center font-medium select-none whitespace-nowrap transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 aria-invalid:border-destructive-ink aria-invalid:hover:border-destructive-ink aria-invalid:focus-visible:outline-destructive-ink disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control\",\n primary: \"bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-warm dark:hover:bg-muted border border-control-edge\",\n outline:\n \"bg-transparent text-foreground border border-control-edge hover:border-primary-ink hover:bg-gold-50 dark:hover:bg-gold-500/10\",\n ghost: \"bg-transparent text-foreground hover:bg-muted border border-transparent\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive-hover border border-transparent\",\n link: \"bg-transparent text-link-ink hover:text-link-ink-hover underline-offset-4 hover:underline border border-transparent p-0 h-auto\"\n },\n size: {\n default: \"h-9 px-4 text-sm rounded-md gap-2\",\n md: \"h-9 px-4 text-sm rounded-md gap-2\",\n sm: \"h-8 px-3 text-xs rounded-md gap-1.5\",\n lg: \"h-11 px-6 text-base rounded-md gap-2\",\n icon: \"h-9 w-9 rounded-md\",\n \"icon-sm\": \"h-8 w-8 rounded-md\",\n \"icon-lg\": \"h-11 w-11 rounded-md\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\"\n }\n }\n);\n\nexport interface ButtonProps extends React.ComponentProps<\"button\">, VariantProps<typeof buttonVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <button>. */\n asChild?: boolean;\n /** Hiện spinner và khoá nút. */\n loading?: boolean;\n /** Icon đặt trước nội dung; bị spinner thay thế khi `loading`. */\n leftIcon?: React.ReactNode;\n /** Icon đặt sau nội dung; ẩn khi `loading`. */\n rightIcon?: React.ReactNode;\n}\n\nfunction Button({\n className,\n variant,\n size,\n asChild = false,\n loading = false,\n leftIcon,\n rightIcon,\n disabled,\n children,\n ...props\n}: ButtonProps) {\n // Slot chỉ nhận đúng MỘT phần tử con → bỏ qua lớp bọc icon/loading khi asChild.\n if (asChild) {\n return (\n <Slot data-slot=\"button\" className={cn(buttonVariants({ variant, size, className }))} {...props}>\n {children}\n </Slot>\n );\n }\n\n return (\n <button\n data-slot=\"button\"\n disabled={disabled || loading}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n >\n {loading ? (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin\" aria-hidden />\n ) : leftIcon ? (\n <span className=\"shrink-0\">{leftIcon}</span>\n ) : null}\n {children}\n {!loading && rightIcon ? <span className=\"shrink-0\">{rightIcon}</span> : null}\n </button>\n );\n}\n\nexport { Button, buttonVariants };\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/button.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Loader2 } from \"lucide-react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Style port NGUYÊN TRẠNG từ comitor-ds — bản duyệt là nguồn sự thật (xem khối NGUỒN SỰ THẬT ở\n * đầu styles.css). API giữ tương thích shadcn (cva + asChild + data-slot); `primary` là alias của\n * `default` — đã là API công khai trong docs comitor-ds nên không bỏ.\n *\n * ⚠ 0.2.x từng đổi nhóm biến thể này sang token semantic + bậc `-ink` để đạt WCAG. 0.3.0 kéo về\n * đúng comitor-ds. Những khoản dưới ngưỡng còn tồn ở đây, ghi ra để không ai \"sửa\" lại lần nữa:\n *\n * destructive : chữ trắng trên `--red` #D64545 = 4,38:1 (thiếu 0,12 so với AA 4,5:1)\n * link : `--gold-400` #B88A00 trên nền trang = 3,15:1 (AA đòi 4,5:1)\n * outline : viền `--border` ⇄ nền trang = 1,16:1 (1.4.11 đòi 3:1)\n *\n * Vì sao `bg-gold-300` chứ không `bg-primary`: comitor-ds tô cùng một gold ở CẢ hai theme\n * (`dark:data-[state=checked]:bg-gold-300` là lối viết quen của bản duyệt), trong khi `--primary`\n * có bản dark riêng #F5D060. Dùng thang gold thô là cách duy nhất khớp bản duyệt ở dark.\n * ⚠ Đánh đổi: app ghi đè `--primary` cho sản phẩm của mình sẽ KHÔNG đổi được nền nút. Muốn nút\n * theo brand riêng thì ghi đè thang `--gold-*`.\n *\n * ─ `aria-invalid`: VIỀN ĐỎ, KHÔNG CHỈ CHỮ ĐỎ (BỔ SUNG, comitor-ds chưa có) ─\n *\n * Control dựng trên `<Button>` mà mang giá trị form — `Combobox`/`MultiCombobox` render trigger\n * bằng `<Button role=\"combobox\" aria-invalid>` — trước đây chỉ có chữ lỗi đỏ ở dưới, còn chính ô\n * thì giữ nguyên viền: người dùng thấy thông báo lỗi mà không thấy ô nào đang lỗi. `Input` đã có\n * nhánh này từ đầu; đặt ở đây để MỌI control dựng trên Button được nhất quán. Token là\n * `--destructive-ink` (= `--destructive` = #D64545), đúng thứ `Input` của comitor-ds dùng\n * (`border-red`).\n */\n/*\n * Icon con không có lớp `size-*` thì lấy cỡ theo `size` của nút (16px · `sm`/`icon-sm` 14px · `lg` 20px) — trước\n * đây nó giữ 24px mặc định của lucide, nên `<Button size=\"icon-sm\"><ThumbsUp /></Button>` ra icon to gần bằng nút.\n * Icon đã có `size-*` thì giữ nguyên.\n */\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center font-medium select-none whitespace-nowrap transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 aria-invalid:border-destructive-ink aria-invalid:hover:border-destructive-ink aria-invalid:focus-visible:outline-destructive-ink disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control\",\n primary: \"bg-primary text-primary-foreground hover:bg-gold-400 border border-transparent shadow-control\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-warm dark:hover:bg-muted border border-control-edge\",\n outline:\n \"bg-transparent text-foreground border border-control-edge hover:border-primary-ink hover:bg-gold-50 dark:hover:bg-gold-500/10\",\n ghost: \"bg-transparent text-foreground hover:bg-muted border border-transparent\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-destructive-hover border border-transparent\",\n link: \"bg-transparent text-link-ink hover:text-link-ink-hover underline-offset-4 hover:underline border border-transparent p-0 h-auto\"\n },\n size: {\n default: \"h-9 px-4 text-sm rounded-md gap-2 [&_svg:not([class*='size-'])]:size-4\",\n md: \"h-9 px-4 text-sm rounded-md gap-2 [&_svg:not([class*='size-'])]:size-4\",\n sm: \"h-8 px-3 text-xs rounded-md gap-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n lg: \"h-11 px-6 text-base rounded-md gap-2 [&_svg:not([class*='size-'])]:size-5\",\n icon: \"h-9 w-9 rounded-md [&_svg:not([class*='size-'])]:size-4\",\n \"icon-sm\": \"h-8 w-8 rounded-md [&_svg:not([class*='size-'])]:size-3.5\",\n \"icon-lg\": \"h-11 w-11 rounded-md [&_svg:not([class*='size-'])]:size-5\"\n }\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\"\n }\n }\n);\n\nexport interface ButtonProps extends React.ComponentProps<\"button\">, VariantProps<typeof buttonVariants> {\n /** Render ra chính phần tử con (Radix Slot) thay vì thẻ <button>. */\n asChild?: boolean;\n /** Hiện spinner và khoá nút. */\n loading?: boolean;\n /** Icon đặt trước nội dung; bị spinner thay thế khi `loading`. */\n leftIcon?: React.ReactNode;\n /** Icon đặt sau nội dung; ẩn khi `loading`. */\n rightIcon?: React.ReactNode;\n}\n\nfunction Button({\n className,\n variant,\n size,\n asChild = false,\n loading = false,\n leftIcon,\n rightIcon,\n disabled,\n children,\n ...props\n}: ButtonProps) {\n // Slot chỉ nhận đúng MỘT phần tử con → bỏ qua lớp bọc icon/loading khi asChild.\n if (asChild) {\n return (\n <Slot data-slot=\"button\" className={cn(buttonVariants({ variant, size, className }))} {...props}>\n {children}\n </Slot>\n );\n }\n\n return (\n <button\n data-slot=\"button\"\n disabled={disabled || loading}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n >\n {loading ? (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin\" aria-hidden />\n ) : leftIcon ? (\n <span className=\"shrink-0\">{leftIcon}</span>\n ) : null}\n {children}\n {!loading && rightIcon ? <span className=\"shrink-0\">{rightIcon}</span> : null}\n </button>\n );\n}\n\nexport { Button, buttonVariants };\n"],"mappings":";AAkGM,cAOF,YAPE;AAhGN,SAAS,YAAY;AACrB,SAAS,WAA8B;AACvC,SAAS,eAAe;AAExB,SAAS,UAAU;AAkCnB,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,WACE;AAAA,QACF,SACE;AAAA,QACF,OAAO;AAAA,QACP,aAAa;AAAA,QACb,MAAM;AAAA,MACR;AAAA,MACA,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAaA,SAAS,OAAO;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgB;AAEd,MAAI,SAAS;AACX,WACE,oBAAC,QAAK,aAAU,UAAS,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC,GAAI,GAAG,OACvF,UACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,UAAU,YAAY;AAAA,MACtB,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,CAAC;AAAA,MACzD,GAAG;AAAA,MAEH;AAAA,kBACC,oBAAC,WAAQ,WAAU,kCAAiC,eAAW,MAAC,IAC9D,WACF,oBAAC,UAAK,WAAU,YAAY,oBAAS,IACnC;AAAA,QACH;AAAA,QACA,CAAC,WAAW,YAAY,oBAAC,UAAK,WAAU,YAAY,qBAAU,IAAU;AAAA;AAAA;AAAA,EAC3E;AAEJ;","names":[]}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
+
/** Tông của vòng — cùng bộ tông với `StatusPill` (`default` là primary). */
|
|
4
|
+
type CircularProgressTone = "default" | "neutral" | "success" | "warning" | "severe" | "destructive" | "info";
|
|
3
5
|
interface CircularProgressProps extends React.ComponentProps<"div"> {
|
|
4
6
|
/** Giá trị hiện tại; `null`/`undefined` = không xác định (vòng quay). */
|
|
5
7
|
value?: number | null | undefined;
|
|
@@ -20,6 +22,10 @@ interface CircularProgressProps extends React.ComponentProps<"div"> {
|
|
|
20
22
|
* chung chung thay app (giống `Progress`).
|
|
21
23
|
*/
|
|
22
24
|
label?: string;
|
|
25
|
+
/** Màu cung VÀ rãnh: cung là bậc tô nền, rãnh là tint nhạt của cùng tông (bộ tông của `StatusPill`). */
|
|
26
|
+
tone?: CircularProgressTone;
|
|
27
|
+
/** `"neutral"` = rãnh xám trung tính, chỉ cung mang màu `tone`. Mặc định `"tone"`. */
|
|
28
|
+
track?: "tone" | "neutral";
|
|
23
29
|
asChild?: boolean;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
@@ -27,9 +33,8 @@ interface CircularProgressProps extends React.ComponentProps<"div"> {
|
|
|
27
33
|
* Ráp từ các phần (`CircularProgressIndicator` › `Track` + `Range`, `CircularProgressValueText`),
|
|
28
34
|
* hoặc dùng sẵn `CircularProgressCombined`.
|
|
29
35
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* token tô nền `text-success` — rãnh không cần đổi theo (số đo ở `CircularProgressTrack`).
|
|
36
|
+
* Màu theo `tone` (bộ tông của `StatusPill`): cung bậc tô nền, rãnh tint nhạt cùng tông; `track="neutral"`
|
|
37
|
+
* giữ rãnh xám. `className` trên `CircularProgressRange`/`Track` vẫn đè được.
|
|
33
38
|
*
|
|
34
39
|
* ⚠ Trạng thái không xác định cần `--animate-spin-around` + `@keyframes spin-around` trong
|
|
35
40
|
* styles.css; thiếu thì vòng đứng yên ở một cung 25% (không lỗi, không cảnh báo).
|
|
@@ -47,4 +52,4 @@ interface CircularProgressValueTextProps extends React.ComponentProps<"span"> {
|
|
|
47
52
|
declare function CircularProgressValueText(props: CircularProgressValueTextProps): React.JSX.Element;
|
|
48
53
|
declare function CircularProgressCombined(props: CircularProgressProps): React.JSX.Element;
|
|
49
54
|
|
|
50
|
-
export { CircularProgress, CircularProgressCombined, CircularProgressIndicator, type CircularProgressProps, CircularProgressRange, CircularProgressTrack, CircularProgressValueText, type CircularProgressValueTextProps };
|
|
55
|
+
export { CircularProgress, CircularProgressCombined, CircularProgressIndicator, type CircularProgressProps, CircularProgressRange, type CircularProgressTone, CircularProgressTrack, CircularProgressValueText, type CircularProgressValueTextProps };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
4
5
|
import * as React from "react";
|
|
5
6
|
import { cn } from "../../lib/cn.js";
|
|
6
7
|
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
@@ -10,6 +11,34 @@ const TRACK_NAME = "CircularProgressTrack";
|
|
|
10
11
|
const RANGE_NAME = "CircularProgressRange";
|
|
11
12
|
const VALUE_TEXT_NAME = "CircularProgressValueText";
|
|
12
13
|
const DEFAULT_MAX = 100;
|
|
14
|
+
const circularProgressRangeVariants = cva("", {
|
|
15
|
+
variants: {
|
|
16
|
+
tone: {
|
|
17
|
+
default: "text-primary",
|
|
18
|
+
neutral: "text-muted-foreground",
|
|
19
|
+
success: "text-success",
|
|
20
|
+
warning: "text-warning",
|
|
21
|
+
severe: "text-severe",
|
|
22
|
+
destructive: "text-destructive",
|
|
23
|
+
info: "text-info"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: { tone: "default" }
|
|
27
|
+
});
|
|
28
|
+
const circularProgressTrackVariants = cva("", {
|
|
29
|
+
variants: {
|
|
30
|
+
tone: {
|
|
31
|
+
default: "text-gold-100 dark:text-gold-500/30",
|
|
32
|
+
neutral: "text-muted-foreground/20",
|
|
33
|
+
success: "text-success-ink/20",
|
|
34
|
+
warning: "text-warning-ink/20",
|
|
35
|
+
severe: "text-severe-ink/20",
|
|
36
|
+
destructive: "text-destructive-ink/20",
|
|
37
|
+
info: "text-info-ink/20"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
defaultVariants: { tone: "default" }
|
|
41
|
+
});
|
|
13
42
|
const IS_DEV = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
|
|
14
43
|
function getProgressState(value, maxValue) {
|
|
15
44
|
return value == null ? "indeterminate" : value === maxValue ? "complete" : "loading";
|
|
@@ -50,6 +79,8 @@ function CircularProgress(props) {
|
|
|
50
79
|
size = 48,
|
|
51
80
|
thickness = 4,
|
|
52
81
|
label,
|
|
82
|
+
tone = "default",
|
|
83
|
+
track = "tone",
|
|
53
84
|
asChild,
|
|
54
85
|
className,
|
|
55
86
|
children,
|
|
@@ -96,10 +127,27 @@ function CircularProgress(props) {
|
|
|
96
127
|
center,
|
|
97
128
|
circumference,
|
|
98
129
|
percentage,
|
|
130
|
+
tone,
|
|
131
|
+
track,
|
|
99
132
|
valueTextId,
|
|
100
133
|
onValueTextMountChange: setHasValueText
|
|
101
134
|
}),
|
|
102
|
-
[
|
|
135
|
+
[
|
|
136
|
+
value,
|
|
137
|
+
valueText,
|
|
138
|
+
max,
|
|
139
|
+
min,
|
|
140
|
+
state,
|
|
141
|
+
radius,
|
|
142
|
+
thickness,
|
|
143
|
+
size,
|
|
144
|
+
center,
|
|
145
|
+
circumference,
|
|
146
|
+
percentage,
|
|
147
|
+
tone,
|
|
148
|
+
track,
|
|
149
|
+
valueTextId
|
|
150
|
+
]
|
|
103
151
|
);
|
|
104
152
|
const RootPrimitive = asChild ? Slot : "div";
|
|
105
153
|
return /* @__PURE__ */ jsx(CircularProgressContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs(
|
|
@@ -117,6 +165,7 @@ function CircularProgress(props) {
|
|
|
117
165
|
"data-max": max,
|
|
118
166
|
"data-min": min,
|
|
119
167
|
"data-percentage": percentage,
|
|
168
|
+
"data-tone": tone,
|
|
120
169
|
"data-slot": "circular-progress",
|
|
121
170
|
...rootProps,
|
|
122
171
|
className: cn("relative inline-flex w-fit items-center justify-center", label && "flex-col gap-1.5", className),
|
|
@@ -169,26 +218,12 @@ function CircularProgressTrack(props) {
|
|
|
169
218
|
...trackProps,
|
|
170
219
|
className: cn(
|
|
171
220
|
/*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* rãnh trung tính tint `primary-ink/20`
|
|
178
|
-
* cung ⇄ rãnh trên --background : 5,03 (L) · 9,65 (D) 4,85 (L) · 7,97 (D)
|
|
179
|
-
* tệ nhất, rãnh trên --muted : 4,71 (L) · 8,00 (D) 4,54 (L) · 6,52 (D)
|
|
180
|
-
* (viền trang trí --border : 2,62 (L) ✗ — loại)
|
|
181
|
-
*
|
|
182
|
-
* Và nó không phụ thuộc tông: app đổi cung sang `text-success-ink`/`text-destructive-ink`
|
|
183
|
-
* thì rãnh vẫn đúng (5,03 · 5,51:1 trên nền trắng) mà không phải đổi theo.
|
|
184
|
-
*
|
|
185
|
-
* Rãnh ⇄ nền trang chỉ ~1,3:1 — CỐ Ý, như rãnh `Progress`: không có cặp màu nào vừa ≥3:1
|
|
186
|
-
* với nhau vừa ≥3:1 với nền trắng (chứng minh ở chú thích `Slider`). Khác `Progress` ở chỗ
|
|
187
|
-
* vòng KHÔNG cần viền khoanh mốc 100%: hình tròn tự khép và cung luôn bắt đầu ở 12 giờ, nên
|
|
188
|
-
* TỈ LỆ đọc được từ góc của cung mà không cần thấy rãnh. Một nét tròn liền, không nét nào
|
|
189
|
-
* cắt nhau, nên màu có alpha không để lộ vệt nối (xem "Cạm bẫy" về icon trong CLAUDE.md).
|
|
221
|
+
* Rãnh là tint nhạt của `tone` (xem đầu file); `track="neutral"` = xám trung tính của thượng nguồn.
|
|
222
|
+
* Rãnh ⇄ nền trang chỉ ~1,1–1,3:1 — CỐ Ý, như rãnh `Progress`: thứ mang THÔNG TIN là ranh giới
|
|
223
|
+
* cung ⇄ rãnh (chỗ cung dừng lại là giá trị), và cung bậc đậm đạt ≥3:1 với cả rãnh lẫn nền trang.
|
|
224
|
+
* Một nét tròn liền, không nét nào cắt nhau, nên màu có alpha không để lộ vệt nối.
|
|
190
225
|
*/
|
|
191
|
-
"
|
|
226
|
+
circularProgressTrackVariants({ tone: context.track === "neutral" ? "neutral" : context.tone }),
|
|
192
227
|
className
|
|
193
228
|
)
|
|
194
229
|
}
|
|
@@ -220,13 +255,12 @@ function CircularProgressRange(props) {
|
|
|
220
255
|
...rangeProps,
|
|
221
256
|
className: cn(
|
|
222
257
|
/*
|
|
223
|
-
* ⚠ Khác thượng nguồn: `text-primary` (bậc TÔ NỀN
|
|
224
|
-
* → `text-primary-ink`, đúng bậc của thanh `Progress`: 6,51:1 (light) · 12,67:1 (dark)
|
|
225
|
-
* trên --background, tệ nhất 6,09:1 · 10,80:1 trên --muted (bảng tương phản cao). Cung là
|
|
258
|
+
* ⚠ Khác thượng nguồn: `text-primary` (bậc TÔ NỀN) → bậc đậm của `tone` (xem đầu file). Cung là
|
|
226
259
|
* thông tin thị giác (WCAG 1.4.11, 3:1).
|
|
227
260
|
* Hiệu ứng quay đọc `--animate-spin-around` của styles.css; `motion-reduce` thì đứng yên.
|
|
228
261
|
*/
|
|
229
|
-
"origin-center
|
|
262
|
+
"origin-center transition-all duration-300 ease-in-out",
|
|
263
|
+
circularProgressRangeVariants({ tone: context.tone }),
|
|
230
264
|
context.state === "indeterminate" && "motion-safe:[animation:var(--animate-spin-around)] motion-reduce:animate-none",
|
|
231
265
|
className
|
|
232
266
|
)
|