@comitor/ui 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -501,17 +501,19 @@ shadcn/ui style `new-york`, đã tùy biến token theo brand Comitor.
501
501
  | Hành động | `Button`, `ButtonGroup`, `Toggle`, `ToggleGroup`, `Kbd` |
502
502
  | Nhập liệu | `Input`, `InputGroup`, `InputOTP`, `Textarea`, `Label`, `Field`, `Checkbox`, `RadioGroup`, `Switch`, `Slider`, `Select`, `Calendar` |
503
503
  | Hiển thị | `Card`, `Badge`, `Avatar`, `Table`, `Separator`, `AspectRatio`, `Skeleton`, `Spinner`, `Progress`, `Empty`, `Item`, `Alert` |
504
+ | Lớp phủ | `Dialog`, `AlertDialog`, `Sheet`, `Drawer`, `Popover`, `HoverCard`, `Tooltip`, `DropdownMenu`, `ContextMenu`, `Menubar`, `Command` |
505
+ | Điều hướng | `Tabs`, `Breadcrumb`, `Pagination`, `NavigationMenu`, `Accordion`, `Collapsible` |
506
+ | Khác | `ScrollArea`, `Resizable`, `Carousel` |
504
507
 
505
508
  `Alert` có ba variant: `default` · `warning` · `destructive` — hai variant sau tô CHỮ bằng bậc `-ink`
506
509
  trên `bg-card`, không tô nền màu (bậc tô nền làm chữ chỉ được 1,59:1 ở light ✗).
507
510
 
508
- `Progress` có `tone`: `default` · `success` · `destructive`. **Cố ý không có `warning`**: ở bảng màu
509
- light `--warning-ink` và `--primary-ink` là cùng một giá trị (`#7A5800`), nên thanh "cảnh báo" sẽ
510
- trông y hệt thanh thường. Cần báo sắp-chạm-ngưỡng thì nói bằng `StatusPill`/`Badge` tông warning
511
- hoặc một dòng chữ cạnh thanh — xem chú thích đầu `progress.tsx`.
512
- | Lớp phủ | `Dialog`, `AlertDialog`, `Sheet`, `Drawer`, `Popover`, `HoverCard`, `Tooltip`, `DropdownMenu`, `ContextMenu`, `Menubar`, `Command` |
513
- | Điều hướng | `Tabs`, `Breadcrumb`, `Pagination`, `NavigationMenu`, `Accordion`, `Collapsible` |
514
- | Khác | `ScrollArea`, `Resizable`, `Carousel` |
511
+ `Progress` có `tone`: `default` · `success` · `warning` · `destructive`. Tông `warning` KHÔNG chỉ đổi
512
+ màu: nó kèm vạch chéo (`hazard-stripes`) khoét rãnh xuyên phần đã chạy. Bắt buộc phải thế, vì brand
513
+ của Comitor là gold còn warning là yellow — `--warning-ink` ⇄ `--primary-ink` chỉ 1,00:1 ở bảng tương
514
+ phản cao (light) và 1,0x:1 ở cả hai bảng dark, tức đổi màu KHÔNG đổi được gì. Vạch là cùng tín hiệu
515
+ ấy nói bằng hình nên nó còn đọc ra ở chế độ xám (WCAG 1.4.1). Số đo và cách chứng minh "không hex nào
516
+ tách được hai bậc ink này" ở chú thích đầu `progress.tsx`.
515
517
 
516
518
  > Hai file primitive **không** nằm ở entry chính: `form.tsx` → `@comitor/ui/form`;
517
519
  > `sonner.tsx` → `@comitor/ui/shell` (tên `SonnerToaster`). Xem §3.
@@ -14,11 +14,13 @@ const alertVariants = cva(
14
14
  * tiêu đề 6,51:1 (light) · 11,60:1 (dark) ✓
15
15
  * mô tả /90 5,17:1 (light) · 9,56:1 (dark) ✓
16
16
  *
17
- * ⚠ Ở bảng màu LIGHT, `--warning-ink` và `--primary-ink` là CÙNG một giá trị (#7A5800 —
18
- * `--yellow-ink` trỏ về `--gold-500`). Với `Alert` chuyện đó vô hại vì variant `default`
19
- * dùng `--card-foreground`, nên `warning` vẫn khác hẳn nó (2,90:1). Nhưng nó là lý do
20
- * `Progress` KHÔNG có tông `warning` — xem chú thích ở đó trước khi thêm `warning` cho một
21
- * component nào khác mà mặc định đang là brand.
17
+ * ⚠ Ở bảng tương phản cao (light), `--warning-ink` và `--primary-ink` là CÙNG một giá trị
18
+ * (#7A5800 — `--yellow-ink` trỏ về `--gold-500`), và ở cả hai bảng dark chúng chỉ cách nhau
19
+ * 1,0x:1. Với `Alert` chuyện đó vô hại vì variant `default` dùng `--card-foreground`, nên
20
+ * `warning` vẫn khác hẳn nó (2,90:1). Nhưng component nào có mặc định là BRAND thì tông
21
+ * `warning` phải mang thêm một tín hiệu KHÔNG PHẢI MÀU — `Progress` giải bằng vạch chéo
22
+ * `hazard-stripes`; đọc chú thích đầu `progress.tsx` trước khi thêm `warning` cho một
23
+ * component kiểu đó.
22
24
  */
23
25
  warning: "text-warning-ink bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-warning-ink/90",
24
26
  destructive: "text-destructive-ink bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-ink/90"
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/alert.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Variant `destructive` dùng bậc `--destructive-ink`, KHÔNG dùng `--destructive`.\n * `--destructive` là màu TÔ NỀN (nút xoá) và cố ý ghim một giá trị ở cả hai theme, nên làm CHỮ\n * trên `bg-card` chỉ được 3,53:1 ở dark ✗; phần mô tả còn bị hạ thêm bằng `/90` nên hỏng ở CẢ\n * hai theme (4,32:1 light · 3,08:1 dark ✗).\n * Sau khi đổi (đo trên `--card`): tiêu đề 7,15:1 (light) / 6,42:1 (dark); mô tả `/90` 5,78:1\n * (light) / 5,40:1 (dark) ✓ — bậc `/90` giữ lại vì nó là phân cấp thị giác, và ở bậc ink nó vẫn\n * còn dư biên so với AA 4,5:1.\n */\nconst alertVariants = cva(\n \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n /*\n * `warning` cùng khuôn với `destructive`, chỉ đổi bậc ink — và cũng vì lý do đó: `--warning`\n * là màu TÔ NỀN (#E8B824 ở light), làm chữ trên `bg-card` chỉ 1,59:1 ✗. Bậc ink đạt AA dư\n * biên ở cả hai theme (đo trên `--card`):\n * tiêu đề 6,51:1 (light) · 11,60:1 (dark) ✓\n * mô tả /90 5,17:1 (light) · 9,56:1 (dark) ✓\n *\n * ⚠ Ở bảng màu LIGHT, `--warning-ink` và `--primary-ink` là CÙNG một giá trị (#7A5800 —\n * `--yellow-ink` trỏ về `--gold-500`). Với `Alert` chuyện đó vô hại vì variant `default`\n * dùng `--card-foreground`, nên `warning` vẫn khác hẳn nó (2,90:1). Nhưng nó là lý do\n * `Progress` KHÔNG có tông `warning` — xem chú thích ở đó trước khi thêm `warning` cho một\n * component nào khác mà mặc định đang là brand.\n */\n warning: \"text-warning-ink bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-warning-ink/90\",\n destructive:\n \"text-destructive-ink bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-ink/90\"\n }\n },\n defaultVariants: {\n variant: \"default\"\n }\n }\n);\n\nexport interface AlertProps extends React.ComponentProps<\"div\">, VariantProps<typeof alertVariants> {}\nexport type AlertTitleProps = React.ComponentProps<\"div\">;\nexport type AlertDescriptionProps = React.ComponentProps<\"div\">;\n\nfunction Alert({ className, variant, ...props }: AlertProps) {\n return <div data-slot=\"alert\" role=\"alert\" className={cn(alertVariants({ variant }), className)} {...props} />;\n}\n\nfunction AlertTitle({ className, ...props }: AlertTitleProps) {\n return (\n <div\n data-slot=\"alert-title\"\n className={cn(\"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nfunction AlertDescription({ className, ...props }: AlertDescriptionProps) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn(\n \"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Alert, AlertDescription, AlertTitle, alertVariants };\n"],"mappings":"AAgDS;AAhDT,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAWnB,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAcT,SAAS;AAAA,QACT,aACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,IACX;AAAA,EACF;AACF;AAMA,SAAS,MAAM,EAAE,WAAW,SAAS,GAAG,MAAM,GAAe;AAC3D,SAAO,oBAAC,SAAI,aAAU,SAAQ,MAAK,SAAQ,WAAW,GAAG,cAAc,EAAE,QAAQ,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AAC9G;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,+DAA+D,SAAS;AAAA,MACrF,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/alert.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/*\n * Variant `destructive` dùng bậc `--destructive-ink`, KHÔNG dùng `--destructive`.\n * `--destructive` là màu TÔ NỀN (nút xoá) và cố ý ghim một giá trị ở cả hai theme, nên làm CHỮ\n * trên `bg-card` chỉ được 3,53:1 ở dark ✗; phần mô tả còn bị hạ thêm bằng `/90` nên hỏng ở CẢ\n * hai theme (4,32:1 light · 3,08:1 dark ✗).\n * Sau khi đổi (đo trên `--card`): tiêu đề 7,15:1 (light) / 6,42:1 (dark); mô tả `/90` 5,78:1\n * (light) / 5,40:1 (dark) ✓ — bậc `/90` giữ lại vì nó là phân cấp thị giác, và ở bậc ink nó vẫn\n * còn dư biên so với AA 4,5:1.\n */\nconst alertVariants = cva(\n \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n /*\n * `warning` cùng khuôn với `destructive`, chỉ đổi bậc ink — và cũng vì lý do đó: `--warning`\n * là màu TÔ NỀN (#E8B824 ở light), làm chữ trên `bg-card` chỉ 1,59:1 ✗. Bậc ink đạt AA dư\n * biên ở cả hai theme (đo trên `--card`):\n * tiêu đề 6,51:1 (light) · 11,60:1 (dark) ✓\n * mô tả /90 5,17:1 (light) · 9,56:1 (dark) ✓\n *\n * ⚠ Ở bảng tương phản cao (light), `--warning-ink` và `--primary-ink` là CÙNG một giá trị\n * (#7A5800 — `--yellow-ink` trỏ về `--gold-500`), và ở cả hai bảng dark chúng chỉ cách nhau\n * 1,0x:1. Với `Alert` chuyện đó vô hại vì variant `default` dùng `--card-foreground`, nên\n * `warning` vẫn khác hẳn nó (2,90:1). Nhưng component nào có mặc định là BRAND thì tông\n * `warning` phải mang thêm một tín hiệu KHÔNG PHẢI MÀU — `Progress` giải bằng vạch chéo\n * `hazard-stripes`; đọc chú thích đầu `progress.tsx` trước khi thêm `warning` cho một\n * component kiểu đó.\n */\n warning: \"text-warning-ink bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-warning-ink/90\",\n destructive:\n \"text-destructive-ink bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-ink/90\"\n }\n },\n defaultVariants: {\n variant: \"default\"\n }\n }\n);\n\nexport interface AlertProps extends React.ComponentProps<\"div\">, VariantProps<typeof alertVariants> {}\nexport type AlertTitleProps = React.ComponentProps<\"div\">;\nexport type AlertDescriptionProps = React.ComponentProps<\"div\">;\n\nfunction Alert({ className, variant, ...props }: AlertProps) {\n return <div data-slot=\"alert\" role=\"alert\" className={cn(alertVariants({ variant }), className)} {...props} />;\n}\n\nfunction AlertTitle({ className, ...props }: AlertTitleProps) {\n return (\n <div\n data-slot=\"alert-title\"\n className={cn(\"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nfunction AlertDescription({ className, ...props }: AlertDescriptionProps) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn(\n \"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Alert, AlertDescription, AlertTitle, alertVariants };\n"],"mappings":"AAkDS;AAlDT,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAWnB,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAgBT,SAAS;AAAA,QACT,aACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,IACX;AAAA,EACF;AACF;AAMA,SAAS,MAAM,EAAE,WAAW,SAAS,GAAG,MAAM,GAAe;AAC3D,SAAO,oBAAC,SAAI,aAAU,SAAQ,MAAK,SAAQ,WAAW,GAAG,cAAc,EAAE,QAAQ,CAAC,GAAG,SAAS,GAAI,GAAG,OAAO;AAC9G;AAEA,SAAS,WAAW,EAAE,WAAW,GAAG,MAAM,GAAoB;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,+DAA+D,SAAS;AAAA,MACrF,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,iBAAiB,EAAE,WAAW,GAAG,MAAM,GAA0B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;","names":[]}
@@ -34,34 +34,56 @@ import * as React from 'react';
34
34
  * Đây đúng vai trò hợp đồng đặt cho `--control-edge` (styles.css §RANH GIỚI): "khi nền control
35
35
  * trong suốt/nhạt thì VIỀN là thứ duy nhất khoanh vùng".
36
36
  *
37
- * ─ TÔNG: BA GIÁ TRỊ, VÀ KHÔNG CÓ `warning` ─
37
+ * ─ TÔNG: BỐN GIÁ TRỊ, VÀ `warning` KHÔNG ĐƯỢC PHÉP CHỈ LÀ MỘT MÀU KHÁC ─
38
38
  * `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
39
- * 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:
39
+ * 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.
40
+ *
41
+ * ⚠ 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
42
+ * ghi ở đầu `scripts/a11y/tailwind.mjs`. Ở bảng mặc định (bản duyệt comitor-ds) `--primary-ink`
43
+ * 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
44
+ * 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.
40
45
  *
41
46
  * tông đã chạy ⇄ nền trang đã chạy ⇄ rãnh cùng tông /20
42
47
  * default 6,09:1 (light) 10,80 (dark) 4,54:1 (light) 6,52:1 (dark) ✓
43
48
  * success 6,09:1 9,25 4,55:1 5,88:1 ✓
49
+ * warning 6,09:1 10,52 4,54:1 6,47:1 ✓
44
50
  * destructive 6,68:1 5,82 4,85:1 4,25:1 ✓
45
51
  *
46
- * ⚠ KHÔNG có `warning`, và đây là quyết định ĐO ĐƯỢC chứ không phải bỏ sót: ở bảng màu light
47
- * `--warning-ink` và `--primary-ink` là CÙNG một giá trị (#7A5800 — `--yellow-ink` trỏ về
48
- * `--gold-500`, xem styles.css:174), nên một thanh `tone="warning"` sẽ trông y hệt thanh mặc định.
49
- * Ở dark hai bậc cũng chỉ cách nhau một nấc vàng (#FACC15 ⇄ #F5D060). Một API đổi tông mà không
50
- * đổi được gì là thứ tệ hơn không có API: chỗ gọi tưởng đã cảnh báo, người dùng không thấy gì.
52
+ * ⚠ 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
53
+ * cảm tính. Đo `--warning-ink` ⇄ `--primary-ink` trên cả BỐN bảng màu của gói:
54
+ *
55
+ * light (mặc định) #7A5800 ⇄ #E8B824 3,51:1 ✓
56
+ * light (tương phản cao) #7A5800 ⇄ #7A5800 1,00:1 ✗ CÙNG MỘT HEX
57
+ * dark (mặc định) #F2C94C ⇄ #F5D060 1,06:1 ✗
58
+ * dark (tương phản cao) #FACC15 ⇄ #F5D060 1,03:1 ✗
59
+ *
60
+ * 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
61
+ * 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
62
+ * 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à
63
+ * 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 để
64
+ * làm chữ). Hai ràng buộc không giao nhau — không có hex nào thoả cả hai.
65
+ *
66
+ * Nên tông `warning` nói bằng thứ KHÔNG PHẢI MÀU: `hazard-stripes` (khai ở styles.css) khoét
67
+ * 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
68
+ * "đã 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
69
+ * 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
70
+ * đọc ra ở chế độ xám, chỗ mà mọi khác biệt sắc độ đều biến mất.
71
+ *
72
+ * ⚠ 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à
73
+ * 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
74
+ * 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.
51
75
  *
52
- * Cần báo "sắp chạm ngưỡng" thì nói bằng thứ KHÁC màu thanh — `StatusPill`/`Badge` tông warning,
53
- * hoặc một dòng chữ `text-warning-ink` cạnh thanh; cả hai đều tương phản tốt với nền trang vì
54
- * chúng không phải cạnh tranh với chính màu brand. Muốn có `tone="warning"` thật thì phải tách
55
- * `--warning-ink` khỏi `--gold-500` ở TẦNG TOKEN trước — đó là thay đổi bảng màu, không phải thay
56
- * đổi component, và nó đụng mọi bề mặt warning đang có (Badge, StatusPill, StatCard, Toast).
76
+ * 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ữ.
77
+ * 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
78
+ * một dòng `text-warning-ink` cạnh thanh.
57
79
  */
58
80
  declare const progressTrackVariants: (props?: ({
59
- tone?: "default" | "success" | "destructive" | null | undefined;
81
+ tone?: "default" | "success" | "warning" | "destructive" | null | undefined;
60
82
  } & class_variance_authority_types.ClassProp) | undefined) => string;
61
83
  declare const progressBarVariants: (props?: ({
62
- tone?: "default" | "success" | "destructive" | null | undefined;
84
+ tone?: "default" | "success" | "warning" | "destructive" | null | undefined;
63
85
  } & class_variance_authority_types.ClassProp) | undefined) => string;
64
- /** Tông của thanh — xem chú thích ở trên về việc vì sao không có `warning`. */
86
+ /** 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. */
65
87
  type ProgressTone = NonNullable<VariantProps<typeof progressBarVariants>["tone"]>;
66
88
  interface ProgressProps extends React.ComponentProps<typeof ProgressPrimitive.Root>, VariantProps<typeof progressBarVariants> {
67
89
  }
@@ -8,6 +8,7 @@ const progressTrackVariants = cva("border-control-edge relative h-2 w-full overf
8
8
  tone: {
9
9
  default: "bg-primary-ink/20",
10
10
  success: "bg-success-ink/20",
11
+ warning: "bg-warning-ink/20",
11
12
  destructive: "bg-destructive-ink/20"
12
13
  }
13
14
  },
@@ -18,6 +19,8 @@ const progressBarVariants = cva("h-full w-full flex-1 transition-transform durat
18
19
  tone: {
19
20
  default: "bg-primary-ink",
20
21
  success: "bg-success-ink",
22
+ // `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.
23
+ warning: "bg-warning-ink hazard-stripes",
21
24
  destructive: "bg-destructive-ink"
22
25
  }
23
26
  },
@@ -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…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 * ─ 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: BA GIÁ TRỊ, VÀ KHÔNG CÓ `warning` ─\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 * 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 * destructive 6,68:1 5,82 4,85:1 4,25:1 ✓\n *\n * ⚠ KHÔNG có `warning`, và đây là quyết định ĐO ĐƯỢC chứ không phải bỏ sót: ở bảng màu light\n * `--warning-ink` và `--primary-ink` là CÙNG một giá trị (#7A5800 — `--yellow-ink` trỏ về\n * `--gold-500`, xem styles.css:174), nên một thanh `tone=\"warning\"` sẽ trông y hệt thanh mặc định.\n * Ở dark hai bậc cũng chỉ cách nhau một nấc vàng (#FACC15 ⇄ #F5D060). Một API đổi tông mà không\n * đổi được gì là thứ tệ hơn không có API: chỗ gọi tưởng đã cảnh báo, người dùng không thấy gì.\n *\n * Cần báo \"sắp chạm ngưỡng\" thì nói bằng thứ KHÁC màu thanh — `StatusPill`/`Badge` tông warning,\n * hoặc một dòng chữ `text-warning-ink` cạnh thanh; cả hai đều tương phản tốt với nền trang vì\n * chúng không phải cạnh tranh với chính màu brand. Muốn có `tone=\"warning\"` thật thì phải tách\n * `--warning-ink` khỏi `--gold-500` ở TẦNG TOKEN trước — đó là thay đổi bảng màu, không phải thay\n * đổi component, và nó đụng mọi bề mặt warning đang có (Badge, StatusPill, StatCard, Toast).\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 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 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 không có `warning`. */\nexport type ProgressTone = NonNullable<VariantProps<typeof progressBarVariants>[\"tone\"]>;\n\nexport interface ProgressProps\n extends React.ComponentProps<typeof ProgressPrimitive.Root>,\n VariantProps<typeof progressBarVariants> {}\n\nfunction Progress({ className, value, tone = \"default\", ...props }: ProgressProps) {\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n data-tone={tone}\n className={cn(progressTrackVariants({ tone }), className)}\n value={value}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={progressBarVariants({ tone })}\n style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { Progress, progressBarVariants, progressTrackVariants };\n"],"mappings":";AA2GM;AAzGN,YAAY,uBAAuB;AACnC,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAgEnB,MAAM,wBAAwB,IAAI,+EAA+E;AAAA,EAC/G,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,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,MACT,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CAAC;AASD,SAAS,SAAS,EAAE,WAAW,OAAO,OAAO,WAAW,GAAG,MAAM,GAAkB;AACjF,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX,WAAW,GAAG,sBAAsB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MACxD;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,OAAO,SAAS,EAAE,KAAK;AAAA;AAAA,MAC5D;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…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 * ─ 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\nfunction Progress({ className, value, tone = \"default\", ...props }: ProgressProps) {\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n data-tone={tone}\n className={cn(progressTrackVariants({ tone }), className)}\n value={value}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={progressBarVariants({ tone })}\n style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { Progress, progressBarVariants, progressTrackVariants };\n"],"mappings":";AAoIM;AAlIN,YAAY,uBAAuB;AACnC,SAAS,WAA8B;AAEvC,SAAS,UAAU;AAsFnB,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;AASD,SAAS,SAAS,EAAE,WAAW,OAAO,OAAO,WAAW,GAAG,MAAM,GAAkB;AACjF,SACE;AAAA,IAAC,kBAAkB;AAAA,IAAlB;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX,WAAW,GAAG,sBAAsB,EAAE,KAAK,CAAC,GAAG,SAAS;AAAA,MACxD;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,OAAO,SAAS,EAAE,KAAK;AAAA;AAAA,MAC5D;AAAA;AAAA,EACF;AAEJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/styles.css CHANGED
@@ -940,3 +940,20 @@
940
940
  display: none;
941
941
  }
942
942
  }
943
+
944
+ /* ─ VẠCH CẢNH BÁO CHÉO — tín hiệu KHÔNG PHẢI MÀU cho `Progress tone="warning"` ─
945
+ Khoét những rãnh chéo xuyên qua phần đã chạy để nền rãnh lộ ra, nên nó KHÔNG thêm màu nào:
946
+ dùng `mask`, không dùng `background-image`, và hai chặn của mask là đen/trong suốt (kênh alpha,
947
+ không phải màu của bảng). Nhờ vậy chuỗi lớp của thanh vẫn chỉ có đúng một `bg-*` như ba tông
948
+ kia, và cổng màu đo nó y hệt các tông khác.
949
+
950
+ VÌ SAO CẦN: `--warning-ink` và `--primary-ink` gần như trùng nhau ở 3 trên 4 bảng màu (đo ở
951
+ chú thích đầu `progress.tsx`), nên tông warning KHÔNG thể phân biệt bằng màu. Rãnh chéo là cùng
952
+ một tín hiệu ấy nói bằng HÌNH, nên nó sống sót qua chế độ xám và qua mù màu — đúng WCAG 1.4.1.
953
+
954
+ 6px đục / 3px rãnh: phần đã chạy vẫn đặc 2/3 nên vẫn đọc được TỈ LỆ, mà rãnh vẫn đủ rộng để
955
+ thấy ở thanh cao 8px. Tĩnh, KHÔNG chạy — vạch trôi là hiệu ứng vô nghĩa với `prefers-reduced-motion`. */
956
+ @utility hazard-stripes {
957
+ -webkit-mask-image: repeating-linear-gradient(135deg, #000 0 6px, transparent 6px 9px);
958
+ mask-image: repeating-linear-gradient(135deg, #000 0 6px, transparent 6px 9px);
959
+ }