@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/ui/circular-progress.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/circular-progress) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\n\nconst CIRCULAR_PROGRESS_NAME = \"CircularProgress\";\nconst INDICATOR_NAME = \"CircularProgressIndicator\";\nconst TRACK_NAME = \"CircularProgressTrack\";\nconst RANGE_NAME = \"CircularProgressRange\";\nconst VALUE_TEXT_NAME = \"CircularProgressValueText\";\n\nconst DEFAULT_MAX = 100;\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc đọc thẳng `process.env.NODE_ENV` ngay trong lúc render. Gói ra ESM\n * từng file, không bundle; ở môi trường không có bundler thay chuỗi đó (ESM thuần trên trình\n * duyệt), `process` không tồn tại → ReferenceError lúc render. Bọc `typeof` để chỗ đó chỉ mất cảnh\n * báo dev chứ không gãy; bundler vẫn thay `process.env.NODE_ENV` và loại nhánh dev như cũ.\n */\nconst IS_DEV = typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\";\n\ntype ProgressState = \"indeterminate\" | \"complete\" | \"loading\";\n\nfunction getProgressState(value: number | undefined | null, maxValue: number): ProgressState {\n return value == null ? \"indeterminate\" : value === maxValue ? \"complete\" : \"loading\";\n}\n\nfunction getIsValidNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction getIsValidMaxNumber(max: unknown): max is number {\n return getIsValidNumber(max) && max > 0;\n}\n\nfunction getIsValidValueNumber(value: unknown, min: number, max: number): value is number {\n return getIsValidNumber(value) && value <= max && value >= min;\n}\n\nfunction getDefaultValueText(value: number, min: number, max: number): string {\n const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100;\n return `${Math.round(percentage)}%`;\n}\n\nfunction getInvalidValueError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`value\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. \\`value\\` phải là số trong khoảng \\`min\\`…\\`max\\` (tính cả hai đầu), hoặc \\`null\\`/\\`undefined\\` cho tiến trình không xác định. Giá trị sẽ bị kẹp về khoảng hợp lệ.`;\n}\n\nfunction getInvalidMaxError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`max\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. Chỉ nhận số lớn hơn 0 — dùng mặc định ${DEFAULT_MAX}.`;\n}\n\ninterface CircularProgressContextValue {\n value: number | null;\n valueText: string | undefined;\n max: number;\n min: number;\n state: ProgressState;\n radius: number;\n thickness: number;\n size: number;\n center: number;\n circumference: number;\n percentage: number | null;\n valueTextId?: string;\n /** `CircularProgressValueText` báo nó đang có mặt — xem chú thích ở `aria-describedby`. */\n onValueTextMountChange: (mounted: boolean) => void;\n}\n\nconst CircularProgressContext = React.createContext<CircularProgressContextValue | null>(null);\n\nfunction useCircularProgressContext(consumerName: string) {\n const context = React.useContext(CircularProgressContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${CIRCULAR_PROGRESS_NAME}\\``);\n }\n return context;\n}\n\ninterface CircularProgressProps extends React.ComponentProps<\"div\"> {\n /** Giá trị hiện tại; `null`/`undefined` = không xác định (vòng quay). */\n value?: number | null | undefined;\n /**\n * Chuỗi trình đọc màn hình đọc thay cho con số (`aria-valuetext`) và chữ giữa vòng — mặc định\n * phần trăm làm tròn, vd `\"45%\"`.\n */\n getValueText?(value: number, min: number, max: number): string;\n min?: number;\n max?: number;\n /** Đường kính, px. */\n size?: number;\n /** Độ dày nét, px. */\n thickness?: number;\n /**\n * Nhãn HIỆN của vòng, đồng thời là tên của `role=\"progressbar\"`. Không đặt nhãn hiện thì PHẢI\n * truyền `aria-label` — progressbar không tên là lỗi a11y (ARIA 1.2), gói không tự đặt tên\n * chung chung thay app (giống `Progress`).\n */\n label?: string;\n asChild?: boolean;\n}\n\n/**\n * CircularProgress — tiến trình dạng VÒNG, cặp với `Progress` (dạng thanh) của gói.\n * Ráp từ các phần (`CircularProgressIndicator` › `Track` + `Range`, `CircularProgressValueText`),\n * hoặc dùng sẵn `CircularProgressCombined`.\n *\n * Phần đã chạy là `text-primary-ink`, cùng bậc với thanh `Progress`; rãnh xám trung tính. Đổi tông\n * thì truyền bậc INK cho `CircularProgressRange` (vd `className=\"text-success-ink\"`), không phải\n * token tô nền `text-success` — rãnh không cần đổi theo (số đo ở `CircularProgressTrack`).\n *\n * ⚠ Trạng thái không xác định cần `--animate-spin-around` + `@keyframes spin-around` trong\n * styles.css; thiếu thì vòng đứng yên ở một cung 25% (không lỗi, không cảnh báo).\n */\nfunction CircularProgress(props: CircularProgressProps) {\n const {\n value: valueProp = null,\n getValueText = getDefaultValueText,\n min: minProp = 0,\n max: maxProp,\n size = 48,\n thickness = 4,\n label,\n asChild,\n className,\n children,\n ...rootProps\n } = props;\n\n if ((maxProp || maxProp === 0) && !getIsValidMaxNumber(maxProp)) {\n if (IS_DEV) {\n console.error(getInvalidMaxError(`${maxProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const rawMax = getIsValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX;\n const min = getIsValidNumber(minProp) ? minProp : 0;\n const max = rawMax <= min ? min + 1 : rawMax;\n\n if (IS_DEV && thickness >= size) {\n console.warn(\n `CircularProgress: \\`thickness\\` (${thickness}) phải nhỏ hơn \\`size\\` (${size}) thì vòng mới vẽ đúng.`\n );\n }\n\n if (valueProp !== null && !getIsValidValueNumber(valueProp, min, max)) {\n if (IS_DEV) {\n console.error(getInvalidValueError(`${valueProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const value = getIsValidValueNumber(valueProp, min, max)\n ? valueProp\n : getIsValidNumber(valueProp) && valueProp > max\n ? max\n : getIsValidNumber(valueProp) && valueProp < min\n ? min\n : null;\n\n const valueText = getIsValidNumber(value) ? getValueText(value, min, max) : undefined;\n const state = getProgressState(value, max);\n const radius = Math.max(0, (size - thickness) / 2);\n const center = size / 2;\n const circumference = 2 * Math.PI * radius;\n\n const percentage = getIsValidNumber(value) ? (max === min ? 1 : (value - min) / (max - min)) : null;\n\n const labelId = React.useId();\n const valueTextId = React.useId();\n const [hasValueText, setHasValueText] = React.useState(false);\n\n const contextValue = React.useMemo<CircularProgressContextValue>(\n () => ({\n value,\n valueText,\n max,\n min,\n state,\n radius,\n thickness,\n size,\n center,\n circumference,\n percentage,\n valueTextId,\n onValueTextMountChange: setHasValueText\n }),\n [value, valueText, max, min, state, radius, thickness, size, center, circumference, percentage, valueTextId]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <CircularProgressContext.Provider value={contextValue}>\n <RootPrimitive\n role=\"progressbar\"\n /*\n * ⚠ Khác thượng nguồn (3.0.1): chỉ trỏ tới phần chữ giá trị khi `CircularProgressValueText`\n * THẬT SỰ có trên trang. Bản gốc luôn đặt `aria-describedby` khi có giá trị — vòng không vẽ\n * chữ (chỉ vòng, hay nhãn ở chỗ khác) thì thuộc tính trỏ vào một id không phần tử nào mang\n * (axe: \"aria-describedby references an element that does not exist\"). Giá trị vẫn đi qua\n * `aria-valuetext` như cũ.\n */\n aria-describedby={valueText && hasValueText ? valueTextId : undefined}\n aria-labelledby={label ? labelId : undefined}\n aria-valuemax={max}\n aria-valuemin={min}\n aria-valuenow={getIsValidNumber(value) ? value : undefined}\n aria-valuetext={valueText}\n data-state={state}\n data-value={value ?? undefined}\n data-max={max}\n data-min={min}\n data-percentage={percentage}\n data-slot=\"circular-progress\"\n {...rootProps}\n className={cn(\"relative inline-flex w-fit items-center justify-center\", label && \"flex-col gap-1.5\", className)}\n >\n {/*\n * ⚠ Khác thượng nguồn: bản gốc đặt nhãn làm anh em của vòng NGAY TRONG gốc, trong khi\n * `CircularProgressValueText` là `absolute inset-0` phủ CẢ gốc — có nhãn là chữ \"65%\" đè\n * lên chữ nhãn. Khi có `label`: bọc phần con trong một khung đúng cỡ vòng (`relative`, nên\n * chữ giá trị căn giữa VÒNG), nhãn xuống dưới vòng. Không có `label` thì DOM y như bản gốc.\n */}\n {label ? (\n <div data-slot=\"circular-progress-ring\" className=\"relative inline-flex items-center justify-center\">\n {children}\n </div>\n ) : (\n children\n )}\n {label && (\n <div id={labelId} data-slot=\"circular-progress-label\" className=\"text-sm\">\n {label}\n </div>\n )}\n </RootPrimitive>\n </CircularProgressContext.Provider>\n );\n}\n\nfunction CircularProgressIndicator(props: React.ComponentProps<\"svg\">) {\n const { className, ...indicatorProps } = props;\n\n const context = useCircularProgressContext(INDICATOR_NAME);\n\n return (\n <svg\n aria-hidden=\"true\"\n focusable=\"false\"\n viewBox={`0 0 ${context.size} ${context.size}`}\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-percentage={context.percentage}\n data-slot=\"circular-progress-indicator\"\n width={context.size}\n height={context.size}\n {...indicatorProps}\n className={cn(\"-rotate-90 transform\", className)}\n />\n );\n}\n\nCircularProgressIndicator.displayName = INDICATOR_NAME;\n\nfunction CircularProgressTrack(props: React.ComponentProps<\"circle\">) {\n const { className, ...trackProps } = props;\n\n const context = useCircularProgressContext(TRACK_NAME);\n\n return (\n <circle\n data-state={context.state}\n data-slot=\"circular-progress-track\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n {...trackProps}\n className={cn(\n /*\n * GIỮ NGUYÊN thượng nguồn — rãnh xám trung tính `text-muted-foreground/20`, KHÔNG phải tint\n * `/20` của bậc ink như rãnh `Progress`. Đã đo cả hai; thứ mang THÔNG TIN ở đây là ranh\n * giới cung-đã-chạy ⇄ rãnh (chỗ cung dừng lại chính là giá trị), và rãnh trung tính giữ\n * ranh giới đó TỐT HƠN (bảng tương phản cao, cung `text-primary-ink`):\n *\n * rãnh trung tính tint `primary-ink/20`\n * cung ⇄ rãnh trên --background : 5,03 (L) · 9,65 (D) 4,85 (L) · 7,97 (D)\n * tệ nhất, rãnh trên --muted : 4,71 (L) · 8,00 (D) 4,54 (L) · 6,52 (D)\n * (viền trang trí --border : 2,62 (L) ✗ — loại)\n *\n * Và nó không phụ thuộc tông: app đổi cung sang `text-success-ink`/`text-destructive-ink`\n * thì rãnh vẫn đúng (5,03 · 5,51:1 trên nền trắng) mà không phải đổi theo.\n *\n * 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\n * với nhau vừa ≥3:1 với nền trắng (chứng minh ở chú thích `Slider`). Khác `Progress` ở chỗ\n * 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\n * 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\n * 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).\n */\n \"text-muted-foreground/20\",\n className\n )}\n />\n );\n}\n\nfunction CircularProgressRange(props: React.ComponentProps<\"circle\">) {\n const { className, ...rangeProps } = props;\n\n const context = useCircularProgressContext(RANGE_NAME);\n\n const strokeDasharray = context.circumference;\n const strokeDashoffset =\n context.state === \"indeterminate\"\n ? context.circumference * 0.75\n : context.percentage !== null\n ? context.circumference - context.percentage * context.circumference\n : context.circumference;\n\n return (\n <circle\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-slot=\"circular-progress-range\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n strokeDasharray={strokeDasharray}\n strokeDashoffset={strokeDashoffset}\n vectorEffect=\"non-scaling-stroke\"\n {...rangeProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: `text-primary` (bậc TÔ NỀN, gold #E8B824 chỉ 1,86:1 trên nền trắng)\n * → `text-primary-ink`, đúng bậc của thanh `Progress`: 6,51:1 (light) · 12,67:1 (dark)\n * trên --background, tệ nhất 6,09:1 · 10,80:1 trên --muted (bảng tương phản cao). Cung là\n * thông tin thị giác (WCAG 1.4.11, 3:1).\n * Hiệu ứng quay đọc `--animate-spin-around` của styles.css; `motion-reduce` thì đứng yên.\n */\n \"origin-center text-primary-ink transition-all duration-300 ease-in-out\",\n context.state === \"indeterminate\" &&\n \"motion-safe:[animation:var(--animate-spin-around)] motion-reduce:animate-none\",\n className\n )}\n />\n );\n}\n\ninterface CircularProgressValueTextProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction CircularProgressValueText(props: CircularProgressValueTextProps) {\n const { asChild, className, children, ...valueTextProps } = props;\n\n const context = useCircularProgressContext(VALUE_TEXT_NAME);\n const { onValueTextMountChange } = context;\n\n useIsomorphicLayoutEffect(() => {\n onValueTextMountChange(true);\n return () => onValueTextMountChange(false);\n }, [onValueTextMountChange]);\n\n const ValueTextPrimitive = asChild ? Slot : \"span\";\n\n return (\n <ValueTextPrimitive\n id={context.valueTextId}\n data-state={context.state}\n data-slot=\"circular-progress-value-text\"\n {...valueTextProps}\n className={cn(\"absolute inset-0 flex items-center justify-center text-sm font-medium\", className)}\n >\n {children ?? context.valueText}\n </ValueTextPrimitive>\n );\n}\n\nfunction CircularProgressCombined(props: CircularProgressProps) {\n return (\n <CircularProgress {...props}>\n <CircularProgressIndicator>\n <CircularProgressTrack />\n <CircularProgressRange />\n </CircularProgressIndicator>\n <CircularProgressValueText />\n </CircularProgress>\n );\n}\n\nexport {\n CircularProgress,\n CircularProgressCombined,\n CircularProgressIndicator,\n type CircularProgressProps,\n CircularProgressRange,\n CircularProgressTrack,\n CircularProgressValueText,\n type CircularProgressValueTextProps\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/circular-progress.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/circular-progress) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\n\nconst CIRCULAR_PROGRESS_NAME = \"CircularProgress\";\nconst INDICATOR_NAME = \"CircularProgressIndicator\";\nconst TRACK_NAME = \"CircularProgressTrack\";\nconst RANGE_NAME = \"CircularProgressRange\";\nconst VALUE_TEXT_NAME = \"CircularProgressValueText\";\n\nconst DEFAULT_MAX = 100;\n\n/*\n * ⚠ Khác thượng nguồn: MÀU đi theo `tone` — cùng bộ tông với `StatusPill` (`neutral` · `default` = primary ·\n * `success` · `warning` · `severe` · `destructive` · `info`).\n *\n * CUNG là bậc TÔ NỀN của tông (`--success`, `--warning`…, `default` = `--primary`) — cùng bậc với chấm của\n * `StatusPill` `severe` và màu thanh Gantt cũ. Bậc `-ink` nặng quá ở cỡ nét 4px (duyệt ở comitor-ds). Cái giá:\n * cung ⇄ rãnh chỉ ~1,4–3,3:1 ở light (vàng/xanh lá thấp nhất), dưới 3:1 của WCAG 1.4.11 — chấp nhận vì cung\n * KHÔNG phải nơi duy nhất mang giá trị: số ở giữa (`CircularProgressValueText`) và `aria-valuetext` mang nó.\n * Ở dark, bậc `-ink` vốn trỏ về chính `--x`, nên không đổi gì.\n *\n * RÃNH là tint `/20` của bậc `-ink` (như rãnh `Progress`); `default` dùng `gold-100` (dark `gold-500/30`).\n * `track=\"neutral\"` giữ rãnh xám `text-muted-foreground/20` của thượng nguồn.\n *\n * Hai `cva` chứ không `Record`: cổng tương phản chỉ đọc chuỗi lớp trong `cva`/`className`/`cn()` trực tiếp\n * (xem đầu `progress.tsx`). Miễn trừ của từng cặp (kèm số đo) ở `scripts/contrast-exemptions.json`.\n */\nconst circularProgressRangeVariants = cva(\"\", {\n variants: {\n tone: {\n default: \"text-primary\",\n neutral: \"text-muted-foreground\",\n success: \"text-success\",\n warning: \"text-warning\",\n severe: \"text-severe\",\n destructive: \"text-destructive\",\n info: \"text-info\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\nconst circularProgressTrackVariants = cva(\"\", {\n variants: {\n tone: {\n default: \"text-gold-100 dark:text-gold-500/30\",\n neutral: \"text-muted-foreground/20\",\n success: \"text-success-ink/20\",\n warning: \"text-warning-ink/20\",\n severe: \"text-severe-ink/20\",\n destructive: \"text-destructive-ink/20\",\n info: \"text-info-ink/20\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\n/** Tông của vòng — cùng bộ tông với `StatusPill` (`default` là primary). */\ntype CircularProgressTone = \"default\" | \"neutral\" | \"success\" | \"warning\" | \"severe\" | \"destructive\" | \"info\";\n\n/*\n * ⚠ Khác thượng nguồn: bản gốc đọc thẳng `process.env.NODE_ENV` ngay trong lúc render. Gói ra ESM\n * từng file, không bundle; ở môi trường không có bundler thay chuỗi đó (ESM thuần trên trình\n * duyệt), `process` không tồn tại → ReferenceError lúc render. Bọc `typeof` để chỗ đó chỉ mất cảnh\n * báo dev chứ không gãy; bundler vẫn thay `process.env.NODE_ENV` và loại nhánh dev như cũ.\n */\nconst IS_DEV = typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\";\n\ntype ProgressState = \"indeterminate\" | \"complete\" | \"loading\";\n\nfunction getProgressState(value: number | undefined | null, maxValue: number): ProgressState {\n return value == null ? \"indeterminate\" : value === maxValue ? \"complete\" : \"loading\";\n}\n\nfunction getIsValidNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction getIsValidMaxNumber(max: unknown): max is number {\n return getIsValidNumber(max) && max > 0;\n}\n\nfunction getIsValidValueNumber(value: unknown, min: number, max: number): value is number {\n return getIsValidNumber(value) && value <= max && value >= min;\n}\n\nfunction getDefaultValueText(value: number, min: number, max: number): string {\n const percentage = max === min ? 100 : ((value - min) / (max - min)) * 100;\n return `${Math.round(percentage)}%`;\n}\n\nfunction getInvalidValueError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`value\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. \\`value\\` phải là số trong khoảng \\`min\\`…\\`max\\` (tính cả hai đầu), hoặc \\`null\\`/\\`undefined\\` cho tiến trình không xác định. Giá trị sẽ bị kẹp về khoảng hợp lệ.`;\n}\n\nfunction getInvalidMaxError(propValue: string, componentName: string): string {\n // i18n-ignore: thông điệp cho LẬP TRÌNH VIÊN (đi vào console.error), không phải chữ hiển thị\n return `Prop \\`max\\` = \\`${propValue}\\` truyền cho \\`${componentName}\\` không hợp lệ. Chỉ nhận số lớn hơn 0 — dùng mặc định ${DEFAULT_MAX}.`;\n}\n\ninterface CircularProgressContextValue {\n value: number | null;\n valueText: string | undefined;\n max: number;\n min: number;\n state: ProgressState;\n radius: number;\n thickness: number;\n size: number;\n center: number;\n circumference: number;\n percentage: number | null;\n tone: CircularProgressTone;\n track: \"tone\" | \"neutral\";\n valueTextId?: string;\n /** `CircularProgressValueText` báo nó đang có mặt — xem chú thích ở `aria-describedby`. */\n onValueTextMountChange: (mounted: boolean) => void;\n}\n\nconst CircularProgressContext = React.createContext<CircularProgressContextValue | null>(null);\n\nfunction useCircularProgressContext(consumerName: string) {\n const context = React.useContext(CircularProgressContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${CIRCULAR_PROGRESS_NAME}\\``);\n }\n return context;\n}\n\ninterface CircularProgressProps extends React.ComponentProps<\"div\"> {\n /** Giá trị hiện tại; `null`/`undefined` = không xác định (vòng quay). */\n value?: number | null | undefined;\n /**\n * Chuỗi trình đọc màn hình đọc thay cho con số (`aria-valuetext`) và chữ giữa vòng — mặc định\n * phần trăm làm tròn, vd `\"45%\"`.\n */\n getValueText?(value: number, min: number, max: number): string;\n min?: number;\n max?: number;\n /** Đường kính, px. */\n size?: number;\n /** Độ dày nét, px. */\n thickness?: number;\n /**\n * Nhãn HIỆN của vòng, đồng thời là tên của `role=\"progressbar\"`. Không đặt nhãn hiện thì PHẢI\n * truyền `aria-label` — progressbar không tên là lỗi a11y (ARIA 1.2), gói không tự đặt tên\n * chung chung thay app (giống `Progress`).\n */\n label?: string;\n /** 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`). */\n tone?: CircularProgressTone;\n /** `\"neutral\"` = rãnh xám trung tính, chỉ cung mang màu `tone`. Mặc định `\"tone\"`. */\n track?: \"tone\" | \"neutral\";\n asChild?: boolean;\n}\n\n/**\n * CircularProgress — tiến trình dạng VÒNG, cặp với `Progress` (dạng thanh) của gói.\n * Ráp từ các phần (`CircularProgressIndicator` › `Track` + `Range`, `CircularProgressValueText`),\n * hoặc dùng sẵn `CircularProgressCombined`.\n *\n * 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\"`\n * giữ rãnh xám. `className` trên `CircularProgressRange`/`Track` vẫn đè được.\n *\n * ⚠ Trạng thái không xác định cần `--animate-spin-around` + `@keyframes spin-around` trong\n * styles.css; thiếu thì vòng đứng yên ở một cung 25% (không lỗi, không cảnh báo).\n */\nfunction CircularProgress(props: CircularProgressProps) {\n const {\n value: valueProp = null,\n getValueText = getDefaultValueText,\n min: minProp = 0,\n max: maxProp,\n size = 48,\n thickness = 4,\n label,\n tone = \"default\",\n track = \"tone\",\n asChild,\n className,\n children,\n ...rootProps\n } = props;\n\n if ((maxProp || maxProp === 0) && !getIsValidMaxNumber(maxProp)) {\n if (IS_DEV) {\n console.error(getInvalidMaxError(`${maxProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const rawMax = getIsValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX;\n const min = getIsValidNumber(minProp) ? minProp : 0;\n const max = rawMax <= min ? min + 1 : rawMax;\n\n if (IS_DEV && thickness >= size) {\n console.warn(\n `CircularProgress: \\`thickness\\` (${thickness}) phải nhỏ hơn \\`size\\` (${size}) thì vòng mới vẽ đúng.`\n );\n }\n\n if (valueProp !== null && !getIsValidValueNumber(valueProp, min, max)) {\n if (IS_DEV) {\n console.error(getInvalidValueError(`${valueProp}`, CIRCULAR_PROGRESS_NAME));\n }\n }\n\n const value = getIsValidValueNumber(valueProp, min, max)\n ? valueProp\n : getIsValidNumber(valueProp) && valueProp > max\n ? max\n : getIsValidNumber(valueProp) && valueProp < min\n ? min\n : null;\n\n const valueText = getIsValidNumber(value) ? getValueText(value, min, max) : undefined;\n const state = getProgressState(value, max);\n const radius = Math.max(0, (size - thickness) / 2);\n const center = size / 2;\n const circumference = 2 * Math.PI * radius;\n\n const percentage = getIsValidNumber(value) ? (max === min ? 1 : (value - min) / (max - min)) : null;\n\n const labelId = React.useId();\n const valueTextId = React.useId();\n const [hasValueText, setHasValueText] = React.useState(false);\n\n const contextValue = React.useMemo<CircularProgressContextValue>(\n () => ({\n value,\n valueText,\n max,\n min,\n state,\n radius,\n thickness,\n size,\n center,\n circumference,\n percentage,\n tone,\n track,\n valueTextId,\n onValueTextMountChange: setHasValueText\n }),\n [\n value,\n valueText,\n max,\n min,\n state,\n radius,\n thickness,\n size,\n center,\n circumference,\n percentage,\n tone,\n track,\n valueTextId\n ]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <CircularProgressContext.Provider value={contextValue}>\n <RootPrimitive\n role=\"progressbar\"\n /*\n * ⚠ Khác thượng nguồn (3.0.1): chỉ trỏ tới phần chữ giá trị khi `CircularProgressValueText`\n * THẬT SỰ có trên trang. Bản gốc luôn đặt `aria-describedby` khi có giá trị — vòng không vẽ\n * chữ (chỉ vòng, hay nhãn ở chỗ khác) thì thuộc tính trỏ vào một id không phần tử nào mang\n * (axe: \"aria-describedby references an element that does not exist\"). Giá trị vẫn đi qua\n * `aria-valuetext` như cũ.\n */\n aria-describedby={valueText && hasValueText ? valueTextId : undefined}\n aria-labelledby={label ? labelId : undefined}\n aria-valuemax={max}\n aria-valuemin={min}\n aria-valuenow={getIsValidNumber(value) ? value : undefined}\n aria-valuetext={valueText}\n data-state={state}\n data-value={value ?? undefined}\n data-max={max}\n data-min={min}\n data-percentage={percentage}\n data-tone={tone}\n data-slot=\"circular-progress\"\n {...rootProps}\n className={cn(\"relative inline-flex w-fit items-center justify-center\", label && \"flex-col gap-1.5\", className)}\n >\n {/*\n * ⚠ Khác thượng nguồn: bản gốc đặt nhãn làm anh em của vòng NGAY TRONG gốc, trong khi\n * `CircularProgressValueText` là `absolute inset-0` phủ CẢ gốc — có nhãn là chữ \"65%\" đè\n * lên chữ nhãn. Khi có `label`: bọc phần con trong một khung đúng cỡ vòng (`relative`, nên\n * chữ giá trị căn giữa VÒNG), nhãn xuống dưới vòng. Không có `label` thì DOM y như bản gốc.\n */}\n {label ? (\n <div data-slot=\"circular-progress-ring\" className=\"relative inline-flex items-center justify-center\">\n {children}\n </div>\n ) : (\n children\n )}\n {label && (\n <div id={labelId} data-slot=\"circular-progress-label\" className=\"text-sm\">\n {label}\n </div>\n )}\n </RootPrimitive>\n </CircularProgressContext.Provider>\n );\n}\n\nfunction CircularProgressIndicator(props: React.ComponentProps<\"svg\">) {\n const { className, ...indicatorProps } = props;\n\n const context = useCircularProgressContext(INDICATOR_NAME);\n\n return (\n <svg\n aria-hidden=\"true\"\n focusable=\"false\"\n viewBox={`0 0 ${context.size} ${context.size}`}\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-percentage={context.percentage}\n data-slot=\"circular-progress-indicator\"\n width={context.size}\n height={context.size}\n {...indicatorProps}\n className={cn(\"-rotate-90 transform\", className)}\n />\n );\n}\n\nCircularProgressIndicator.displayName = INDICATOR_NAME;\n\nfunction CircularProgressTrack(props: React.ComponentProps<\"circle\">) {\n const { className, ...trackProps } = props;\n\n const context = useCircularProgressContext(TRACK_NAME);\n\n return (\n <circle\n data-state={context.state}\n data-slot=\"circular-progress-track\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n {...trackProps}\n className={cn(\n /*\n * 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.\n * 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\n * 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.\n * 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.\n */\n circularProgressTrackVariants({ tone: context.track === \"neutral\" ? \"neutral\" : context.tone }),\n className\n )}\n />\n );\n}\n\nfunction CircularProgressRange(props: React.ComponentProps<\"circle\">) {\n const { className, ...rangeProps } = props;\n\n const context = useCircularProgressContext(RANGE_NAME);\n\n const strokeDasharray = context.circumference;\n const strokeDashoffset =\n context.state === \"indeterminate\"\n ? context.circumference * 0.75\n : context.percentage !== null\n ? context.circumference - context.percentage * context.circumference\n : context.circumference;\n\n return (\n <circle\n data-state={context.state}\n data-value={context.value ?? undefined}\n data-max={context.max}\n data-min={context.min}\n data-slot=\"circular-progress-range\"\n cx={context.center}\n cy={context.center}\n r={context.radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={context.thickness}\n strokeLinecap=\"round\"\n strokeDasharray={strokeDasharray}\n strokeDashoffset={strokeDashoffset}\n vectorEffect=\"non-scaling-stroke\"\n {...rangeProps}\n className={cn(\n /*\n * ⚠ Khác thượng nguồn: `text-primary` (bậc TÔ NỀN) → bậc đậm của `tone` (xem đầu file). Cung là\n * thông tin thị giác (WCAG 1.4.11, 3:1).\n * Hiệu ứng quay đọc `--animate-spin-around` của styles.css; `motion-reduce` thì đứng yên.\n */\n \"origin-center transition-all duration-300 ease-in-out\",\n circularProgressRangeVariants({ tone: context.tone }),\n context.state === \"indeterminate\" &&\n \"motion-safe:[animation:var(--animate-spin-around)] motion-reduce:animate-none\",\n className\n )}\n />\n );\n}\n\ninterface CircularProgressValueTextProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction CircularProgressValueText(props: CircularProgressValueTextProps) {\n const { asChild, className, children, ...valueTextProps } = props;\n\n const context = useCircularProgressContext(VALUE_TEXT_NAME);\n const { onValueTextMountChange } = context;\n\n useIsomorphicLayoutEffect(() => {\n onValueTextMountChange(true);\n return () => onValueTextMountChange(false);\n }, [onValueTextMountChange]);\n\n const ValueTextPrimitive = asChild ? Slot : \"span\";\n\n return (\n <ValueTextPrimitive\n id={context.valueTextId}\n data-state={context.state}\n data-slot=\"circular-progress-value-text\"\n {...valueTextProps}\n className={cn(\"absolute inset-0 flex items-center justify-center text-sm font-medium\", className)}\n >\n {children ?? context.valueText}\n </ValueTextPrimitive>\n );\n}\n\nfunction CircularProgressCombined(props: CircularProgressProps) {\n return (\n <CircularProgress {...props}>\n <CircularProgressIndicator>\n <CircularProgressTrack />\n <CircularProgressRange />\n </CircularProgressIndicator>\n <CircularProgressValueText />\n </CircularProgress>\n );\n}\n\nexport {\n CircularProgress,\n CircularProgressCombined,\n CircularProgressIndicator,\n type CircularProgressProps,\n CircularProgressRange,\n type CircularProgressTone,\n CircularProgressTrack,\n CircularProgressValueText,\n type CircularProgressValueTextProps\n};\n"],"mappings":"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","names":[]}
|
|
@@ -20,6 +20,12 @@ import * as React from 'react';
|
|
|
20
20
|
* `aria-valuetext`, kèm lỗi console; nay thanh vẫn đầy VÀ trình đọc màn hình đọc "100%". Nói rõ là
|
|
21
21
|
* đã vượt (bao nhiêu, làm gì tiếp) vẫn là việc của chữ cạnh thanh và `tone`, không của thanh.
|
|
22
22
|
*
|
|
23
|
+
* ─ ⛔ ĐỪNG ĐỔI TÔNG `default` SANG MÀU ĐẬM HƠN (chốt 2026-11-01) ─
|
|
24
|
+
* 3.13.0 đổi thanh sang `gold-600` / rãnh `gold-100` để qua tương phản ở bảng mặc định; trên giao diện thật thanh nâu
|
|
25
|
+
* sẫm nặng hơn mọi thứ quanh nó và bị trả về đây ở 3.13.1. Mức 1,73:1 (thanh ⇄ nền) / 1,55:1 (thanh ⇄ rãnh) ở bảng
|
|
26
|
+
* MẶC ĐỊNH là ĐÃ CHẤP NHẬN — bỏ qua phép đo tương phản cho phần tô này. Cần rõ hơn thì chữ cạnh thanh, `tone`, hoặc
|
|
27
|
+
* bảng tương phản cao (`data-contrast="high"`, nơi các số đo bên dưới đạt). Xem CLAUDE.md, mục màu.
|
|
28
|
+
*
|
|
23
29
|
* ─ MÀU: `--primary-ink`, KHÔNG PHẢI `--primary` ─
|
|
24
30
|
* Phần đã chạy là thông tin thị giác (WCAG 1.4.11, 3:1) và phải nổi trên CẢ nền trang lẫn phần
|
|
25
31
|
* rãnh còn lại. `bg-primary` gold #E8B824 ở light chỉ **1,86:1** với nền trắng và **1,64:1** với
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Progress (Radix) — thanh tiến trình xác định (`value` 0…`max`, `max` mặc định 100).\n * Bỏ trống `value` để về trạng thái indeterminate của Radix (thanh đứng yên ở 0);\n * animation cho indeterminate là việc của app.\n *\n * ─ THANH VẼ THEO `value / max`, VÀ THANH VỚI `aria-*` NÓI CÙNG MỘT CON SỐ ─\n * Radix đọc `max` để dựng `aria-valuemax` và `aria-valuetext` (`round(value / max · 100)%`). Trước\n * `3.3.3` phần đã chạy lại vẽ bằng `100 - value` — bỏ qua `max` —, nên `max={30} value={24}` vẽ 24%\n * trong khi trình đọc màn hình đọc 80%: hai người nhìn cùng một control nhận hai con số.\n *\n * Nay cả hai đi từ MỘT giá trị: `value` được KẸP vào `[0, max]` rồi mới giao cho Radix (`max` không\n * phải số dương → 100, đúng luật Radix). Kẹp chứ không coi là indeterminate, vì ca vượt ngưỡng hay\n * gặp nhất là CÓ Ý: hạn mức dùng 120% phải là thanh ĐẦY, không phải thanh rỗng. Trước `3.3.3` ca đó\n * tình cờ đã đầy (`translateX(--20%)` là CSS vô hiệu) nhưng Radix coi `value` sai → không có\n * `aria-valuetext`, kèm lỗi console; nay thanh vẫn đầy VÀ trình đọc màn hình đọc \"100%\". Nói rõ là\n * đã vượt (bao nhiêu, làm gì tiếp) vẫn là việc của chữ cạnh thanh và `tone`, không của thanh.\n *\n * ─ MÀU: `--primary-ink`, KHÔNG PHẢI `--primary` ─\n * Phần đã chạy là thông tin thị giác (WCAG 1.4.11, 3:1) và phải nổi trên CẢ nền trang lẫn phần\n * rãnh còn lại. `bg-primary` gold #E8B824 ở light chỉ **1,86:1** với nền trắng và **1,64:1** với\n * rãnh `bg-primary/20` — thanh 8px vàng nhạt trên nền trắng gần như không thấy. Bậc ink của\n * brand đạt cả hai phía ở cả hai theme:\n *\n * đã chạy ⇄ nền trang : 6,51:1 (light) · 12,67:1 (dark) ✓\n * đã chạy ⇄ rãnh `/20` : 4,85:1 (light) · 7,97:1 (dark) ✓\n * (tệ nhất khi rãnh nằm trên --muted: 4,54:1 · 6,52:1 ✓)\n *\n * ─ RÃNH: TINT `/20` GIỮ NGUYÊN, NHƯNG PHẢI CÓ VIỀN `--control-edge` ─\n * Bản trước chỉ có tint: rãnh ⇄ nền trang mới **1,34:1** (light) và **1,59–1,66:1** (dark), tức\n * KHÔNG nhìn thấy mốc 100% — ở `value` nhỏ không đọc được TỈ LỆ, ở `value=0` cả control biến mất.\n * Cách sửa hiển nhiên (đổi rãnh sang `bg-control-edge` như Slider/Switch) lại hỏng chỗ khác:\n * `--primary-ink` ⇄ `--control-edge` chỉ **1,70:1** (light) / **2,57:1** (dark), mà Progress\n * KHÔNG có núm nên ranh giới đã-chạy/chưa-chạy là thứ DUY NHẤT mang giá trị — mất nó là mất\n * thông tin. Với nền trang trắng không tồn tại cặp màu nào vừa ≥3:1 với nhau vừa ≥3:1 với nền\n * (chứng minh trong chú thích của `Slider`), nên hai ràng buộc phải tách ra hai lớp:\n *\n * khoanh vùng control → `border-control-edge` ⇄ nền trang : 3,83:1 (light) · 4,94:1 (dark) ✓\n * ⇄ --muted : 3,58:1 · 4,21:1 ✓\n * đọc tỉ lệ → đã chạy ⇄ rãnh `/20` : 4,85:1 · 7,97:1 ✓\n *\n * Đây đúng vai trò hợp đồng đặt cho `--control-edge` (styles.css §RANH GIỚI): \"khi nền control\n * trong suốt/nhạt thì VIỀN là thứ duy nhất khoanh vùng\".\n *\n * ─ TÔNG: BỐN GIÁ TRỊ, VÀ `warning` KHÔNG ĐƯỢC PHÉP CHỈ LÀ MỘT MÀU KHÁC ─\n * `tone` đổi CẢ phần đã chạy lẫn rãnh sang cùng một bậc ink, nên tỉ lệ đọc-được giữ nguyên cấu\n * trúc của bản mặc định. Đo trên ba bề mặt (`--background`, `--card`, `--muted`), tệ nhất mỗi cột.\n *\n * ⚠ Cột \"light\"/\"dark\" dưới đây là **bảng TƯƠNG PHẢN CAO** — đúng bảng mà cổng màu đo, và lý do\n * ghi ở đầu `scripts/a11y/tailwind.mjs`. Ở bảng mặc định (bản duyệt comitor-ds) `--primary-ink`\n * chính là `--primary` gold #E8B824, nên tông `default` chỉ được 1,73:1 với nền và 1,55:1 với\n * rãnh: đó là con số của BẢN DUYỆT, không phải lỗi của component — đổi nó là đổi bảng màu.\n *\n * tông đã chạy ⇄ nền trang đã chạy ⇄ rãnh cùng tông /20\n * default 6,09:1 (light) 10,80 (dark) 4,54:1 (light) 6,52:1 (dark) ✓\n * success 6,09:1 9,25 4,55:1 5,88:1 ✓\n * warning 6,09:1 10,52 4,54:1 6,47:1 ✓\n * destructive 6,68:1 5,82 4,85:1 4,25:1 ✓\n *\n * ⚠ NHƯNG `warning` KHÔNG phân biệt được với `default` BẰNG MÀU, và đó là số đo chứ không phải\n * cảm tính. Đo `--warning-ink` ⇄ `--primary-ink` trên cả BỐN bảng màu của gói:\n *\n * light (mặc định) #7A5800 ⇄ #E8B824 3,51:1 ✓\n * light (tương phản cao) #7A5800 ⇄ #7A5800 1,00:1 ✗ CÙNG MỘT HEX\n * dark (mặc định) #F2C94C ⇄ #F5D060 1,06:1 ✗\n * dark (tương phản cao) #FACC15 ⇄ #F5D060 1,03:1 ✗\n *\n * Ba trên bốn bảng màu cho ra hai thanh trông y hệt nhau. Đây KHÔNG sửa được ở tầng token: brand\n * của Comitor là gold, warning là yellow — cùng một sắc. Muốn tách hai bậc ink ra 3:1 thì một\n * trong hai phải rời khỏi vùng đọc-được: với nền trắng, chữ đạt 4,5:1 đòi độ chói ≤ 0,183, mà\n * cách `--gold-500` (0,111) đúng 3:1 thì phải ≤ 0,004 (gần như đen) hoặc ≥ 0,433 (quá sáng để\n * làm chữ). Hai ràng buộc không giao nhau — không có hex nào thoả cả hai.\n *\n * Nên tông `warning` nói bằng thứ KHÔNG PHẢI MÀU: `hazard-stripes` (khai ở styles.css) khoét\n * những rãnh chéo xuyên phần đã chạy để nền rãnh lộ ra. Ranh giới vạch ⇄ rãnh chính là cặp\n * \"đã chạy ⇄ rãnh\" đã đo ở bảng trên (4,54:1 light · 6,47:1 dark ✓), nên vạch KHÔNG thêm màu nào\n * và không cần đo thêm. Đây đúng là WCAG 1.4.1: tín hiệu không được chỉ nằm ở màu — bản vạch còn\n * đọc ra ở chế độ xám, chỗ mà mọi khác biệt sắc độ đều biến mất.\n *\n * ⚠ Vì vậy: KHÔNG được \"đơn giản hoá\" `warning` xuống còn mỗi `bg-warning-ink`. Bỏ vạch đi là\n * API đổi tông mà không đổi được gì ở ba trên bốn bảng màu — chỗ gọi tưởng đã cảnh báo, người\n * dùng không thấy gì. Cùng lý do đó, ĐỪNG thêm tông thứ năm nào nữa mà chưa đo lại bảng trên.\n *\n * Vạch chỉ là cách nói \"sắp chạm ngưỡng\" trên chính cái thanh; nó KHÔNG thay được một câu chữ.\n * Chỗ nào cần nói rõ ngưỡng là bao nhiêu và làm gì tiếp thì vẫn phải có `Alert`/`StatusPill` hoặc\n * một dòng `text-warning-ink` cạnh thanh.\n */\n\n/*\n * HAI `cva`, không phải hai bảng `Record`.\n *\n * Không phải chuyện phong cách: cổng màu (`scripts/check-contrast.mjs`) chỉ bóc chuỗi lớp từ ba\n * chỗ — thuộc tính `className`, `cva(base, { variants })`, và đối số TRỰC TIẾP của `cn()`. Class\n * nằm trong một `Record` rồi đi vào `cn()` qua biến là VÔ HÌNH với cổng: nó không báo lỗi, nó\n * lặng lẽ không đo — và miễn trừ của rãnh sẽ thành \"ruỗng\", đúng thứ cổng coi là nơi giấu lỗi.\n * Dạng `cva` cho cổng thấy cả base lẫn từng lá biến thể.\n */\nconst progressTrackVariants = cva(\"border-control-edge relative h-2 w-full overflow-hidden rounded-full border\", {\n variants: {\n tone: {\n default: \"bg-primary-ink/20\",\n success: \"bg-success-ink/20\",\n warning: \"bg-warning-ink/20\",\n destructive: \"bg-destructive-ink/20\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\nconst progressBarVariants = cva(\"h-full w-full flex-1 transition-transform duration-150 ease-out\", {\n variants: {\n tone: {\n default: \"bg-primary-ink\",\n success: \"bg-success-ink\",\n // `hazard-stripes` là PHẦN BẮT BUỘC của tông này, không phải trang trí — xem chú thích đầu file.\n warning: \"bg-warning-ink hazard-stripes\",\n destructive: \"bg-destructive-ink\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\n/** Tông của thanh — xem chú thích ở trên về việc vì sao `warning` phải kèm vạch chéo. */\nexport type ProgressTone = NonNullable<VariantProps<typeof progressBarVariants>[\"tone\"]>;\n\nexport interface ProgressProps\n extends React.ComponentProps<typeof ProgressPrimitive.Root>,\n VariantProps<typeof progressBarVariants> {}\n\n/** Mặc định của Radix — `max` không hợp lệ thì Radix cũng rơi về đúng số này cho `aria-*`. */\nconst DEFAULT_MAX = 100;\n\n/**\n * Kẹp `value` vào `[0, max]` — giá trị DUY NHẤT mà cả thanh lẫn Radix (`aria-*`) đọc (xem chú thích\n * đầu file). Không phải số hữu hạn (`null`, `undefined`, `NaN`) thì giữ nguyên: Radix coi là\n * indeterminate và thanh đứng ở 0.\n */\nfunction clampValue(value: number | null | undefined, max: number): number | null | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return value;\n return Math.min(Math.max(value, 0), max);\n}\n\nfunction Progress({ className, value, max, tone = \"default\", ...props }: ProgressProps) {\n const validMax = typeof max === \"number\" && max > 0 ? max : DEFAULT_MAX;\n const clamped = clampValue(value, validMax);\n const percent = typeof clamped === \"number\" && Number.isFinite(clamped) ? (clamped / validMax) * 100 : 0;\n\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n data-tone={tone}\n className={cn(progressTrackVariants({ tone }), className)}\n value={clamped}\n max={max}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={progressBarVariants({ tone })}\n style={{ transform: `translateX(-${100 - percent}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { Progress, progressBarVariants, progressTrackVariants };\n"],"mappings":";AAkKM;AAhKN,YAAY,uBAAuB;AACnC,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAkGnB,MAAM,wBAAwB,IAAI,+EAA+E;AAAA,EAC/G,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AAED,MAAM,sBAAsB,IAAI,mEAAmE;AAAA,EACjG,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA;AAAA,MAET,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AAUD,MAAM,cAAc;AAOpB,SAAS,WAAW,OAAkC,KAAwC;AAC5F,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACjE,SAAO,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,GAAG;AACzC;AAEA,SAAS,SAAS,EAAE,WAAW,OAAO,KAAK,OAAO,WAAW,GAAG,MAAM,GAAkB;AACtF,QAAM,WAAW,OAAO,QAAQ,YAAY,MAAM,IAAI,MAAM;AAC5D,QAAM,UAAU,WAAW,OAAO,QAAQ;AAC1C,QAAM,UAAU,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,IAAK,UAAU,WAAY,MAAM;AAEvG,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX,WAAW,GAAG,sBAAsB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MACxD,OAAO;AAAA,MACP;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,kBAAkB;AAAA,QAAlB;AAAA,UACC,aAAU;AAAA,UACV,WAAW,oBAAoB,EAAE,KAAK,CAAC;AAAA,UACvC,OAAO,EAAE,WAAW,eAAe,MAAM,OAAO,KAAK;AAAA;AAAA,MACvD;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Progress (Radix) — thanh tiến trình xác định (`value` 0…`max`, `max` mặc định 100).\n * Bỏ trống `value` để về trạng thái indeterminate của Radix (thanh đứng yên ở 0);\n * animation cho indeterminate là việc của app.\n *\n * ─ THANH VẼ THEO `value / max`, VÀ THANH VỚI `aria-*` NÓI CÙNG MỘT CON SỐ ─\n * Radix đọc `max` để dựng `aria-valuemax` và `aria-valuetext` (`round(value / max · 100)%`). Trước\n * `3.3.3` phần đã chạy lại vẽ bằng `100 - value` — bỏ qua `max` —, nên `max={30} value={24}` vẽ 24%\n * trong khi trình đọc màn hình đọc 80%: hai người nhìn cùng một control nhận hai con số.\n *\n * Nay cả hai đi từ MỘT giá trị: `value` được KẸP vào `[0, max]` rồi mới giao cho Radix (`max` không\n * phải số dương → 100, đúng luật Radix). Kẹp chứ không coi là indeterminate, vì ca vượt ngưỡng hay\n * gặp nhất là CÓ Ý: hạn mức dùng 120% phải là thanh ĐẦY, không phải thanh rỗng. Trước `3.3.3` ca đó\n * tình cờ đã đầy (`translateX(--20%)` là CSS vô hiệu) nhưng Radix coi `value` sai → không có\n * `aria-valuetext`, kèm lỗi console; nay thanh vẫn đầy VÀ trình đọc màn hình đọc \"100%\". Nói rõ là\n * đã vượt (bao nhiêu, làm gì tiếp) vẫn là việc của chữ cạnh thanh và `tone`, không của thanh.\n *\n * ─ ⛔ ĐỪNG ĐỔI TÔNG `default` SANG MÀU ĐẬM HƠN (chốt 2026-11-01) ─\n * 3.13.0 đổi thanh sang `gold-600` / rãnh `gold-100` để qua tương phản ở bảng mặc định; trên giao diện thật thanh nâu\n * sẫm nặng hơn mọi thứ quanh nó và bị trả về đây ở 3.13.1. Mức 1,73:1 (thanh ⇄ nền) / 1,55:1 (thanh ⇄ rãnh) ở bảng\n * MẶC ĐỊNH là ĐÃ CHẤP NHẬN — bỏ qua phép đo tương phản cho phần tô này. Cần rõ hơn thì chữ cạnh thanh, `tone`, hoặc\n * bảng tương phản cao (`data-contrast=\"high\"`, nơi các số đo bên dưới đạt). Xem CLAUDE.md, mục màu.\n *\n * ─ MÀU: `--primary-ink`, KHÔNG PHẢI `--primary` ─\n * Phần đã chạy là thông tin thị giác (WCAG 1.4.11, 3:1) và phải nổi trên CẢ nền trang lẫn phần\n * rãnh còn lại. `bg-primary` gold #E8B824 ở light chỉ **1,86:1** với nền trắng và **1,64:1** với\n * rãnh `bg-primary/20` — thanh 8px vàng nhạt trên nền trắng gần như không thấy. Bậc ink của\n * brand đạt cả hai phía ở cả hai theme:\n *\n * đã chạy ⇄ nền trang : 6,51:1 (light) · 12,67:1 (dark) ✓\n * đã chạy ⇄ rãnh `/20` : 4,85:1 (light) · 7,97:1 (dark) ✓\n * (tệ nhất khi rãnh nằm trên --muted: 4,54:1 · 6,52:1 ✓)\n *\n * ─ RÃNH: TINT `/20` GIỮ NGUYÊN, NHƯNG PHẢI CÓ VIỀN `--control-edge` ─\n * Bản trước chỉ có tint: rãnh ⇄ nền trang mới **1,34:1** (light) và **1,59–1,66:1** (dark), tức\n * KHÔNG nhìn thấy mốc 100% — ở `value` nhỏ không đọc được TỈ LỆ, ở `value=0` cả control biến mất.\n * Cách sửa hiển nhiên (đổi rãnh sang `bg-control-edge` như Slider/Switch) lại hỏng chỗ khác:\n * `--primary-ink` ⇄ `--control-edge` chỉ **1,70:1** (light) / **2,57:1** (dark), mà Progress\n * KHÔNG có núm nên ranh giới đã-chạy/chưa-chạy là thứ DUY NHẤT mang giá trị — mất nó là mất\n * thông tin. Với nền trang trắng không tồn tại cặp màu nào vừa ≥3:1 với nhau vừa ≥3:1 với nền\n * (chứng minh trong chú thích của `Slider`), nên hai ràng buộc phải tách ra hai lớp:\n *\n * khoanh vùng control → `border-control-edge` ⇄ nền trang : 3,83:1 (light) · 4,94:1 (dark) ✓\n * ⇄ --muted : 3,58:1 · 4,21:1 ✓\n * đọc tỉ lệ → đã chạy ⇄ rãnh `/20` : 4,85:1 · 7,97:1 ✓\n *\n * Đây đúng vai trò hợp đồng đặt cho `--control-edge` (styles.css §RANH GIỚI): \"khi nền control\n * trong suốt/nhạt thì VIỀN là thứ duy nhất khoanh vùng\".\n *\n * ─ TÔNG: BỐN GIÁ TRỊ, VÀ `warning` KHÔNG ĐƯỢC PHÉP CHỈ LÀ MỘT MÀU KHÁC ─\n * `tone` đổi CẢ phần đã chạy lẫn rãnh sang cùng một bậc ink, nên tỉ lệ đọc-được giữ nguyên cấu\n * trúc của bản mặc định. Đo trên ba bề mặt (`--background`, `--card`, `--muted`), tệ nhất mỗi cột.\n *\n * ⚠ Cột \"light\"/\"dark\" dưới đây là **bảng TƯƠNG PHẢN CAO** — đúng bảng mà cổng màu đo, và lý do\n * ghi ở đầu `scripts/a11y/tailwind.mjs`. Ở bảng mặc định (bản duyệt comitor-ds) `--primary-ink`\n * chính là `--primary` gold #E8B824, nên tông `default` chỉ được 1,73:1 với nền và 1,55:1 với\n * rãnh: đó là con số của BẢN DUYỆT, không phải lỗi của component — đổi nó là đổi bảng màu.\n *\n * tông đã chạy ⇄ nền trang đã chạy ⇄ rãnh cùng tông /20\n * default 6,09:1 (light) 10,80 (dark) 4,54:1 (light) 6,52:1 (dark) ✓\n * success 6,09:1 9,25 4,55:1 5,88:1 ✓\n * warning 6,09:1 10,52 4,54:1 6,47:1 ✓\n * destructive 6,68:1 5,82 4,85:1 4,25:1 ✓\n *\n * ⚠ NHƯNG `warning` KHÔNG phân biệt được với `default` BẰNG MÀU, và đó là số đo chứ không phải\n * cảm tính. Đo `--warning-ink` ⇄ `--primary-ink` trên cả BỐN bảng màu của gói:\n *\n * light (mặc định) #7A5800 ⇄ #E8B824 3,51:1 ✓\n * light (tương phản cao) #7A5800 ⇄ #7A5800 1,00:1 ✗ CÙNG MỘT HEX\n * dark (mặc định) #F2C94C ⇄ #F5D060 1,06:1 ✗\n * dark (tương phản cao) #FACC15 ⇄ #F5D060 1,03:1 ✗\n *\n * Ba trên bốn bảng màu cho ra hai thanh trông y hệt nhau. Đây KHÔNG sửa được ở tầng token: brand\n * của Comitor là gold, warning là yellow — cùng một sắc. Muốn tách hai bậc ink ra 3:1 thì một\n * trong hai phải rời khỏi vùng đọc-được: với nền trắng, chữ đạt 4,5:1 đòi độ chói ≤ 0,183, mà\n * cách `--gold-500` (0,111) đúng 3:1 thì phải ≤ 0,004 (gần như đen) hoặc ≥ 0,433 (quá sáng để\n * làm chữ). Hai ràng buộc không giao nhau — không có hex nào thoả cả hai.\n *\n * Nên tông `warning` nói bằng thứ KHÔNG PHẢI MÀU: `hazard-stripes` (khai ở styles.css) khoét\n * những rãnh chéo xuyên phần đã chạy để nền rãnh lộ ra. Ranh giới vạch ⇄ rãnh chính là cặp\n * \"đã chạy ⇄ rãnh\" đã đo ở bảng trên (4,54:1 light · 6,47:1 dark ✓), nên vạch KHÔNG thêm màu nào\n * và không cần đo thêm. Đây đúng là WCAG 1.4.1: tín hiệu không được chỉ nằm ở màu — bản vạch còn\n * đọc ra ở chế độ xám, chỗ mà mọi khác biệt sắc độ đều biến mất.\n *\n * ⚠ Vì vậy: KHÔNG được \"đơn giản hoá\" `warning` xuống còn mỗi `bg-warning-ink`. Bỏ vạch đi là\n * API đổi tông mà không đổi được gì ở ba trên bốn bảng màu — chỗ gọi tưởng đã cảnh báo, người\n * dùng không thấy gì. Cùng lý do đó, ĐỪNG thêm tông thứ năm nào nữa mà chưa đo lại bảng trên.\n *\n * Vạch chỉ là cách nói \"sắp chạm ngưỡng\" trên chính cái thanh; nó KHÔNG thay được một câu chữ.\n * Chỗ nào cần nói rõ ngưỡng là bao nhiêu và làm gì tiếp thì vẫn phải có `Alert`/`StatusPill` hoặc\n * một dòng `text-warning-ink` cạnh thanh.\n */\n\n/*\n * HAI `cva`, không phải hai bảng `Record`.\n *\n * Không phải chuyện phong cách: cổng màu (`scripts/check-contrast.mjs`) chỉ bóc chuỗi lớp từ ba\n * chỗ — thuộc tính `className`, `cva(base, { variants })`, và đối số TRỰC TIẾP của `cn()`. Class\n * nằm trong một `Record` rồi đi vào `cn()` qua biến là VÔ HÌNH với cổng: nó không báo lỗi, nó\n * lặng lẽ không đo — và miễn trừ của rãnh sẽ thành \"ruỗng\", đúng thứ cổng coi là nơi giấu lỗi.\n * Dạng `cva` cho cổng thấy cả base lẫn từng lá biến thể.\n */\nconst progressTrackVariants = cva(\"border-control-edge relative h-2 w-full overflow-hidden rounded-full border\", {\n variants: {\n tone: {\n default: \"bg-primary-ink/20\",\n success: \"bg-success-ink/20\",\n warning: \"bg-warning-ink/20\",\n destructive: \"bg-destructive-ink/20\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\nconst progressBarVariants = cva(\"h-full w-full flex-1 transition-transform duration-150 ease-out\", {\n variants: {\n tone: {\n default: \"bg-primary-ink\",\n success: \"bg-success-ink\",\n // `hazard-stripes` là PHẦN BẮT BUỘC của tông này, không phải trang trí — xem chú thích đầu file.\n warning: \"bg-warning-ink hazard-stripes\",\n destructive: \"bg-destructive-ink\"\n }\n },\n defaultVariants: { tone: \"default\" }\n});\n\n/** Tông của thanh — xem chú thích ở trên về việc vì sao `warning` phải kèm vạch chéo. */\nexport type ProgressTone = NonNullable<VariantProps<typeof progressBarVariants>[\"tone\"]>;\n\nexport interface ProgressProps\n extends React.ComponentProps<typeof ProgressPrimitive.Root>,\n VariantProps<typeof progressBarVariants> {}\n\n/** Mặc định của Radix — `max` không hợp lệ thì Radix cũng rơi về đúng số này cho `aria-*`. */\nconst DEFAULT_MAX = 100;\n\n/**\n * Kẹp `value` vào `[0, max]` — giá trị DUY NHẤT mà cả thanh lẫn Radix (`aria-*`) đọc (xem chú thích\n * đầu file). Không phải số hữu hạn (`null`, `undefined`, `NaN`) thì giữ nguyên: Radix coi là\n * indeterminate và thanh đứng ở 0.\n */\nfunction clampValue(value: number | null | undefined, max: number): number | null | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return value;\n return Math.min(Math.max(value, 0), max);\n}\n\nfunction Progress({ className, value, max, tone = \"default\", ...props }: ProgressProps) {\n const validMax = typeof max === \"number\" && max > 0 ? max : DEFAULT_MAX;\n const clamped = clampValue(value, validMax);\n const percent = typeof clamped === \"number\" && Number.isFinite(clamped) ? (clamped / validMax) * 100 : 0;\n\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n data-tone={tone}\n className={cn(progressTrackVariants({ tone }), className)}\n value={clamped}\n max={max}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={progressBarVariants({ tone })}\n style={{ transform: `translateX(-${100 - percent}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { Progress, progressBarVariants, progressTrackVariants };\n"],"mappings":";AAwKM;AAtKN,YAAY,uBAAuB;AACnC,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAwGnB,MAAM,wBAAwB,IAAI,+EAA+E;AAAA,EAC/G,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AAED,MAAM,sBAAsB,IAAI,mEAAmE;AAAA,EACjG,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,SAAS;AAAA;AAAA,MAET,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AAUD,MAAM,cAAc;AAOpB,SAAS,WAAW,OAAkC,KAAwC;AAC5F,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACjE,SAAO,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,GAAG;AACzC;AAEA,SAAS,SAAS,EAAE,WAAW,OAAO,KAAK,OAAO,WAAW,GAAG,MAAM,GAAkB;AACtF,QAAM,WAAW,OAAO,QAAQ,YAAY,MAAM,IAAI,MAAM;AAC5D,QAAM,UAAU,WAAW,OAAO,QAAQ;AAC1C,QAAM,UAAU,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,IAAK,UAAU,WAAY,MAAM;AAEvG,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX,WAAW,GAAG,sBAAsB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MACxD,OAAO;AAAA,MACP;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,QAAC,kBAAkB;AAAA,QAAlB;AAAA,UACC,aAAU;AAAA,UACV,WAAW,oBAAoB,EAAE,KAAK,CAAC;AAAA,UACvC,OAAO,EAAE,WAAW,eAAe,MAAM,OAAO,KAAK;AAAA;AAAA,MACvD;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -23,20 +23,14 @@ declare function TableHeader({ className, ...props }: TableHeaderProps): React.J
|
|
|
23
23
|
declare function TableBody({ className, ...props }: TableBodyProps): React.JSX.Element;
|
|
24
24
|
declare function TableFooter({ className, ...props }: TableFooterProps): React.JSX.Element;
|
|
25
25
|
/**
|
|
26
|
-
* TableRow — dòng bảng. Trạng thái ĐÃ CHỌN (`data-state="selected"`)
|
|
26
|
+
* TableRow — dòng bảng. Trạng thái ĐÃ CHỌN (`data-state="selected"`) là nền `--accent` — đúng bậc tint
|
|
27
|
+
* TRẠNG THÁI mà DataGrid tô cho dòng đã chọn (`data-grid-row.tsx`), để hai bảng của gói nói cùng một thứ.
|
|
28
|
+
* Không có vạch viền: dấu mang thông tin là ô checkbox đã tích (DataTable `selectable`), tint chỉ để nhận
|
|
29
|
+
* ra dòng — 1,07:1 với `--card`, cố ý nhạt để chữ trong dòng vẫn đọc tốt.
|
|
27
30
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* tới mức thành mảng đặc (xem lý lẽ y hệt ở đầu `progress.tsx`).
|
|
32
|
-
* 2. vạch `border-b-app-accent-ink` — ĐÂY mới là dấu mang thông tin, và nó đạt ngưỡng:
|
|
33
|
-
* 6,51:1 với `--card`/`--background`, tệ nhất 6,09:1 khi dòng nằm trên `--muted` (light);
|
|
34
|
-
* 12,67:1 / 10,80:1 (dark). Dùng bậc `-ink` chứ không `--app-accent` trần: gold `#E8B824`
|
|
35
|
-
* chỉ 1,86:1 với nền trắng — đúng cái bẫy "token tô NỀN đem vẽ DẤU".
|
|
36
|
-
*
|
|
37
|
-
* Vạch nằm ở CẠNH DƯỚI chứ không phải cạnh trái vì `<tr>` cuộn ngang cùng bảng: mép trái
|
|
38
|
-
* trôi khỏi khung cuộn (và bị ô ghim `position:sticky` của `DataTable` đè lên), còn cạnh dưới
|
|
39
|
-
* trải hết bề ngang nên luôn còn thấy. Bề rộng viền không đổi theo trạng thái ⇒ không xô layout.
|
|
31
|
+
* Nền tô trên TỪNG Ô (`*:`) chứ không trên `<tr>`: sọc của `striped` khai ở cấp `<table>` với độ đặc tả
|
|
32
|
+
* (0,2,2) và thắng mọi class trên `<tr>`, nên tô ở `<tr>` thì dòng chọn ở hàng CHẴN không hiện gì. Ô ghim
|
|
33
|
+
* của `DataTable` (`bg-inherit`) cũng nhận nền đặc này, không trong mờ.
|
|
40
34
|
*/
|
|
41
35
|
declare function TableRow({ className, ...props }: TableRowProps): React.JSX.Element;
|
|
42
36
|
declare function TableHead({ className, ...props }: TableHeadProps): React.JSX.Element;
|
|
@@ -38,7 +38,7 @@ function TableRow({ className, ...props }) {
|
|
|
38
38
|
{
|
|
39
39
|
"data-slot": "table-row",
|
|
40
40
|
className: cn(
|
|
41
|
-
"border-b border-border transition-colors hover:bg-table-row-hover data-[state=selected]
|
|
41
|
+
"border-b border-border transition-colors hover:bg-table-row-hover data-[state=selected]:*:bg-accent",
|
|
42
42
|
className
|
|
43
43
|
),
|
|
44
44
|
...props
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/table.tsx"],"sourcesContent":["\"use client\";\n\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Table — bảng cơ bản, đã bọc sẵn khung cuộn ngang + viền bo `rounded-xl`.\n * `variant=\"striped\"` (hoặc `striped`) tô nền xen kẽ cho hàng chẵn.\n *\n * Đây là primitive tĩnh: sort / chọn dòng / ghim cột nằm ở composite `DataTable`.\n */\n\nexport type TableProps = React.ComponentProps<\"table\"> & {\n variant?: \"default\" | \"striped\";\n /** Alias tiện dụng của `variant=\"striped\"`. */\n striped?: boolean;\n};\nexport type TableHeaderProps = React.ComponentProps<\"thead\">;\nexport type TableBodyProps = React.ComponentProps<\"tbody\">;\nexport type TableFooterProps = React.ComponentProps<\"tfoot\">;\nexport type TableRowProps = React.ComponentProps<\"tr\">;\nexport type TableHeadProps = React.ComponentProps<\"th\">;\nexport type TableCellProps = React.ComponentProps<\"td\">;\nexport type TableCaptionProps = React.ComponentProps<\"caption\">;\n\nfunction Table({ className, variant = \"default\", striped, ...props }: TableProps) {\n const isStriped = striped || variant === \"striped\";\n return (\n <div data-slot=\"table-container\" className=\"w-full overflow-auto rounded-xl border border-border\">\n <table\n data-slot=\"table\"\n className={cn(\n \"w-full caption-bottom text-sm\",\n isStriped &&\n \"[&_tbody_tr:nth-child(even)]:bg-table-row-stripe [&_tbody_tr:nth-child(even):hover]:bg-table-row-hover\",\n className\n )}\n {...props}\n />\n </div>\n );\n}\n\nfunction TableHeader({ className, ...props }: TableHeaderProps) {\n return <thead data-slot=\"table-header\" className={cn(\"bg-muted/60 border-b border-border\", className)} {...props} />;\n}\n\nfunction TableBody({ className, ...props }: TableBodyProps) {\n return <tbody data-slot=\"table-body\" className={cn(\"[&_tr:last-child]:border-0\", className)} {...props} />;\n}\n\nfunction TableFooter({ className, ...props }: TableFooterProps) {\n return (\n <tfoot\n data-slot=\"table-footer\"\n className={cn(\"bg-muted/40 border-t border-border font-medium [&>tr]:last:border-b-0\", className)}\n {...props}\n />\n );\n}\n\n/**\n * TableRow — dòng bảng. Trạng thái ĐÃ CHỌN (`data-state=\"selected\"`)
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/table.tsx"],"sourcesContent":["\"use client\";\n\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Table — bảng cơ bản, đã bọc sẵn khung cuộn ngang + viền bo `rounded-xl`.\n * `variant=\"striped\"` (hoặc `striped`) tô nền xen kẽ cho hàng chẵn.\n *\n * Đây là primitive tĩnh: sort / chọn dòng / ghim cột nằm ở composite `DataTable`.\n */\n\nexport type TableProps = React.ComponentProps<\"table\"> & {\n variant?: \"default\" | \"striped\";\n /** Alias tiện dụng của `variant=\"striped\"`. */\n striped?: boolean;\n};\nexport type TableHeaderProps = React.ComponentProps<\"thead\">;\nexport type TableBodyProps = React.ComponentProps<\"tbody\">;\nexport type TableFooterProps = React.ComponentProps<\"tfoot\">;\nexport type TableRowProps = React.ComponentProps<\"tr\">;\nexport type TableHeadProps = React.ComponentProps<\"th\">;\nexport type TableCellProps = React.ComponentProps<\"td\">;\nexport type TableCaptionProps = React.ComponentProps<\"caption\">;\n\nfunction Table({ className, variant = \"default\", striped, ...props }: TableProps) {\n const isStriped = striped || variant === \"striped\";\n return (\n <div data-slot=\"table-container\" className=\"w-full overflow-auto rounded-xl border border-border\">\n <table\n data-slot=\"table\"\n className={cn(\n \"w-full caption-bottom text-sm\",\n isStriped &&\n \"[&_tbody_tr:nth-child(even)]:bg-table-row-stripe [&_tbody_tr:nth-child(even):hover]:bg-table-row-hover\",\n className\n )}\n {...props}\n />\n </div>\n );\n}\n\nfunction TableHeader({ className, ...props }: TableHeaderProps) {\n return <thead data-slot=\"table-header\" className={cn(\"bg-muted/60 border-b border-border\", className)} {...props} />;\n}\n\nfunction TableBody({ className, ...props }: TableBodyProps) {\n return <tbody data-slot=\"table-body\" className={cn(\"[&_tr:last-child]:border-0\", className)} {...props} />;\n}\n\nfunction TableFooter({ className, ...props }: TableFooterProps) {\n return (\n <tfoot\n data-slot=\"table-footer\"\n className={cn(\"bg-muted/40 border-t border-border font-medium [&>tr]:last:border-b-0\", className)}\n {...props}\n />\n );\n}\n\n/**\n * TableRow — dòng bảng. Trạng thái ĐÃ CHỌN (`data-state=\"selected\"`) là nền `--accent` — đúng bậc tint\n * TRẠNG THÁI mà DataGrid tô cho dòng đã chọn (`data-grid-row.tsx`), để hai bảng của gói nói cùng một thứ.\n * Không có vạch viền: dấu mang thông tin là ô checkbox đã tích (DataTable `selectable`), tint chỉ để nhận\n * ra dòng — 1,07:1 với `--card`, cố ý nhạt để chữ trong dòng vẫn đọc tốt.\n *\n * Nền tô trên TỪNG Ô (`*:`) chứ không trên `<tr>`: sọc của `striped` khai ở cấp `<table>` với độ đặc tả\n * (0,2,2) và thắng mọi class trên `<tr>`, nên tô ở `<tr>` thì dòng chọn ở hàng CHẴN không hiện gì. Ô ghim\n * của `DataTable` (`bg-inherit`) cũng nhận nền đặc này, không trong mờ.\n */\nfunction TableRow({ className, ...props }: TableRowProps) {\n return (\n <tr\n data-slot=\"table-row\"\n className={cn(\n \"border-b border-border transition-colors hover:bg-table-row-hover data-[state=selected]:*:bg-accent\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction TableHead({ className, ...props }: TableHeadProps) {\n return (\n <th\n data-slot=\"table-head\"\n className={cn(\n \"h-(--table-head-h) px-4 text-left align-middle text-xs font-bold tracking-[0.08em] uppercase text-muted-foreground whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction TableCell({ className, ...props }: TableCellProps) {\n return (\n <td\n data-slot=\"table-cell\"\n className={cn(\n \"px-4 py-(--row-py) align-middle text-foreground whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction TableCaption({ className, ...props }: TableCaptionProps) {\n return (\n <caption data-slot=\"table-caption\" className={cn(\"mt-4 text-sm text-muted-foreground\", className)} {...props} />\n );\n}\n\nexport { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };\n"],"mappings":";AA6BM;AA1BN,SAAS,UAAU;AAsBnB,SAAS,MAAM,EAAE,WAAW,UAAU,WAAW,SAAS,GAAG,MAAM,GAAe;AAChF,QAAM,YAAY,WAAW,YAAY;AACzC,SACE,oBAAC,SAAI,aAAU,mBAAkB,WAAU,wDACzC;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA,aACE;AAAA,QACF;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN,GACF;AAEJ;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SAAO,oBAAC,WAAM,aAAU,gBAAe,WAAW,GAAG,sCAAsC,SAAS,GAAI,GAAG,OAAO;AACpH;AAEA,SAAS,UAAU,EAAE,WAAW,GAAG,MAAM,GAAmB;AAC1D,SAAO,oBAAC,WAAM,aAAU,cAAa,WAAW,GAAG,8BAA8B,SAAS,GAAI,GAAG,OAAO;AAC1G;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,yEAAyE,SAAS;AAAA,MAC/F,GAAG;AAAA;AAAA,EACN;AAEJ;AAYA,SAAS,SAAS,EAAE,WAAW,GAAG,MAAM,GAAkB;AACxD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,UAAU,EAAE,WAAW,GAAG,MAAM,GAAmB;AAC1D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,UAAU,EAAE,WAAW,GAAG,MAAM,GAAmB;AAC1D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,aAAa,EAAE,WAAW,GAAG,MAAM,GAAsB;AAChE,SACE,oBAAC,aAAQ,aAAU,iBAAgB,WAAW,GAAG,sCAAsC,SAAS,GAAI,GAAG,OAAO;AAElH;","names":[]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const tabsListVariants = {
|
|
2
|
-
default: "
|
|
2
|
+
default: "shadow-[inset_0_-1px_0_var(--border)]",
|
|
3
3
|
pills: "bg-muted p-1 rounded-lg"
|
|
4
4
|
};
|
|
5
5
|
const tabsTriggerVariants = {
|
|
6
|
-
default: "px-4 py-2.5 text-sm font-medium border-b-2
|
|
6
|
+
default: "px-4 py-2.5 text-sm font-medium border-b-2 transition-colors duration-150 border-transparent text-muted-foreground hover:text-foreground data-[state=active]:border-primary-ink data-[state=active]:text-foreground",
|
|
7
7
|
pills: "px-3 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
|
8
8
|
};
|
|
9
9
|
const TABS_TRIGGER_BASE = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/tabs-variants.ts"],"sourcesContent":["/**\n * Bảng lớp của hai biến thể Tabs — module THUẦN, cố ý KHÔNG có `\"use client\"`.\n *\n * Vì sao tách khỏi `tabs.tsx`: hai nơi dùng chung nó — `Tabs` (Radix, có state, client) và\n * `RouteTabs` (điều hướng bằng link, không state). Để bảng ở file client thì `RouteTabs` phải\n * chép lại chuỗi lớp, và hai bản chép sẽ lệch nhau ngay lần đầu có người sửa một bên.\n *\n * Ranh giới RSC cắt theo FILE, nên hằng dữ liệu thuần phải sống ở module không directive —\n * xem CHANGELOG 0.2.2 §2 và cổng `verify:directives`.\n */\n\n/**\n * - `default`: gạch chân, vạch active màu brand — tab CẤP TRANG.\n * - `pills`: viên bo tròn trên nền muted — tab cấp panel/thẻ, và tab điều hướng của trang cài đặt.\n */\nexport type TabsVariant = \"default\" | \"pills\";\n\nexport const tabsListVariants: Record<TabsVariant, string> = {\n default: \"
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/tabs-variants.ts"],"sourcesContent":["/**\n * Bảng lớp của hai biến thể Tabs — module THUẦN, cố ý KHÔNG có `\"use client\"`.\n *\n * Vì sao tách khỏi `tabs.tsx`: hai nơi dùng chung nó — `Tabs` (Radix, có state, client) và\n * `RouteTabs` (điều hướng bằng link, không state). Để bảng ở file client thì `RouteTabs` phải\n * chép lại chuỗi lớp, và hai bản chép sẽ lệch nhau ngay lần đầu có người sửa một bên.\n *\n * Ranh giới RSC cắt theo FILE, nên hằng dữ liệu thuần phải sống ở module không directive —\n * xem CHANGELOG 0.2.2 §2 và cổng `verify:directives`.\n */\n\n/**\n * - `default`: gạch chân, vạch active màu brand — tab CẤP TRANG.\n * - `pills`: viên bo tròn trên nền muted — tab cấp panel/thẻ, và tab điều hướng của trang cài đặt.\n */\nexport type TabsVariant = \"default\" | \"pills\";\n\n/*\n * Vạch đáy của biến thể `default` là BÓNG INSET 1px, không phải `border-b`: `RouteTabs` có `overflow-x-auto` (cuộn\n * ngang khi hẹp), và `overflow` cắt mọi thứ ra ngoài khung đệm — viền nằm NGOÀI khung đệm, nên vạch active 2px kéo\n * xuống đè viền bằng `-mb-px` bị cắt mất 1px (chỉ còn 1px vàng). Bóng inset vẽ BÊN TRONG, dưới các tab; vạch 2px\n * của tab nằm trọn trong khung và che đúng chỗ đó.\n */\nexport const tabsListVariants: Record<TabsVariant, string> = {\n default: \"shadow-[inset_0_-1px_0_var(--border)]\",\n pills: \"bg-muted p-1 rounded-lg\"\n};\n\n/*\n * Vạch active của biến thể `default` là `border-primary-ink`, KHÔNG phải `border-gold-300`:\n * vạch 2px này là thứ DUY NHẤT mang màu để nói \"tab nào đang mở\" (chữ chỉ đổi từ\n * `muted-foreground` sang `foreground`), nên nó là thông tin thị giác. Đi qua token nên app đổi\n * brand là vạch đổi theo, thay vì đóng đinh vào một bậc của thang gold.\n */\nexport const tabsTriggerVariants: Record<TabsVariant, string> = {\n default:\n \"px-4 py-2.5 text-sm font-medium border-b-2 transition-colors duration-150 border-transparent text-muted-foreground hover:text-foreground data-[state=active]:border-primary-ink data-[state=active]:text-foreground\",\n pills:\n \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\"\n};\n\n/** Vòng focus + trạng thái vô hiệu, chung cho cả hai biến thể và cả hai kiểu tab. */\nexport const TABS_TRIGGER_BASE =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\";\n"],"mappings":"AAuBO,MAAM,mBAAgD;AAAA,EAC3D,SAAS;AAAA,EACT,OAAO;AACT;AAQO,MAAM,sBAAmD;AAAA,EAC9D,SACE;AAAA,EACF,OACE;AACJ;AAGO,MAAM,oBACX;","names":[]}
|
|
@@ -79,12 +79,12 @@ const timelineVariants = cva("relative flex [--timeline-connector-thickness:0.12
|
|
|
79
79
|
alternate: ""
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
|
+
/*
|
|
83
|
+
* ⚠ Khác thượng nguồn: chiều dọc KHÔNG có `gap` ở gốc. Thượng nguồn CỘNG `gap` ở đây với `pb` của từng mục —
|
|
84
|
+
* `default` 24 + 32 = 56px, `alternate` 12 + 48 = 60px giữa hai mục. Khoảng cách nay chỉ nằm ở `pb-6` của
|
|
85
|
+
* `TimelineItem`, để vạch nối (đo theo chiều cao mục) chạm đúng chấm kế tiếp.
|
|
86
|
+
*/
|
|
82
87
|
compoundVariants: [
|
|
83
|
-
{
|
|
84
|
-
orientation: "vertical",
|
|
85
|
-
variant: "default",
|
|
86
|
-
class: "gap-6"
|
|
87
|
-
},
|
|
88
88
|
{
|
|
89
89
|
orientation: "horizontal",
|
|
90
90
|
variant: "default",
|
|
@@ -93,7 +93,7 @@ const timelineVariants = cva("relative flex [--timeline-connector-thickness:0.12
|
|
|
93
93
|
{
|
|
94
94
|
orientation: "vertical",
|
|
95
95
|
variant: "alternate",
|
|
96
|
-
class: "relative w-full
|
|
96
|
+
class: "relative w-full"
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
orientation: "horizontal",
|
|
@@ -223,7 +223,7 @@ const timelineItemVariants = cva("relative flex", {
|
|
|
223
223
|
{
|
|
224
224
|
orientation: "vertical",
|
|
225
225
|
variant: "default",
|
|
226
|
-
class: "gap-3 pb-
|
|
226
|
+
class: "gap-3 pb-6 last:pb-0"
|
|
227
227
|
},
|
|
228
228
|
{
|
|
229
229
|
orientation: "horizontal",
|
|
@@ -234,13 +234,13 @@ const timelineItemVariants = cva("relative flex", {
|
|
|
234
234
|
orientation: "vertical",
|
|
235
235
|
variant: "alternate",
|
|
236
236
|
isAlternateRight: false,
|
|
237
|
-
class: "w-1/2 gap-3 pr-6 pb-
|
|
237
|
+
class: "w-1/2 gap-3 pr-6 pb-6 last:pb-0"
|
|
238
238
|
},
|
|
239
239
|
{
|
|
240
240
|
orientation: "vertical",
|
|
241
241
|
variant: "alternate",
|
|
242
242
|
isAlternateRight: true,
|
|
243
|
-
class: "ml-auto w-1/2 flex-row-reverse gap-3 pb-
|
|
243
|
+
class: "ml-auto w-1/2 flex-row-reverse gap-3 pb-6 pl-6 last:pb-0"
|
|
244
244
|
},
|
|
245
245
|
{
|
|
246
246
|
orientation: "horizontal",
|
|
@@ -391,18 +391,23 @@ const timelineDotVariants = cva(
|
|
|
391
391
|
false: ""
|
|
392
392
|
}
|
|
393
393
|
},
|
|
394
|
+
/*
|
|
395
|
+
* ⚠ Khác thượng nguồn: chấm dọc `alternate` lùi đúng NỬA CHẤM ra ngoài mép mục, để tâm chấm nằm trên mép —
|
|
396
|
+
* nơi vạch nối (`-right/-left: thickness/2`) đặt TÂM của nó. Thượng nguồn trừ thêm nửa bề dày vạch, nên tâm
|
|
397
|
+
* chấm lệch 1px vào trong mục: mục trái lệch trái, mục phải lệch phải — chấm đi zíc zắc quanh vạch.
|
|
398
|
+
*/
|
|
394
399
|
compoundVariants: [
|
|
395
400
|
{
|
|
396
401
|
variant: "alternate",
|
|
397
402
|
orientation: "vertical",
|
|
398
403
|
isAlternateRight: false,
|
|
399
|
-
class: "absolute -right-[calc(var(--timeline-dot-size)/2
|
|
404
|
+
class: "absolute -right-[calc(var(--timeline-dot-size)/2)] bg-background"
|
|
400
405
|
},
|
|
401
406
|
{
|
|
402
407
|
variant: "alternate",
|
|
403
408
|
orientation: "vertical",
|
|
404
409
|
isAlternateRight: true,
|
|
405
|
-
class: "absolute -left-[calc(var(--timeline-dot-size)/2
|
|
410
|
+
class: "absolute -left-[calc(var(--timeline-dot-size)/2)] bg-background"
|
|
406
411
|
},
|
|
407
412
|
{
|
|
408
413
|
variant: "alternate",
|
|
@@ -475,7 +480,7 @@ const timelineConnectorVariants = cva("absolute z-0", {
|
|
|
475
480
|
{
|
|
476
481
|
orientation: "vertical",
|
|
477
482
|
variant: "default",
|
|
478
|
-
class: "start-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] top-
|
|
483
|
+
class: "start-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] top-[var(--timeline-dot-size)] h-[calc(100%-var(--timeline-dot-size))] w-[var(--timeline-connector-thickness)]"
|
|
479
484
|
},
|
|
480
485
|
{
|
|
481
486
|
orientation: "horizontal",
|
|
@@ -486,13 +491,13 @@ const timelineConnectorVariants = cva("absolute z-0", {
|
|
|
486
491
|
orientation: "vertical",
|
|
487
492
|
variant: "alternate",
|
|
488
493
|
isAlternateRight: false,
|
|
489
|
-
class: "top-
|
|
494
|
+
class: "top-[var(--timeline-dot-size)] -right-[calc(var(--timeline-connector-thickness)/2)] h-[calc(100%-var(--timeline-dot-size))] w-[var(--timeline-connector-thickness)]"
|
|
490
495
|
},
|
|
491
496
|
{
|
|
492
497
|
orientation: "vertical",
|
|
493
498
|
variant: "alternate",
|
|
494
499
|
isAlternateRight: true,
|
|
495
|
-
class: "top-
|
|
500
|
+
class: "top-[var(--timeline-dot-size)] -left-[calc(var(--timeline-connector-thickness)/2)] h-[calc(100%-var(--timeline-dot-size))] w-[var(--timeline-connector-thickness)]"
|
|
496
501
|
},
|
|
497
502
|
{
|
|
498
503
|
orientation: "horizontal",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/ui/timeline.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/timeline) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { collectItemElements } from \"../../lib/collect-item-elements.js\";\nimport { useComposedRefs } from \"../../lib/compose-refs.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"vertical\" | \"horizontal\";\ntype Variant = \"default\" | \"alternate\";\ntype Status = \"completed\" | \"active\" | \"pending\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype ItemElement = React.ComponentRef<typeof TimelineItem>;\n\nconst ROOT_NAME = \"Timeline\";\nconst ITEM_NAME = \"TimelineItem\";\nconst DOT_NAME = \"TimelineDot\";\nconst CONNECTOR_NAME = \"TimelineConnector\";\nconst CONTENT_NAME = \"TimelineContent\";\n\nfunction getItemStatus(itemIndex: number, activeIndex?: number): Status {\n /*\n * ⚠ Khác thượng nguồn: mục đăng ký chỉ số của mình trong layout effect, nên ở lượt render phía\n * server (và lượt đầu trước hydrate) MỌI mục đều mang chỉ số -1. Bản gốc so thẳng `-1 < activeIndex`\n * → mọi chấm cùng ra \"completed\" (tô gold) cho tới khi hydrate xong — một trạng thái SAI, không\n * chỉ là thiếu. Chưa biết vị trí thì coi là \"pending\": trung tính, và đúng với phần lớn mục.\n * Từ 3.1.0 các lượt đó đọc vị trí từ cây phần tử (`getItemsFromChildren`); -1 chỉ còn khi đọc\n * không được — mục nằm trong component của app — và nhánh này vẫn giữ nó ở \"pending\".\n */\n if (activeIndex === undefined || itemIndex < 0) return \"pending\";\n if (itemIndex < activeIndex) return \"completed\";\n if (itemIndex === activeIndex) return \"active\";\n return \"pending\";\n}\n\nfunction getSortedEntries(entries: [string, React.RefObject<ItemElement | null>][]) {\n return entries.sort((a, b) => {\n const elementA = a[1].current;\n const elementB = b[1].current;\n if (!elementA || !elementB) return 0;\n const position = elementA.compareDocumentPosition(elementB);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;\n if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;\n return 0;\n });\n}\n\n/*\n * ⚠ Khác thượng nguồn (3.1.0): vị trí các mục ĐỌC TỪ CÂY PHẦN TỬ của `Timeline`, theo thứ tự tài\n * liệu — `TimelineItem` viết trong gốc qua mảng, fragment, thẻ HTML (luật ở `collectItemElements`).\n * Mục không có `value`, nên vị trí khoá bằng CHÍNH object props của phần tử: React đưa đúng object\n * đó cho `TimelineItem` khi render nó — ở server cũng như ở trình duyệt. Mục do component của app\n * render thì không có trong bảng, nên không có vị trí (-1). `WeakMap` để không giữ props của lượt\n * đầu sống mãi.\n */\ninterface InitialItems {\n indexes: WeakMap<object, number>;\n count: number;\n}\n\nfunction getItemsFromChildren(children: React.ReactNode): InitialItems {\n const indexes = new WeakMap<object, number>();\n const items = collectItemElements(children, { item: TimelineItem }) ?? [];\n for (const [index, { props }] of items.entries()) {\n // Cùng MỘT phần tử đặt hai lần thì hai mục chung một object props — không phân biệt được.\n if (indexes.has(props)) return { indexes: new WeakMap(), count: 0 };\n indexes.set(props, index);\n }\n return { indexes, count: items.length };\n}\n\n/*\n * ⚠ Khác thượng nguồn (3.1.0): `initialSelector` là giá trị đọc từ cây phần tử (xem `Timeline`).\n * Nó là snapshot SERVER — React dùng ở server và ở lượt hydrate, nên hai bên ra cùng một HTML — và\n * là snapshot client cho tới layout effect đầu của gốc. Bản gốc dùng `selector` cho mọi lượt.\n */\nfunction useStore<T>(selector: (store: Store) => T, initialSelector: (store: Store) => T): T {\n const store = React.useContext(StoreContext);\n if (!store) {\n throw new Error(`\\`useStore\\` phải được gọi bên trong \\`${ROOT_NAME}\\``);\n }\n\n const getSnapshot = React.useCallback(\n () => (store.hasRegistered() ? selector(store) : initialSelector(store)),\n [store, selector, initialSelector]\n );\n const getServerSnapshot = React.useCallback(() => initialSelector(store), [store, initialSelector]);\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);\n}\n\ninterface StoreState {\n items: Map<string, React.RefObject<ItemElement | null>>;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n notify: () => void;\n onItemRegister: (id: string, ref: React.RefObject<ItemElement | null>) => void;\n onItemUnregister: (id: string) => void;\n getNextItemStatus: (id: string, activeIndex?: number) => Status | undefined;\n getItemIndex: (id: string) => number;\n hasRegistered: () => boolean;\n getInitialItemIndex: (itemProps: object) => number;\n getInitialNextItemStatus: (initialIndex: number, activeIndex?: number) => Status | undefined;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface TimelineContextValue {\n dir: Direction;\n orientation: Orientation;\n variant: Variant;\n activeIndex?: number;\n}\n\nconst TimelineContext = React.createContext<TimelineContextValue | null>(null);\n\nfunction useTimelineContext(consumerName: string) {\n const context = React.useContext(TimelineContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\nconst timelineVariants = cva(\"relative flex [--timeline-connector-thickness:0.125rem] [--timeline-dot-size:0.875rem]\", {\n variants: {\n orientation: {\n vertical: \"flex-col\",\n horizontal: \"flex-row items-start\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n }\n },\n compoundVariants: [\n {\n orientation: \"vertical\",\n variant: \"default\",\n class: \"gap-6\"\n },\n {\n orientation: \"horizontal\",\n variant: \"default\",\n class: \"gap-8\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n class: \"relative w-full gap-3\"\n },\n {\n orientation: \"horizontal\",\n variant: \"alternate\",\n class: \"items-center gap-4\"\n }\n ],\n defaultVariants: {\n orientation: \"vertical\",\n variant: \"default\"\n }\n});\n\ninterface TimelineProps extends DivProps {\n dir?: Direction;\n orientation?: Orientation;\n variant?: Variant;\n activeIndex?: number;\n}\n\nfunction Timeline(props: TimelineProps) {\n const {\n orientation = \"vertical\",\n variant = \"default\",\n dir: dirProp,\n activeIndex,\n asChild,\n className,\n ...rootProps\n } = props;\n\n const dir = useDirection(dirProp);\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef<StoreState>(() => ({\n items: new Map()\n }));\n /*\n * ⚠ Khác thượng nguồn (3.1.0): vị trí cho mọi lượt render TRƯỚC khi các mục đăng ký. Bản gốc chỉ\n * biết vị trí qua đăng ký trong layout effect, nên HTML server — và khung hình đầu, cả lúc hydrate\n * lẫn lúc mount thuần client — có mọi mục \"pending\", KHÔNG vạch nối nào, và ở `variant=\"alternate\"`\n * mọi mục dồn một bên; mount xong mới tự đúng, thành một khung nháy (kèm bố cục nhảy).\n * - Chụp MỘT lần, ở lượt render đầu, và không bao giờ sửa: nó là snapshot SERVER của `useStore`,\n * nên phải đúng là cái server đã render — kể cả cho phần hydrate muộn hơn gốc.\n * - Chỉ có hiệu lực tới layout effect đầu của gốc (`registeredRef`). Effect của con chạy trước của\n * cha, nên lúc đó mọi mục đọc được đã đăng ký, và thứ tự DOM của chúng chính là thứ tự đọc được —\n * đổi nguồn không đổi gì trên màn hình. Từ đó thứ tự DOM của các mục đã đăng ký thắng như cũ.\n * - Đọc không được (mục nằm trong component của app, hay cây viết thẳng trong Server Component) thì\n * không mục nào có vị trí — y như trước bản này (xem `getItemStatus`).\n */\n const initialItemsRef = useLazyRef(() => getItemsFromChildren(props.children));\n const registeredRef = React.useRef(false);\n\n const store = React.useMemo<Store>(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n onItemRegister: (id: string, ref: React.RefObject<ItemElement | null>) => {\n stateRef.current.items.set(id, ref);\n store.notify();\n },\n onItemUnregister: (id: string) => {\n stateRef.current.items.delete(id);\n store.notify();\n },\n getNextItemStatus: (id: string, activeIndex?: number) => {\n const entries = Array.from(stateRef.current.items.entries());\n const sortedEntries = getSortedEntries(entries);\n\n const currentIndex = sortedEntries.findIndex(([key]) => key === id);\n if (currentIndex === -1 || currentIndex === sortedEntries.length - 1) {\n return undefined;\n }\n\n const nextItemIndex = currentIndex + 1;\n return getItemStatus(nextItemIndex, activeIndex);\n },\n getItemIndex: (id: string) => {\n const entries = Array.from(stateRef.current.items.entries());\n const sortedEntries = getSortedEntries(entries);\n return sortedEntries.findIndex(([key]) => key === id);\n },\n hasRegistered: () => registeredRef.current,\n getInitialItemIndex: (itemProps: object) => initialItemsRef.current.indexes.get(itemProps) ?? -1,\n // Cùng luật với `getNextItemStatus`, trên vị trí đọc từ cây.\n getInitialNextItemStatus: (initialIndex: number, activeIndex?: number) => {\n if (initialIndex === -1 || initialIndex === initialItemsRef.current.count - 1) {\n return undefined;\n }\n return getItemStatus(initialIndex + 1, activeIndex);\n }\n };\n }, [listenersRef, stateRef, initialItemsRef]);\n\n // ⚠ Khác thượng nguồn (3.1.0): từ đây vị trí ĐĂNG KÝ là nguồn sự thật — xem `initialItemsRef`.\n useIsomorphicLayoutEffect(() => {\n registeredRef.current = true;\n store.notify();\n }, [store]);\n\n const contextValue = React.useMemo<TimelineContextValue>(\n () => ({\n dir,\n orientation,\n variant,\n activeIndex\n }),\n [dir, orientation, variant, activeIndex]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <StoreContext.Provider value={store}>\n <TimelineContext.Provider value={contextValue}>\n {/*\n * ⚠ Khác thượng nguồn: bỏ `aria-orientation` khỏi thẻ gốc. `role=\"list\"` không nhận thuộc\n * tính đó theo ARIA 1.2 (axe báo `aria-allowed-attr`); hướng vẫn có ở `data-orientation`.\n */}\n <RootPrimitive\n role=\"list\"\n data-slot=\"timeline\"\n data-orientation={orientation}\n data-variant={variant}\n dir={dir}\n {...rootProps}\n className={cn(timelineVariants({ orientation, variant, className }))}\n />\n </TimelineContext.Provider>\n </StoreContext.Provider>\n );\n}\n\ninterface TimelineItemContextValue {\n id: string;\n status: Status;\n isAlternateRight: boolean;\n /** Vị trí đọc từ cây phần tử (-1 nếu không đọc được) — cho snapshot server của `TimelineConnector`. */\n initialIndex: number;\n}\n\nconst TimelineItemContext = React.createContext<TimelineItemContextValue | null>(null);\n\nfunction useTimelineItemContext(consumerName: string) {\n const context = React.useContext(TimelineItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ITEM_NAME}\\``);\n }\n return context;\n}\n\nconst timelineItemVariants = cva(\"relative flex\", {\n variants: {\n orientation: {\n vertical: \"\",\n horizontal: \"\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n },\n isAlternateRight: {\n true: \"\",\n false: \"\"\n }\n },\n compoundVariants: [\n {\n orientation: \"vertical\",\n variant: \"default\",\n class: \"gap-3 pb-8 last:pb-0\"\n },\n {\n orientation: \"horizontal\",\n variant: \"default\",\n class: \"flex-col gap-3\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n isAlternateRight: false,\n class: \"w-1/2 gap-3 pr-6 pb-12 last:pb-0\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n isAlternateRight: true,\n class: \"ml-auto w-1/2 flex-row-reverse gap-3 pb-12 pl-6 last:pb-0\"\n },\n {\n orientation: \"horizontal\",\n variant: \"alternate\",\n class: \"grid min-w-0 grid-rows-[1fr_auto_1fr] gap-3\"\n }\n ],\n defaultVariants: {\n orientation: \"vertical\",\n variant: \"default\",\n isAlternateRight: false\n }\n});\n\nfunction TimelineItem(props: DivProps) {\n const { asChild, className, id, ref, ...itemProps } = props;\n\n const { dir, orientation, variant, activeIndex } = useTimelineContext(ITEM_NAME);\n const store = useStoreContext(ITEM_NAME);\n\n const instanceId = React.useId();\n const itemId = id ?? instanceId;\n const itemRef = React.useRef<ItemElement | null>(null);\n const composedRef = useComposedRefs(ref, itemRef);\n\n // ⚠ Khác thượng nguồn (3.1.0): trước khi đăng ký, vị trí là vị trí đọc từ cây phần tử của gốc —\n // tra bằng chính object `props` này (xem `getItemsFromChildren`).\n const initialIndex = store.getInitialItemIndex(props);\n const itemIndex = useStore(\n (state) => state.getItemIndex(itemId),\n () => initialIndex\n );\n\n const status = React.useMemo<Status>(() => {\n return getItemStatus(itemIndex, activeIndex);\n }, [activeIndex, itemIndex]);\n\n // ⚠ Khác thượng nguồn: phụ thuộc là `itemId` (thứ thật sự được dùng) thay cho `id` — cùng\n // nghĩa, vì `itemId = id ?? instanceId` và `instanceId` không đổi suốt vòng đời.\n useIsomorphicLayoutEffect(() => {\n store.onItemRegister(itemId, itemRef);\n return () => {\n store.onItemUnregister(itemId);\n };\n }, [itemId, store]);\n\n const isAlternateRight = variant === \"alternate\" && itemIndex % 2 === 1;\n\n const itemContextValue = React.useMemo<TimelineItemContextValue>(\n () => ({ id: itemId, status, isAlternateRight, initialIndex }),\n [itemId, status, isAlternateRight, initialIndex]\n );\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n <TimelineItemContext.Provider value={itemContextValue}>\n <ItemPrimitive\n role=\"listitem\"\n aria-current={status === \"active\" ? \"step\" : undefined}\n data-slot=\"timeline-item\"\n data-status={status}\n data-orientation={orientation}\n data-alternate-right={isAlternateRight ? \"\" : undefined}\n id={itemId}\n dir={dir}\n {...itemProps}\n ref={composedRef}\n className={cn(\n timelineItemVariants({\n orientation,\n variant,\n isAlternateRight,\n className\n })\n )}\n />\n </TimelineItemContext.Provider>\n );\n}\n\nconst timelineContentVariants = cva(\"flex-1\", {\n variants: {\n orientation: {\n vertical: \"\",\n horizontal: \"\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n },\n isAlternateRight: {\n true: \"\",\n false: \"\"\n }\n },\n compoundVariants: [\n {\n variant: \"alternate\",\n orientation: \"vertical\",\n isAlternateRight: false,\n class: \"text-right\"\n },\n {\n variant: \"alternate\",\n orientation: \"horizontal\",\n isAlternateRight: false,\n class: \"row-start-3 pt-2\"\n },\n {\n variant: \"alternate\",\n orientation: \"horizontal\",\n isAlternateRight: true,\n class: \"row-start-1 pb-2\"\n }\n ],\n defaultVariants: {\n orientation: \"vertical\",\n variant: \"default\",\n isAlternateRight: false\n }\n});\n\nfunction TimelineContent(props: DivProps) {\n const { asChild, className, ...contentProps } = props;\n\n const { variant, orientation } = useTimelineContext(CONTENT_NAME);\n const { status, isAlternateRight } = useTimelineItemContext(CONTENT_NAME);\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ContentPrimitive\n data-slot=\"timeline-content\"\n data-status={status}\n {...contentProps}\n className={cn(\n timelineContentVariants({\n orientation,\n variant,\n isAlternateRight,\n className\n })\n )}\n />\n );\n}\n\n/*\n * ⚠ Khác thượng nguồn — màu chấm đi qua token VAI TRÒ (docs/hop-dong-mau.md §1):\n * chấm là thành phần PHI VĂN BẢN mang nghĩa (đánh dấu từng mốc và trạng thái của nó) → viền chịu\n * 3:1 với nền trang.\n * - đã xong / đang diễn ra: `border-primary-ink` thay `border-primary` (token TÔ NỀN, ~1,86:1 với\n * nền trang; bậc ink đạt 6,51:1 light · 12,67:1 dark ở bảng tương phản cao).\n * - chờ: `border-muted-foreground` thay `border-border` (bậc TRANG TRÍ 1,16:1, và cổng bắt viền trang\n * trí khi phần tử có bo góc + cỡ cố định). Cùng lựa chọn với tone `neutral` của `StatusPill`\n * (`STATUS_TONES.neutral.borderColor`): 5,23:1 light · 5,70:1 dark.\n */\nconst timelineDotVariants = cva(\n \"relative z-10 flex size-[var(--timeline-dot-size)] shrink-0 items-center justify-center rounded-full border-2 bg-background\",\n {\n variants: {\n status: {\n completed: \"border-primary-ink\",\n active: \"border-primary-ink\",\n pending: \"border-muted-foreground\"\n },\n orientation: {\n vertical: \"\",\n horizontal: \"\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n },\n isAlternateRight: {\n true: \"\",\n false: \"\"\n }\n },\n compoundVariants: [\n {\n variant: \"alternate\",\n orientation: \"vertical\",\n isAlternateRight: false,\n class: \"absolute -right-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] bg-background\"\n },\n {\n variant: \"alternate\",\n orientation: \"vertical\",\n isAlternateRight: true,\n class: \"absolute -left-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] bg-background\"\n },\n {\n variant: \"alternate\",\n orientation: \"horizontal\",\n class: \"row-start-2 bg-background\"\n },\n {\n variant: \"alternate\",\n status: \"completed\",\n class: \"bg-background\"\n },\n {\n variant: \"alternate\",\n status: \"active\",\n class: \"bg-background\"\n }\n ],\n defaultVariants: {\n status: \"pending\",\n orientation: \"vertical\",\n variant: \"default\",\n isAlternateRight: false\n }\n }\n);\n\nfunction TimelineDot(props: DivProps) {\n const { asChild, className, ...dotProps } = props;\n\n const { orientation, variant } = useTimelineContext(DOT_NAME);\n const { status, isAlternateRight } = useTimelineItemContext(DOT_NAME);\n\n const DotPrimitive = asChild ? Slot : \"div\";\n\n return (\n <DotPrimitive\n data-slot=\"timeline-dot\"\n data-status={status}\n data-orientation={orientation}\n {...dotProps}\n className={cn(\n timelineDotVariants({\n status,\n orientation,\n variant,\n isAlternateRight,\n className\n })\n )}\n />\n );\n}\n\n/*\n * ⚠ Khác thượng nguồn: đoạn nối ĐÃ XONG mang nghĩa tiến độ → `bg-primary-ink` thay `bg-primary`\n * (token tô nền, ~1,86:1 với nền trang). Đoạn chưa tới giữ `bg-border` — đường kẻ trang trí.\n */\nconst timelineConnectorVariants = cva(\"absolute z-0\", {\n variants: {\n isCompleted: {\n true: \"bg-primary-ink\",\n false: \"bg-border\"\n },\n orientation: {\n vertical: \"\",\n horizontal: \"\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n },\n isAlternateRight: {\n true: \"\",\n false: \"\"\n }\n },\n compoundVariants: [\n {\n orientation: \"vertical\",\n variant: \"default\",\n class:\n \"start-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] top-3 h-[calc(100%+0.5rem)] w-[var(--timeline-connector-thickness)]\"\n },\n {\n orientation: \"horizontal\",\n variant: \"default\",\n class:\n \"start-3 top-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] h-[var(--timeline-connector-thickness)] w-[calc(100%+0.5rem)]\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n isAlternateRight: false,\n class: \"top-2 -right-[calc(var(--timeline-connector-thickness)/2)] h-full w-[var(--timeline-connector-thickness)]\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n isAlternateRight: true,\n class: \"top-2 -left-[calc(var(--timeline-connector-thickness)/2)] h-full w-[var(--timeline-connector-thickness)]\"\n },\n {\n orientation: \"horizontal\",\n variant: \"alternate\",\n class:\n \"top-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] left-3 row-start-2 h-[var(--timeline-connector-thickness)] w-[calc(100%+0.5rem)]\"\n }\n ],\n defaultVariants: {\n isCompleted: false,\n orientation: \"vertical\",\n variant: \"default\",\n isAlternateRight: false\n }\n});\n\ninterface TimelineConnectorProps extends DivProps {\n forceMount?: boolean;\n}\n\nfunction TimelineConnector(props: TimelineConnectorProps) {\n const { asChild, forceMount, className, ...connectorProps } = props;\n\n const { orientation, variant, activeIndex } = useTimelineContext(CONNECTOR_NAME);\n const { id, status, isAlternateRight, initialIndex } = useTimelineItemContext(CONNECTOR_NAME);\n\n const nextItemStatus = useStore(\n (state) => state.getNextItemStatus(id, activeIndex),\n (state) => state.getInitialNextItemStatus(initialIndex, activeIndex)\n );\n\n const isLastItem = nextItemStatus === undefined;\n\n if (!forceMount && isLastItem) return null;\n\n const isConnectorCompleted = nextItemStatus === \"completed\" || nextItemStatus === \"active\";\n\n const ConnectorPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ConnectorPrimitive\n aria-hidden=\"true\"\n data-slot=\"timeline-connector\"\n data-completed={isConnectorCompleted ? \"\" : undefined}\n data-status={status}\n data-orientation={orientation}\n {...connectorProps}\n className={cn(\n timelineConnectorVariants({\n isCompleted: isConnectorCompleted,\n orientation,\n variant,\n isAlternateRight,\n className\n })\n )}\n />\n );\n}\n\nfunction TimelineHeader(props: DivProps) {\n const { asChild, className, ...headerProps } = props;\n\n const HeaderPrimitive = asChild ? Slot : \"div\";\n\n return (\n <HeaderPrimitive data-slot=\"timeline-header\" {...headerProps} className={cn(\"flex flex-col gap-1\", className)} />\n );\n}\n\nfunction TimelineTitle(props: DivProps) {\n const { asChild, className, ...titleProps } = props;\n\n const TitlePrimitive = asChild ? Slot : \"div\";\n\n return (\n <TitlePrimitive\n data-slot=\"timeline-title\"\n {...titleProps}\n className={cn(\"leading-none font-semibold\", className)}\n />\n );\n}\n\nfunction TimelineDescription(props: DivProps) {\n const { asChild, className, ...descriptionProps } = props;\n\n const DescriptionPrimitive = asChild ? Slot : \"div\";\n\n return (\n <DescriptionPrimitive\n data-slot=\"timeline-description\"\n {...descriptionProps}\n className={cn(\"text-sm text-muted-foreground\", className)}\n />\n );\n}\n\ninterface TimelineTimeProps extends React.ComponentProps<\"time\"> {\n asChild?: boolean;\n}\n\n/** Mốc thời gian — gói KHÔNG tự định dạng ngày: app truyền chuỗi đã định dạng + `dateTime` máy đọc. */\nfunction TimelineTime(props: TimelineTimeProps) {\n const { asChild, className, ...timeProps } = props;\n\n const TimePrimitive = asChild ? Slot : \"time\";\n\n return (\n <TimePrimitive\n data-slot=\"timeline-time\"\n {...timeProps}\n className={cn(\"text-xs text-muted-foreground\", className)}\n />\n );\n}\n\nexport {\n Timeline,\n TimelineConnector,\n type TimelineConnectorProps,\n TimelineContent,\n TimelineDescription,\n TimelineDot,\n TimelineHeader,\n TimelineItem,\n type TimelineProps,\n TimelineTime,\n type TimelineTimeProps,\n TimelineTitle\n};\n"],"mappings":"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","names":["activeIndex"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/ui/timeline.tsx"],"sourcesContent":["\"use client\";\n\n/*\n * Port từ Dice UI (https://diceui.com/docs/components/timeline) — MIT, © 2024 Sadman Sakib.\n * Giữ nguyên cấu trúc thượng nguồn để còn đối chiếu ngược được; mọi chỗ đổi đều có chú thích.\n */\n\nimport { useDirection } from \"@radix-ui/react-direction\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { collectItemElements } from \"../../lib/collect-item-elements.js\";\nimport { useComposedRefs } from \"../../lib/compose-refs.js\";\nimport { useIsomorphicLayoutEffect } from \"../../lib/use-isomorphic-layout-effect.js\";\nimport { useLazyRef } from \"../../lib/use-lazy-ref.js\";\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"vertical\" | \"horizontal\";\ntype Variant = \"default\" | \"alternate\";\ntype Status = \"completed\" | \"active\" | \"pending\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\ntype ItemElement = React.ComponentRef<typeof TimelineItem>;\n\nconst ROOT_NAME = \"Timeline\";\nconst ITEM_NAME = \"TimelineItem\";\nconst DOT_NAME = \"TimelineDot\";\nconst CONNECTOR_NAME = \"TimelineConnector\";\nconst CONTENT_NAME = \"TimelineContent\";\n\nfunction getItemStatus(itemIndex: number, activeIndex?: number): Status {\n /*\n * ⚠ Khác thượng nguồn: mục đăng ký chỉ số của mình trong layout effect, nên ở lượt render phía\n * server (và lượt đầu trước hydrate) MỌI mục đều mang chỉ số -1. Bản gốc so thẳng `-1 < activeIndex`\n * → mọi chấm cùng ra \"completed\" (tô gold) cho tới khi hydrate xong — một trạng thái SAI, không\n * chỉ là thiếu. Chưa biết vị trí thì coi là \"pending\": trung tính, và đúng với phần lớn mục.\n * Từ 3.1.0 các lượt đó đọc vị trí từ cây phần tử (`getItemsFromChildren`); -1 chỉ còn khi đọc\n * không được — mục nằm trong component của app — và nhánh này vẫn giữ nó ở \"pending\".\n */\n if (activeIndex === undefined || itemIndex < 0) return \"pending\";\n if (itemIndex < activeIndex) return \"completed\";\n if (itemIndex === activeIndex) return \"active\";\n return \"pending\";\n}\n\nfunction getSortedEntries(entries: [string, React.RefObject<ItemElement | null>][]) {\n return entries.sort((a, b) => {\n const elementA = a[1].current;\n const elementB = b[1].current;\n if (!elementA || !elementB) return 0;\n const position = elementA.compareDocumentPosition(elementB);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;\n if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;\n return 0;\n });\n}\n\n/*\n * ⚠ Khác thượng nguồn (3.1.0): vị trí các mục ĐỌC TỪ CÂY PHẦN TỬ của `Timeline`, theo thứ tự tài\n * liệu — `TimelineItem` viết trong gốc qua mảng, fragment, thẻ HTML (luật ở `collectItemElements`).\n * Mục không có `value`, nên vị trí khoá bằng CHÍNH object props của phần tử: React đưa đúng object\n * đó cho `TimelineItem` khi render nó — ở server cũng như ở trình duyệt. Mục do component của app\n * render thì không có trong bảng, nên không có vị trí (-1). `WeakMap` để không giữ props của lượt\n * đầu sống mãi.\n */\ninterface InitialItems {\n indexes: WeakMap<object, number>;\n count: number;\n}\n\nfunction getItemsFromChildren(children: React.ReactNode): InitialItems {\n const indexes = new WeakMap<object, number>();\n const items = collectItemElements(children, { item: TimelineItem }) ?? [];\n for (const [index, { props }] of items.entries()) {\n // Cùng MỘT phần tử đặt hai lần thì hai mục chung một object props — không phân biệt được.\n if (indexes.has(props)) return { indexes: new WeakMap(), count: 0 };\n indexes.set(props, index);\n }\n return { indexes, count: items.length };\n}\n\n/*\n * ⚠ Khác thượng nguồn (3.1.0): `initialSelector` là giá trị đọc từ cây phần tử (xem `Timeline`).\n * Nó là snapshot SERVER — React dùng ở server và ở lượt hydrate, nên hai bên ra cùng một HTML — và\n * là snapshot client cho tới layout effect đầu của gốc. Bản gốc dùng `selector` cho mọi lượt.\n */\nfunction useStore<T>(selector: (store: Store) => T, initialSelector: (store: Store) => T): T {\n const store = React.useContext(StoreContext);\n if (!store) {\n throw new Error(`\\`useStore\\` phải được gọi bên trong \\`${ROOT_NAME}\\``);\n }\n\n const getSnapshot = React.useCallback(\n () => (store.hasRegistered() ? selector(store) : initialSelector(store)),\n [store, selector, initialSelector]\n );\n const getServerSnapshot = React.useCallback(() => initialSelector(store), [store, initialSelector]);\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);\n}\n\ninterface StoreState {\n items: Map<string, React.RefObject<ItemElement | null>>;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n notify: () => void;\n onItemRegister: (id: string, ref: React.RefObject<ItemElement | null>) => void;\n onItemUnregister: (id: string) => void;\n getNextItemStatus: (id: string, activeIndex?: number) => Status | undefined;\n getItemIndex: (id: string) => number;\n hasRegistered: () => boolean;\n getInitialItemIndex: (itemProps: object) => number;\n getInitialNextItemStatus: (initialIndex: number, activeIndex?: number) => Status | undefined;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface TimelineContextValue {\n dir: Direction;\n orientation: Orientation;\n variant: Variant;\n activeIndex?: number;\n}\n\nconst TimelineContext = React.createContext<TimelineContextValue | null>(null);\n\nfunction useTimelineContext(consumerName: string) {\n const context = React.useContext(TimelineContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\nconst timelineVariants = cva(\"relative flex [--timeline-connector-thickness:0.125rem] [--timeline-dot-size:0.875rem]\", {\n variants: {\n orientation: {\n vertical: \"flex-col\",\n horizontal: \"flex-row items-start\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n }\n },\n /*\n * ⚠ Khác thượng nguồn: chiều dọc KHÔNG có `gap` ở gốc. Thượng nguồn CỘNG `gap` ở đây với `pb` của từng mục —\n * `default` 24 + 32 = 56px, `alternate` 12 + 48 = 60px giữa hai mục. Khoảng cách nay chỉ nằm ở `pb-6` của\n * `TimelineItem`, để vạch nối (đo theo chiều cao mục) chạm đúng chấm kế tiếp.\n */\n compoundVariants: [\n {\n orientation: \"horizontal\",\n variant: \"default\",\n class: \"gap-8\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n class: \"relative w-full\"\n },\n {\n orientation: \"horizontal\",\n variant: \"alternate\",\n class: \"items-center gap-4\"\n }\n ],\n defaultVariants: {\n orientation: \"vertical\",\n variant: \"default\"\n }\n});\n\ninterface TimelineProps extends DivProps {\n dir?: Direction;\n orientation?: Orientation;\n variant?: Variant;\n activeIndex?: number;\n}\n\nfunction Timeline(props: TimelineProps) {\n const {\n orientation = \"vertical\",\n variant = \"default\",\n dir: dirProp,\n activeIndex,\n asChild,\n className,\n ...rootProps\n } = props;\n\n const dir = useDirection(dirProp);\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef<StoreState>(() => ({\n items: new Map()\n }));\n /*\n * ⚠ Khác thượng nguồn (3.1.0): vị trí cho mọi lượt render TRƯỚC khi các mục đăng ký. Bản gốc chỉ\n * biết vị trí qua đăng ký trong layout effect, nên HTML server — và khung hình đầu, cả lúc hydrate\n * lẫn lúc mount thuần client — có mọi mục \"pending\", KHÔNG vạch nối nào, và ở `variant=\"alternate\"`\n * mọi mục dồn một bên; mount xong mới tự đúng, thành một khung nháy (kèm bố cục nhảy).\n * - Chụp MỘT lần, ở lượt render đầu, và không bao giờ sửa: nó là snapshot SERVER của `useStore`,\n * nên phải đúng là cái server đã render — kể cả cho phần hydrate muộn hơn gốc.\n * - Chỉ có hiệu lực tới layout effect đầu của gốc (`registeredRef`). Effect của con chạy trước của\n * cha, nên lúc đó mọi mục đọc được đã đăng ký, và thứ tự DOM của chúng chính là thứ tự đọc được —\n * đổi nguồn không đổi gì trên màn hình. Từ đó thứ tự DOM của các mục đã đăng ký thắng như cũ.\n * - Đọc không được (mục nằm trong component của app, hay cây viết thẳng trong Server Component) thì\n * không mục nào có vị trí — y như trước bản này (xem `getItemStatus`).\n */\n const initialItemsRef = useLazyRef(() => getItemsFromChildren(props.children));\n const registeredRef = React.useRef(false);\n\n const store = React.useMemo<Store>(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n onItemRegister: (id: string, ref: React.RefObject<ItemElement | null>) => {\n stateRef.current.items.set(id, ref);\n store.notify();\n },\n onItemUnregister: (id: string) => {\n stateRef.current.items.delete(id);\n store.notify();\n },\n getNextItemStatus: (id: string, activeIndex?: number) => {\n const entries = Array.from(stateRef.current.items.entries());\n const sortedEntries = getSortedEntries(entries);\n\n const currentIndex = sortedEntries.findIndex(([key]) => key === id);\n if (currentIndex === -1 || currentIndex === sortedEntries.length - 1) {\n return undefined;\n }\n\n const nextItemIndex = currentIndex + 1;\n return getItemStatus(nextItemIndex, activeIndex);\n },\n getItemIndex: (id: string) => {\n const entries = Array.from(stateRef.current.items.entries());\n const sortedEntries = getSortedEntries(entries);\n return sortedEntries.findIndex(([key]) => key === id);\n },\n hasRegistered: () => registeredRef.current,\n getInitialItemIndex: (itemProps: object) => initialItemsRef.current.indexes.get(itemProps) ?? -1,\n // Cùng luật với `getNextItemStatus`, trên vị trí đọc từ cây.\n getInitialNextItemStatus: (initialIndex: number, activeIndex?: number) => {\n if (initialIndex === -1 || initialIndex === initialItemsRef.current.count - 1) {\n return undefined;\n }\n return getItemStatus(initialIndex + 1, activeIndex);\n }\n };\n }, [listenersRef, stateRef, initialItemsRef]);\n\n // ⚠ Khác thượng nguồn (3.1.0): từ đây vị trí ĐĂNG KÝ là nguồn sự thật — xem `initialItemsRef`.\n useIsomorphicLayoutEffect(() => {\n registeredRef.current = true;\n store.notify();\n }, [store]);\n\n const contextValue = React.useMemo<TimelineContextValue>(\n () => ({\n dir,\n orientation,\n variant,\n activeIndex\n }),\n [dir, orientation, variant, activeIndex]\n );\n\n const RootPrimitive = asChild ? Slot : \"div\";\n\n return (\n <StoreContext.Provider value={store}>\n <TimelineContext.Provider value={contextValue}>\n {/*\n * ⚠ Khác thượng nguồn: bỏ `aria-orientation` khỏi thẻ gốc. `role=\"list\"` không nhận thuộc\n * tính đó theo ARIA 1.2 (axe báo `aria-allowed-attr`); hướng vẫn có ở `data-orientation`.\n */}\n <RootPrimitive\n role=\"list\"\n data-slot=\"timeline\"\n data-orientation={orientation}\n data-variant={variant}\n dir={dir}\n {...rootProps}\n className={cn(timelineVariants({ orientation, variant, className }))}\n />\n </TimelineContext.Provider>\n </StoreContext.Provider>\n );\n}\n\ninterface TimelineItemContextValue {\n id: string;\n status: Status;\n isAlternateRight: boolean;\n /** Vị trí đọc từ cây phần tử (-1 nếu không đọc được) — cho snapshot server của `TimelineConnector`. */\n initialIndex: number;\n}\n\nconst TimelineItemContext = React.createContext<TimelineItemContextValue | null>(null);\n\nfunction useTimelineItemContext(consumerName: string) {\n const context = React.useContext(TimelineItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` phải nằm bên trong \\`${ITEM_NAME}\\``);\n }\n return context;\n}\n\nconst timelineItemVariants = cva(\"relative flex\", {\n variants: {\n orientation: {\n vertical: \"\",\n horizontal: \"\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n },\n isAlternateRight: {\n true: \"\",\n false: \"\"\n }\n },\n compoundVariants: [\n {\n orientation: \"vertical\",\n variant: \"default\",\n class: \"gap-3 pb-6 last:pb-0\"\n },\n {\n orientation: \"horizontal\",\n variant: \"default\",\n class: \"flex-col gap-3\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n isAlternateRight: false,\n class: \"w-1/2 gap-3 pr-6 pb-6 last:pb-0\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n isAlternateRight: true,\n class: \"ml-auto w-1/2 flex-row-reverse gap-3 pb-6 pl-6 last:pb-0\"\n },\n {\n orientation: \"horizontal\",\n variant: \"alternate\",\n class: \"grid min-w-0 grid-rows-[1fr_auto_1fr] gap-3\"\n }\n ],\n defaultVariants: {\n orientation: \"vertical\",\n variant: \"default\",\n isAlternateRight: false\n }\n});\n\nfunction TimelineItem(props: DivProps) {\n const { asChild, className, id, ref, ...itemProps } = props;\n\n const { dir, orientation, variant, activeIndex } = useTimelineContext(ITEM_NAME);\n const store = useStoreContext(ITEM_NAME);\n\n const instanceId = React.useId();\n const itemId = id ?? instanceId;\n const itemRef = React.useRef<ItemElement | null>(null);\n const composedRef = useComposedRefs(ref, itemRef);\n\n // ⚠ Khác thượng nguồn (3.1.0): trước khi đăng ký, vị trí là vị trí đọc từ cây phần tử của gốc —\n // tra bằng chính object `props` này (xem `getItemsFromChildren`).\n const initialIndex = store.getInitialItemIndex(props);\n const itemIndex = useStore(\n (state) => state.getItemIndex(itemId),\n () => initialIndex\n );\n\n const status = React.useMemo<Status>(() => {\n return getItemStatus(itemIndex, activeIndex);\n }, [activeIndex, itemIndex]);\n\n // ⚠ Khác thượng nguồn: phụ thuộc là `itemId` (thứ thật sự được dùng) thay cho `id` — cùng\n // nghĩa, vì `itemId = id ?? instanceId` và `instanceId` không đổi suốt vòng đời.\n useIsomorphicLayoutEffect(() => {\n store.onItemRegister(itemId, itemRef);\n return () => {\n store.onItemUnregister(itemId);\n };\n }, [itemId, store]);\n\n const isAlternateRight = variant === \"alternate\" && itemIndex % 2 === 1;\n\n const itemContextValue = React.useMemo<TimelineItemContextValue>(\n () => ({ id: itemId, status, isAlternateRight, initialIndex }),\n [itemId, status, isAlternateRight, initialIndex]\n );\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n <TimelineItemContext.Provider value={itemContextValue}>\n <ItemPrimitive\n role=\"listitem\"\n aria-current={status === \"active\" ? \"step\" : undefined}\n data-slot=\"timeline-item\"\n data-status={status}\n data-orientation={orientation}\n data-alternate-right={isAlternateRight ? \"\" : undefined}\n id={itemId}\n dir={dir}\n {...itemProps}\n ref={composedRef}\n className={cn(\n timelineItemVariants({\n orientation,\n variant,\n isAlternateRight,\n className\n })\n )}\n />\n </TimelineItemContext.Provider>\n );\n}\n\nconst timelineContentVariants = cva(\"flex-1\", {\n variants: {\n orientation: {\n vertical: \"\",\n horizontal: \"\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n },\n isAlternateRight: {\n true: \"\",\n false: \"\"\n }\n },\n compoundVariants: [\n {\n variant: \"alternate\",\n orientation: \"vertical\",\n isAlternateRight: false,\n class: \"text-right\"\n },\n {\n variant: \"alternate\",\n orientation: \"horizontal\",\n isAlternateRight: false,\n class: \"row-start-3 pt-2\"\n },\n {\n variant: \"alternate\",\n orientation: \"horizontal\",\n isAlternateRight: true,\n class: \"row-start-1 pb-2\"\n }\n ],\n defaultVariants: {\n orientation: \"vertical\",\n variant: \"default\",\n isAlternateRight: false\n }\n});\n\nfunction TimelineContent(props: DivProps) {\n const { asChild, className, ...contentProps } = props;\n\n const { variant, orientation } = useTimelineContext(CONTENT_NAME);\n const { status, isAlternateRight } = useTimelineItemContext(CONTENT_NAME);\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ContentPrimitive\n data-slot=\"timeline-content\"\n data-status={status}\n {...contentProps}\n className={cn(\n timelineContentVariants({\n orientation,\n variant,\n isAlternateRight,\n className\n })\n )}\n />\n );\n}\n\n/*\n * ⚠ Khác thượng nguồn — màu chấm đi qua token VAI TRÒ (docs/hop-dong-mau.md §1):\n * chấm là thành phần PHI VĂN BẢN mang nghĩa (đánh dấu từng mốc và trạng thái của nó) → viền chịu\n * 3:1 với nền trang.\n * - đã xong / đang diễn ra: `border-primary-ink` thay `border-primary` (token TÔ NỀN, ~1,86:1 với\n * nền trang; bậc ink đạt 6,51:1 light · 12,67:1 dark ở bảng tương phản cao).\n * - chờ: `border-muted-foreground` thay `border-border` (bậc TRANG TRÍ 1,16:1, và cổng bắt viền trang\n * trí khi phần tử có bo góc + cỡ cố định). Cùng lựa chọn với tone `neutral` của `StatusPill`\n * (`STATUS_TONES.neutral.borderColor`): 5,23:1 light · 5,70:1 dark.\n */\nconst timelineDotVariants = cva(\n \"relative z-10 flex size-[var(--timeline-dot-size)] shrink-0 items-center justify-center rounded-full border-2 bg-background\",\n {\n variants: {\n status: {\n completed: \"border-primary-ink\",\n active: \"border-primary-ink\",\n pending: \"border-muted-foreground\"\n },\n orientation: {\n vertical: \"\",\n horizontal: \"\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n },\n isAlternateRight: {\n true: \"\",\n false: \"\"\n }\n },\n /*\n * ⚠ Khác thượng nguồn: chấm dọc `alternate` lùi đúng NỬA CHẤM ra ngoài mép mục, để tâm chấm nằm trên mép —\n * nơi vạch nối (`-right/-left: thickness/2`) đặt TÂM của nó. Thượng nguồn trừ thêm nửa bề dày vạch, nên tâm\n * chấm lệch 1px vào trong mục: mục trái lệch trái, mục phải lệch phải — chấm đi zíc zắc quanh vạch.\n */\n compoundVariants: [\n {\n variant: \"alternate\",\n orientation: \"vertical\",\n isAlternateRight: false,\n class: \"absolute -right-[calc(var(--timeline-dot-size)/2)] bg-background\"\n },\n {\n variant: \"alternate\",\n orientation: \"vertical\",\n isAlternateRight: true,\n class: \"absolute -left-[calc(var(--timeline-dot-size)/2)] bg-background\"\n },\n {\n variant: \"alternate\",\n orientation: \"horizontal\",\n class: \"row-start-2 bg-background\"\n },\n {\n variant: \"alternate\",\n status: \"completed\",\n class: \"bg-background\"\n },\n {\n variant: \"alternate\",\n status: \"active\",\n class: \"bg-background\"\n }\n ],\n defaultVariants: {\n status: \"pending\",\n orientation: \"vertical\",\n variant: \"default\",\n isAlternateRight: false\n }\n }\n);\n\nfunction TimelineDot(props: DivProps) {\n const { asChild, className, ...dotProps } = props;\n\n const { orientation, variant } = useTimelineContext(DOT_NAME);\n const { status, isAlternateRight } = useTimelineItemContext(DOT_NAME);\n\n const DotPrimitive = asChild ? Slot : \"div\";\n\n return (\n <DotPrimitive\n data-slot=\"timeline-dot\"\n data-status={status}\n data-orientation={orientation}\n {...dotProps}\n className={cn(\n timelineDotVariants({\n status,\n orientation,\n variant,\n isAlternateRight,\n className\n })\n )}\n />\n );\n}\n\n/*\n * ⚠ Khác thượng nguồn: đoạn nối ĐÃ XONG mang nghĩa tiến độ → `bg-primary-ink` thay `bg-primary`\n * (token tô nền, ~1,86:1 với nền trang). Đoạn chưa tới giữ `bg-border` — đường kẻ trang trí.\n */\nconst timelineConnectorVariants = cva(\"absolute z-0\", {\n variants: {\n isCompleted: {\n true: \"bg-primary-ink\",\n false: \"bg-border\"\n },\n orientation: {\n vertical: \"\",\n horizontal: \"\"\n },\n variant: {\n default: \"\",\n alternate: \"\"\n },\n isAlternateRight: {\n true: \"\",\n false: \"\"\n }\n },\n compoundVariants: [\n {\n orientation: \"vertical\",\n variant: \"default\",\n class:\n \"start-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] top-[var(--timeline-dot-size)] h-[calc(100%-var(--timeline-dot-size))] w-[var(--timeline-connector-thickness)]\"\n },\n {\n orientation: \"horizontal\",\n variant: \"default\",\n class:\n \"start-3 top-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] h-[var(--timeline-connector-thickness)] w-[calc(100%+0.5rem)]\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n isAlternateRight: false,\n class:\n \"top-[var(--timeline-dot-size)] -right-[calc(var(--timeline-connector-thickness)/2)] h-[calc(100%-var(--timeline-dot-size))] w-[var(--timeline-connector-thickness)]\"\n },\n {\n orientation: \"vertical\",\n variant: \"alternate\",\n isAlternateRight: true,\n class:\n \"top-[var(--timeline-dot-size)] -left-[calc(var(--timeline-connector-thickness)/2)] h-[calc(100%-var(--timeline-dot-size))] w-[var(--timeline-connector-thickness)]\"\n },\n {\n orientation: \"horizontal\",\n variant: \"alternate\",\n class:\n \"top-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] left-3 row-start-2 h-[var(--timeline-connector-thickness)] w-[calc(100%+0.5rem)]\"\n }\n ],\n defaultVariants: {\n isCompleted: false,\n orientation: \"vertical\",\n variant: \"default\",\n isAlternateRight: false\n }\n});\n\ninterface TimelineConnectorProps extends DivProps {\n forceMount?: boolean;\n}\n\nfunction TimelineConnector(props: TimelineConnectorProps) {\n const { asChild, forceMount, className, ...connectorProps } = props;\n\n const { orientation, variant, activeIndex } = useTimelineContext(CONNECTOR_NAME);\n const { id, status, isAlternateRight, initialIndex } = useTimelineItemContext(CONNECTOR_NAME);\n\n const nextItemStatus = useStore(\n (state) => state.getNextItemStatus(id, activeIndex),\n (state) => state.getInitialNextItemStatus(initialIndex, activeIndex)\n );\n\n const isLastItem = nextItemStatus === undefined;\n\n if (!forceMount && isLastItem) return null;\n\n const isConnectorCompleted = nextItemStatus === \"completed\" || nextItemStatus === \"active\";\n\n const ConnectorPrimitive = asChild ? Slot : \"div\";\n\n return (\n <ConnectorPrimitive\n aria-hidden=\"true\"\n data-slot=\"timeline-connector\"\n data-completed={isConnectorCompleted ? \"\" : undefined}\n data-status={status}\n data-orientation={orientation}\n {...connectorProps}\n className={cn(\n timelineConnectorVariants({\n isCompleted: isConnectorCompleted,\n orientation,\n variant,\n isAlternateRight,\n className\n })\n )}\n />\n );\n}\n\nfunction TimelineHeader(props: DivProps) {\n const { asChild, className, ...headerProps } = props;\n\n const HeaderPrimitive = asChild ? Slot : \"div\";\n\n return (\n <HeaderPrimitive data-slot=\"timeline-header\" {...headerProps} className={cn(\"flex flex-col gap-1\", className)} />\n );\n}\n\nfunction TimelineTitle(props: DivProps) {\n const { asChild, className, ...titleProps } = props;\n\n const TitlePrimitive = asChild ? Slot : \"div\";\n\n return (\n <TitlePrimitive\n data-slot=\"timeline-title\"\n {...titleProps}\n className={cn(\"leading-none font-semibold\", className)}\n />\n );\n}\n\nfunction TimelineDescription(props: DivProps) {\n const { asChild, className, ...descriptionProps } = props;\n\n const DescriptionPrimitive = asChild ? Slot : \"div\";\n\n return (\n <DescriptionPrimitive\n data-slot=\"timeline-description\"\n {...descriptionProps}\n className={cn(\"text-sm text-muted-foreground\", className)}\n />\n );\n}\n\ninterface TimelineTimeProps extends React.ComponentProps<\"time\"> {\n asChild?: boolean;\n}\n\n/** Mốc thời gian — gói KHÔNG tự định dạng ngày: app truyền chuỗi đã định dạng + `dateTime` máy đọc. */\nfunction TimelineTime(props: TimelineTimeProps) {\n const { asChild, className, ...timeProps } = props;\n\n const TimePrimitive = asChild ? Slot : \"time\";\n\n return (\n <TimePrimitive\n data-slot=\"timeline-time\"\n {...timeProps}\n className={cn(\"text-xs text-muted-foreground\", className)}\n />\n );\n}\n\nexport {\n Timeline,\n TimelineConnector,\n type TimelineConnectorProps,\n TimelineContent,\n TimelineDescription,\n TimelineDot,\n TimelineHeader,\n TimelineItem,\n type TimelineProps,\n TimelineTime,\n type TimelineTimeProps,\n TimelineTitle\n};\n"],"mappings":"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","names":["activeIndex"]}
|