@comitor/ui 3.1.0 → 3.2.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/dist/components/composite/avatar-group.d.ts +10 -1
- package/dist/components/composite/avatar-group.js +37 -14
- package/dist/components/composite/avatar-group.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +41 -1
- package/dist/components/composite/status-config.js +6 -0
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/composite/status-pill.d.ts +4 -1
- package/dist/components/composite/status-pill.js +1 -1
- package/dist/components/composite/status-pill.js.map +1 -1
- package/dist/data-grid/data-grid-cell-variants.js +23 -5
- package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
- package/dist/data-grid/types.d.ts +16 -0
- package/dist/data-grid/use-data-grid.d.ts +2 -2
- package/dist/tokens.d.ts +43 -11
- package/dist/tokens.js +36 -9
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
- package/styles.css +73 -13
|
@@ -49,6 +49,15 @@ interface AvatarGroupProps extends Omit<ComponentProps<"div">, "children"> {
|
|
|
49
49
|
* Đặt nhóm lên bề mặt khác thì khai lại cả hai class, vd `"bg-sidebar ring-sidebar"`.
|
|
50
50
|
*/
|
|
51
51
|
surfaceClassName?: string;
|
|
52
|
+
/**
|
|
53
|
+
* `false` (thêm ở `3.2.0`): vẽ TĨNH — không nút, không tooltip, ô "+k" không bấm được
|
|
54
|
+
* (`onOverflowClick` bị bỏ qua). Dùng khi nhóm nằm TRONG một phần tử tương tác khác: ô của
|
|
55
|
+
* `DataGrid` (ô đã là `role="button"`), thẻ bấm được, dòng là link. Nút lồng trong nút là
|
|
56
|
+
* HTML/ARIA sai (axe: nested-interactive) và chen điểm dừng Tab vào giữa các ô. Tên từng người
|
|
57
|
+
* vẫn tới trình đọc màn hình qua `sr-only` của `LetterAvatar`, và chuột thấy tên qua `title`
|
|
58
|
+
* gốc của nó. Mặc định `true`.
|
|
59
|
+
*/
|
|
60
|
+
interactive?: boolean;
|
|
52
61
|
}
|
|
53
62
|
/**
|
|
54
63
|
* Chồng avatar + ô "+k" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).
|
|
@@ -59,6 +68,6 @@ interface AvatarGroupProps extends Omit<ComponentProps<"div">, "children"> {
|
|
|
59
68
|
* Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình
|
|
60
69
|
* — xem chú thích ở trigger bên dưới.
|
|
61
70
|
*/
|
|
62
|
-
declare function AvatarGroup({ items, max, size, onOverflowClick, overflowLabel: overflowLabelProp, label: labelProp, surfaceClassName, className, ...props }: AvatarGroupProps): React.JSX.Element;
|
|
71
|
+
declare function AvatarGroup({ items, max, size, onOverflowClick, overflowLabel: overflowLabelProp, label: labelProp, surfaceClassName, interactive, className, ...props }: AvatarGroupProps): React.JSX.Element;
|
|
63
72
|
|
|
64
73
|
export { AvatarGroup, type AvatarGroupItem, type AvatarGroupProps };
|
|
@@ -26,6 +26,7 @@ function AvatarGroup({
|
|
|
26
26
|
overflowLabel: overflowLabelProp,
|
|
27
27
|
label: labelProp,
|
|
28
28
|
surfaceClassName = "bg-card ring-card",
|
|
29
|
+
interactive = true,
|
|
29
30
|
className,
|
|
30
31
|
...props
|
|
31
32
|
}) {
|
|
@@ -34,6 +35,41 @@ function AvatarGroup({
|
|
|
34
35
|
const label = labelProp ?? t.label;
|
|
35
36
|
const visible = items.slice(0, Math.max(0, max));
|
|
36
37
|
const overflow = items.length - visible.length;
|
|
38
|
+
const staticOverflow = overflow > 0 ? /* @__PURE__ */ jsxs(
|
|
39
|
+
"span",
|
|
40
|
+
{
|
|
41
|
+
className: cn(
|
|
42
|
+
surfaceClassName,
|
|
43
|
+
"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2",
|
|
44
|
+
overflowSizeClasses[size]
|
|
45
|
+
),
|
|
46
|
+
children: [
|
|
47
|
+
"+",
|
|
48
|
+
overflow
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
) : null;
|
|
52
|
+
if (!interactive) {
|
|
53
|
+
return /* @__PURE__ */ jsxs(
|
|
54
|
+
"div",
|
|
55
|
+
{
|
|
56
|
+
"data-slot": "avatar-group",
|
|
57
|
+
role: "group",
|
|
58
|
+
"aria-label": label,
|
|
59
|
+
className: cn("flex items-center", overlapClasses[size], className),
|
|
60
|
+
...props,
|
|
61
|
+
children: [
|
|
62
|
+
visible.map((item) => (
|
|
63
|
+
// Cùng lớp nền đục + vòng như bản tương tác (xem `surfaceClassName`), chỉ bỏ nút và
|
|
64
|
+
// tooltip. Không truyền `title={undefined}`: `title={name}` của LetterAvatar ở lại để
|
|
65
|
+
// chuột vẫn đọc được tên khi không còn tooltip.
|
|
66
|
+
/* @__PURE__ */ jsx("span", { className: cn("inline-flex rounded-full ring-2", surfaceClassName), children: /* @__PURE__ */ jsx(LetterAvatar, { name: item.name, src: item.src, size }) }, item.id)
|
|
67
|
+
)),
|
|
68
|
+
staticOverflow
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
37
73
|
return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(
|
|
38
74
|
"div",
|
|
39
75
|
{
|
|
@@ -78,20 +114,7 @@ function AvatarGroup({
|
|
|
78
114
|
/* @__PURE__ */ jsx("span", { className: "sr-only", children: overflowLabel(items.length) })
|
|
79
115
|
]
|
|
80
116
|
}
|
|
81
|
-
) :
|
|
82
|
-
"span",
|
|
83
|
-
{
|
|
84
|
-
className: cn(
|
|
85
|
-
surfaceClassName,
|
|
86
|
-
"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2",
|
|
87
|
-
overflowSizeClasses[size]
|
|
88
|
-
),
|
|
89
|
-
children: [
|
|
90
|
-
"+",
|
|
91
|
-
overflow
|
|
92
|
-
]
|
|
93
|
-
}
|
|
94
|
-
))
|
|
117
|
+
) : staticOverflow)
|
|
95
118
|
]
|
|
96
119
|
}
|
|
97
120
|
) });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/avatar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip.js\";\nimport { LetterAvatar, type LetterAvatarSize } from \"./letter-avatar.js\";\n\nexport interface AvatarGroupItem {\n id: string;\n name: string;\n src?: string | null;\n /** Dòng phụ trong tooltip (vd email, vai trò) */\n caption?: string;\n}\n\nexport interface AvatarGroupProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n items: AvatarGroupItem[];\n /** Số avatar hiện tối đa; phần dư gộp vào ô \"+k\" (mặc định 4) */\n max?: number;\n size?: LetterAvatarSize;\n /** Bấm vào ô \"+k\" — vd mở danh sách đầy đủ */\n onOverflowClick?: () => void;\n /**\n * Nhãn `sr-only` của ô \"+k\" khi ô đó bấm được. Không truyền thì lấy khoá `overflowLabel` của\n * namespace `avatarGroup` (`ComitorLocaleProvider`, mặc định tiếng Anh: \"View all 5 members\").\n *\n * Là HÀM chứ không phải chuỗi mẫu vì có số chèn vào giữa câu: trật tự từ quanh con số khác nhau\n * theo ngôn ngữ, nên mọi cách để gói tự ghép chuỗi đều là chỗ chắc chắn dịch sai.\n *\n * Nhận TỔNG số thành viên (`items.length`), không phải phần dư đang bị giấu: mặt nút đã in sẵn\n * \"+k\" là phần dư rồi, còn câu này nói ra HÀNH ĐỘNG — bấm vào là xem hết cả nhóm.\n *\n * Khác `label` ở tầm phủ: `label` đặt tên cho CẢ NHÓM (`aria-label` của `role=\"group\"`), đọc lên\n * khi con trỏ bước vào nhóm; `overflowLabel` đặt tên cho MỘT nút bên trong, đọc lên khi Tab tới\n * đúng ô \"+k\". Đè bằng prop thì đè cả hai — đè mỗi `label` thì nhóm mang tên mới mà nút cuối\n * vẫn xưng theo bảng của provider.\n */\n overflowLabel?: (count: number) => string;\n /**\n * Nhãn của cả nhóm cho trình đọc màn hình (khoá `label` của namespace `avatarGroup`) — xem\n * `overflowLabel` để phân biệt\n */\n label?: string;\n /**\n * BỀ MẶT mà nhóm đang đứng lên — cho cả lớp nền đục dưới mỗi avatar lẫn màu vòng ngăn cách.\n *\n * Vì sao phải khai: tone avatar là các tint CÓ ALPHA (`bg-teal/15`, `bg-yellow/25`…), mà nhóm\n * thì cố tình cho chúng chồng lên nhau `-space-x-2`. Không có lớp đục ở dưới thì tint của avatar\n * trên hợp thành với MÀU CỦA AVATAR DƯỚI chứ không phải với nền trang — ba khuôn mặt lẫn vào\n * nhau đúng ở chỗ chồng. Đây cũng chính là bề mặt mà `getAvatarToneColors` giả định khi tính hex\n * đục cho canvas/email (`tintOver(…, p.card, …)`), nên mặc định là `--card`.\n *\n * Đặt nhóm lên bề mặt khác thì khai lại cả hai class, vd `\"bg-sidebar ring-sidebar\"`.\n */\n surfaceClassName?: string;\n}\n\n/**\n * Độ chồng theo CỠ, không phải một hằng số cho mọi cỡ.\n *\n * Mẫu Avatar stack ở comitor-ds là `-space-x-2` trên avatar 40px — tức chồng **20%**. Áp thẳng\n * `-space-x-2` cho mọi cỡ thì ở `xs` (24px) thành chồng 33%, và cộng thêm vòng 2px nữa là phần bị\n * che ăn vào đúng chữ cái đầu của initials: \"ĐH\" ở 10px bắt đầu cách mép ~5,5px, trong khi mép che\n * đã tới 10px. Giữ tỉ lệ ~20% thì mọi cỡ đọc được mà mật độ vẫn y như bản duyệt.\n *\n * xs 24px → 4px (17%) · sm 28px → 6px (21%) · md 36px → 8px (22%)\n * lg 44px → 10px (23%) · xl 56px → 12px (21%)\n */\nconst overlapClasses: Record<LetterAvatarSize, string> = {\n xs: \"-space-x-1\",\n sm: \"-space-x-1.5\",\n md: \"-space-x-2\",\n lg: \"-space-x-2.5\",\n xl: \"-space-x-3\"\n};\n\nconst overflowSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-7 text-xs\",\n md: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\"\n};\n\n/**\n * Chồng avatar + ô \"+k\" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).\n *\n * Tooltip hiện tên đầy đủ vì avatar chồng nhau thì initials bị che một phần; không có tooltip\n * thì nhóm avatar chỉ là trang trí, người dùng không biết ai đang ở trong việc.\n *\n * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình\n * — xem chú thích ở trigger bên dưới.\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"sm\",\n onOverflowClick,\n overflowLabel: overflowLabelProp,\n label: labelProp,\n surfaceClassName = \"bg-card ring-card\",\n className,\n ...props\n}: AvatarGroupProps) {\n const t = useComitorMessages(\"avatarGroup\");\n const overflowLabel = overflowLabelProp ?? t.overflowLabel;\n const label = labelProp ?? t.label;\n const visible = items.slice(0, Math.max(0, max));\n const overflow = items.length - visible.length;\n\n return (\n <TooltipProvider delayDuration={200}>\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center\", overlapClasses[size], className)}\n {...props}\n >\n {visible.map((item) => (\n <Tooltip key={item.id}>\n <TooltipTrigger asChild>\n {/* Trigger phải FOCUSABLE: `<span>` thường chỉ nhận sự kiện chuột nên tooltip\n (và cả tên thành viên) biến mất với người dùng bàn phím. Nút không có\n onClick là chủ ý — nó chỉ mở tooltip; tên accessible lấy từ `sr-only`\n bên trong LetterAvatar nên không cần aria-label lặp lại. */}\n {/* Viền focus dùng `foreground` chứ không `ring`: `--ring` là gold — #E8B824 chỉ\n 1,86:1 trên nền trang ở light, dưới 3:1 mà WCAG 1.4.11/2.4.11 đòi cho chỉ báo\n focus (ở dark #F5D060 mới đạt 12,67:1, tức hỏng đúng một nửa số người dùng).\n `outline-foreground` đạt 18,91:1 (light) / 17,66:1 (dark) — cùng lối với viền\n focus của dòng trong `DataTable`. */}\n {/* Nền đục nằm DƯỚI tint có alpha của avatar — xem `surfaceClassName`. Vòng\n `ring-2` cùng màu bề mặt chỉ tách hai avatar ra, nó KHÔNG che được phần tint\n chồng lên nhau bên trong vòng. */}\n {/* ⚠ `inline-flex` KHÔNG phải để bố trí — nó vá một khoảng trắng có thật.\n `LetterAvatar` là `inline-flex`, nên trong một nút KHỐI nó thành nội dung\n inline và bị canh theo BASELINE: line box chừa thêm khoảng chân chữ BÊN DƯỚI.\n Đã đo ở `xs` trên `/`: nút cao **30,5px** ôm một avatar **24px** — dư 6,5px,\n toàn bộ ở đáy. Nút lại có `bg-card` ĐỤC và `rounded-full`, nên chỗ dư ấy là\n một mảng trắng hình lưỡi liềm dưới mỗi khuôn mặt. Lúc nghỉ nó lẫn vào nền thẻ;\n `hover:z-10` + `hover:-translate-y-0.5` kéo nút lên trước và phơi nó ra.\n ⚠ `leading-none` KHÔNG đủ — đã đo: còn dư 2,5px. Phải bỏ hẳn line box.\n Ô \"+k\" bên dưới vốn đã `inline-flex`; chỗ này chỉ là sót. */}\n <button\n type=\"button\"\n className={cn(\n \"inline-flex rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n surfaceClassName\n )}\n >\n <LetterAvatar name={item.name} src={item.src} size={size} title={undefined} />\n </button>\n </TooltipTrigger>\n <TooltipContent>\n <span className=\"font-medium\">{item.name}</span>\n {/* `opacity-80` ở đây ĐẠT chuẩn và đã đo, không phải chỗ sót: Tooltip là\n `bg-foreground text-background`, nên chữ mờ 80% ra 12,15:1 (light) / 9,85:1\n (dark) — trên xa ngưỡng 4,5:1. Tooltip cũng không tương tác được. */}\n {item.caption && <span className=\"block text-xs opacity-80\">{item.caption}</span>}\n </TooltipContent>\n </Tooltip>\n ))}\n\n {overflow > 0 &&\n (onOverflowClick ? (\n <button\n type=\"button\"\n onClick={onOverflowClick}\n className={cn(\n // `surfaceClassName` đứng TRƯỚC để `bg-muted` ngay sau đè lại phần nền của nó:\n // ô \"+k\" vốn đã đục, ở đây chỉ mượn màu VÒNG của bề mặt.\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 transition-colors hover:bg-accent hover:text-accent-foreground\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n <span className=\"sr-only\">{overflowLabel(items.length)}</span>\n </button>\n ) : (\n <span\n className={cn(\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n </span>\n ))}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";AAuJgB,cAGJ,YAHI;AApJhB,SAAS,0BAA0B;AACnC,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,iBAAiB,sBAAsB;AACzE,SAAS,oBAA2C;AA+DpD,MAAM,iBAAmD;AAAA,EACvD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,sBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA,eAAe;AAAA,EACf,OAAO;AAAA,EACP,mBAAmB;AAAA,EACnB;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,IAAI,mBAAmB,aAAa;AAC1C,QAAM,gBAAgB,qBAAqB,EAAE;AAC7C,QAAM,QAAQ,aAAa,EAAE;AAC7B,QAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AAC/C,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,SACE,oBAAC,mBAAgB,eAAe,KAC9B;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,eAAe,IAAI,GAAG,SAAS;AAAA,MACjE,GAAG;AAAA,MAEH;AAAA,gBAAQ,IAAI,CAAC,SACZ,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MAsBrB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,OAAO,QAAW;AAAA;AAAA,UAC9E,GACF;AAAA,UACA,qBAAC,kBACC;AAAA,gCAAC,UAAK,WAAU,eAAe,eAAK,MAAK;AAAA,YAIxC,KAAK,WAAW,oBAAC,UAAK,WAAU,4BAA4B,eAAK,SAAQ;AAAA,aAC5E;AAAA,aAvCY,KAAK,EAwCnB,CACD;AAAA,QAEA,WAAW,MACT,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA,cACF,oBAAC,UAAK,WAAU,WAAW,wBAAc,MAAM,MAAM,GAAE;AAAA;AAAA;AAAA,QACzD,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA;AAAA;AAAA,QACJ;AAAA;AAAA;AAAA,EAEN,GACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/avatar-group.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { useComitorMessages } from \"../../i18n/locale-provider.js\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip.js\";\nimport { LetterAvatar, type LetterAvatarSize } from \"./letter-avatar.js\";\n\nexport interface AvatarGroupItem {\n id: string;\n name: string;\n src?: string | null;\n /** Dòng phụ trong tooltip (vd email, vai trò) */\n caption?: string;\n}\n\nexport interface AvatarGroupProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n items: AvatarGroupItem[];\n /** Số avatar hiện tối đa; phần dư gộp vào ô \"+k\" (mặc định 4) */\n max?: number;\n size?: LetterAvatarSize;\n /** Bấm vào ô \"+k\" — vd mở danh sách đầy đủ */\n onOverflowClick?: () => void;\n /**\n * Nhãn `sr-only` của ô \"+k\" khi ô đó bấm được. Không truyền thì lấy khoá `overflowLabel` của\n * namespace `avatarGroup` (`ComitorLocaleProvider`, mặc định tiếng Anh: \"View all 5 members\").\n *\n * Là HÀM chứ không phải chuỗi mẫu vì có số chèn vào giữa câu: trật tự từ quanh con số khác nhau\n * theo ngôn ngữ, nên mọi cách để gói tự ghép chuỗi đều là chỗ chắc chắn dịch sai.\n *\n * Nhận TỔNG số thành viên (`items.length`), không phải phần dư đang bị giấu: mặt nút đã in sẵn\n * \"+k\" là phần dư rồi, còn câu này nói ra HÀNH ĐỘNG — bấm vào là xem hết cả nhóm.\n *\n * Khác `label` ở tầm phủ: `label` đặt tên cho CẢ NHÓM (`aria-label` của `role=\"group\"`), đọc lên\n * khi con trỏ bước vào nhóm; `overflowLabel` đặt tên cho MỘT nút bên trong, đọc lên khi Tab tới\n * đúng ô \"+k\". Đè bằng prop thì đè cả hai — đè mỗi `label` thì nhóm mang tên mới mà nút cuối\n * vẫn xưng theo bảng của provider.\n */\n overflowLabel?: (count: number) => string;\n /**\n * Nhãn của cả nhóm cho trình đọc màn hình (khoá `label` của namespace `avatarGroup`) — xem\n * `overflowLabel` để phân biệt\n */\n label?: string;\n /**\n * BỀ MẶT mà nhóm đang đứng lên — cho cả lớp nền đục dưới mỗi avatar lẫn màu vòng ngăn cách.\n *\n * Vì sao phải khai: tone avatar là các tint CÓ ALPHA (`bg-teal/15`, `bg-yellow/25`…), mà nhóm\n * thì cố tình cho chúng chồng lên nhau `-space-x-2`. Không có lớp đục ở dưới thì tint của avatar\n * trên hợp thành với MÀU CỦA AVATAR DƯỚI chứ không phải với nền trang — ba khuôn mặt lẫn vào\n * nhau đúng ở chỗ chồng. Đây cũng chính là bề mặt mà `getAvatarToneColors` giả định khi tính hex\n * đục cho canvas/email (`tintOver(…, p.card, …)`), nên mặc định là `--card`.\n *\n * Đặt nhóm lên bề mặt khác thì khai lại cả hai class, vd `\"bg-sidebar ring-sidebar\"`.\n */\n surfaceClassName?: string;\n /**\n * `false` (thêm ở `3.2.0`): vẽ TĨNH — không nút, không tooltip, ô \"+k\" không bấm được\n * (`onOverflowClick` bị bỏ qua). Dùng khi nhóm nằm TRONG một phần tử tương tác khác: ô của\n * `DataGrid` (ô đã là `role=\"button\"`), thẻ bấm được, dòng là link. Nút lồng trong nút là\n * HTML/ARIA sai (axe: nested-interactive) và chen điểm dừng Tab vào giữa các ô. Tên từng người\n * vẫn tới trình đọc màn hình qua `sr-only` của `LetterAvatar`, và chuột thấy tên qua `title`\n * gốc của nó. Mặc định `true`.\n */\n interactive?: boolean;\n}\n\n/**\n * Độ chồng theo CỠ, không phải một hằng số cho mọi cỡ.\n *\n * Mẫu Avatar stack ở comitor-ds là `-space-x-2` trên avatar 40px — tức chồng **20%**. Áp thẳng\n * `-space-x-2` cho mọi cỡ thì ở `xs` (24px) thành chồng 33%, và cộng thêm vòng 2px nữa là phần bị\n * che ăn vào đúng chữ cái đầu của initials: \"ĐH\" ở 10px bắt đầu cách mép ~5,5px, trong khi mép che\n * đã tới 10px. Giữ tỉ lệ ~20% thì mọi cỡ đọc được mà mật độ vẫn y như bản duyệt.\n *\n * xs 24px → 4px (17%) · sm 28px → 6px (21%) · md 36px → 8px (22%)\n * lg 44px → 10px (23%) · xl 56px → 12px (21%)\n */\nconst overlapClasses: Record<LetterAvatarSize, string> = {\n xs: \"-space-x-1\",\n sm: \"-space-x-1.5\",\n md: \"-space-x-2\",\n lg: \"-space-x-2.5\",\n xl: \"-space-x-3\"\n};\n\nconst overflowSizeClasses: Record<LetterAvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-7 text-xs\",\n md: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\"\n};\n\n/**\n * Chồng avatar + ô \"+k\" — dùng cho người thực hiện (Tasks), người tham gia (Chat), người theo dõi (CRM).\n *\n * Tooltip hiện tên đầy đủ vì avatar chồng nhau thì initials bị che một phần; không có tooltip\n * thì nhóm avatar chỉ là trang trí, người dùng không biết ai đang ở trong việc.\n *\n * Mỗi avatar là một nút focusable (Tab tới được) và mang tên thành viên cho trình đọc màn hình\n * — xem chú thích ở trigger bên dưới.\n */\nexport function AvatarGroup({\n items,\n max = 4,\n size = \"sm\",\n onOverflowClick,\n overflowLabel: overflowLabelProp,\n label: labelProp,\n surfaceClassName = \"bg-card ring-card\",\n interactive = true,\n className,\n ...props\n}: AvatarGroupProps) {\n const t = useComitorMessages(\"avatarGroup\");\n const overflowLabel = overflowLabelProp ?? t.overflowLabel;\n const label = labelProp ?? t.label;\n const visible = items.slice(0, Math.max(0, max));\n const overflow = items.length - visible.length;\n\n const staticOverflow =\n overflow > 0 ? (\n <span\n className={cn(\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n </span>\n ) : null;\n\n if (!interactive) {\n return (\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center\", overlapClasses[size], className)}\n {...props}\n >\n {visible.map((item) => (\n // Cùng lớp nền đục + vòng như bản tương tác (xem `surfaceClassName`), chỉ bỏ nút và\n // tooltip. Không truyền `title={undefined}`: `title={name}` của LetterAvatar ở lại để\n // chuột vẫn đọc được tên khi không còn tooltip.\n <span key={item.id} className={cn(\"inline-flex rounded-full ring-2\", surfaceClassName)}>\n <LetterAvatar name={item.name} src={item.src} size={size} />\n </span>\n ))}\n {staticOverflow}\n </div>\n );\n }\n\n return (\n <TooltipProvider delayDuration={200}>\n <div\n data-slot=\"avatar-group\"\n role=\"group\"\n aria-label={label}\n className={cn(\"flex items-center\", overlapClasses[size], className)}\n {...props}\n >\n {visible.map((item) => (\n <Tooltip key={item.id}>\n <TooltipTrigger asChild>\n {/* Trigger phải FOCUSABLE: `<span>` thường chỉ nhận sự kiện chuột nên tooltip\n (và cả tên thành viên) biến mất với người dùng bàn phím. Nút không có\n onClick là chủ ý — nó chỉ mở tooltip; tên accessible lấy từ `sr-only`\n bên trong LetterAvatar nên không cần aria-label lặp lại. */}\n {/* Viền focus dùng `foreground` chứ không `ring`: `--ring` là gold — #E8B824 chỉ\n 1,86:1 trên nền trang ở light, dưới 3:1 mà WCAG 1.4.11/2.4.11 đòi cho chỉ báo\n focus (ở dark #F5D060 mới đạt 12,67:1, tức hỏng đúng một nửa số người dùng).\n `outline-foreground` đạt 18,91:1 (light) / 17,66:1 (dark) — cùng lối với viền\n focus của dòng trong `DataTable`. */}\n {/* Nền đục nằm DƯỚI tint có alpha của avatar — xem `surfaceClassName`. Vòng\n `ring-2` cùng màu bề mặt chỉ tách hai avatar ra, nó KHÔNG che được phần tint\n chồng lên nhau bên trong vòng. */}\n {/* ⚠ `inline-flex` KHÔNG phải để bố trí — nó vá một khoảng trắng có thật.\n `LetterAvatar` là `inline-flex`, nên trong một nút KHỐI nó thành nội dung\n inline và bị canh theo BASELINE: line box chừa thêm khoảng chân chữ BÊN DƯỚI.\n Đã đo ở `xs` trên `/`: nút cao **30,5px** ôm một avatar **24px** — dư 6,5px,\n toàn bộ ở đáy. Nút lại có `bg-card` ĐỤC và `rounded-full`, nên chỗ dư ấy là\n một mảng trắng hình lưỡi liềm dưới mỗi khuôn mặt. Lúc nghỉ nó lẫn vào nền thẻ;\n `hover:z-10` + `hover:-translate-y-0.5` kéo nút lên trước và phơi nó ra.\n ⚠ `leading-none` KHÔNG đủ — đã đo: còn dư 2,5px. Phải bỏ hẳn line box.\n Ô \"+k\" bên dưới vốn đã `inline-flex`; chỗ này chỉ là sót. */}\n <button\n type=\"button\"\n className={cn(\n \"inline-flex rounded-full ring-2 transition-transform hover:z-10 hover:-translate-y-0.5 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground\",\n surfaceClassName\n )}\n >\n <LetterAvatar name={item.name} src={item.src} size={size} title={undefined} />\n </button>\n </TooltipTrigger>\n <TooltipContent>\n <span className=\"font-medium\">{item.name}</span>\n {/* `opacity-80` ở đây ĐẠT chuẩn và đã đo, không phải chỗ sót: Tooltip là\n `bg-foreground text-background`, nên chữ mờ 80% ra 12,15:1 (light) / 9,85:1\n (dark) — trên xa ngưỡng 4,5:1. Tooltip cũng không tương tác được. */}\n {item.caption && <span className=\"block text-xs opacity-80\">{item.caption}</span>}\n </TooltipContent>\n </Tooltip>\n ))}\n\n {overflow > 0 &&\n (onOverflowClick ? (\n <button\n type=\"button\"\n onClick={onOverflowClick}\n className={cn(\n // `surfaceClassName` đứng TRƯỚC để `bg-muted` ngay sau đè lại phần nền của nó:\n // ô \"+k\" vốn đã đục, ở đây chỉ mượn màu VÒNG của bề mặt.\n surfaceClassName,\n \"z-10 inline-flex shrink-0 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground ring-2 transition-colors hover:bg-accent hover:text-accent-foreground\",\n overflowSizeClasses[size]\n )}\n >\n +{overflow}\n <span className=\"sr-only\">{overflowLabel(items.length)}</span>\n </button>\n ) : (\n staticOverflow\n ))}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";AA2HM,SAyBM,KAzBN;AAxHN,SAAS,0BAA0B;AACnC,SAAS,UAAU;AACnB,SAAS,SAAS,gBAAgB,iBAAiB,sBAAsB;AACzE,SAAS,oBAA2C;AAwEpD,MAAM,iBAAmD;AAAA,EACvD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,sBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA,eAAe;AAAA,EACf,OAAO;AAAA,EACP,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,IAAI,mBAAmB,aAAa;AAC1C,QAAM,gBAAgB,qBAAqB,EAAE;AAC7C,QAAM,QAAQ,aAAa,EAAE;AAC7B,QAAM,UAAU,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,GAAG,CAAC;AAC/C,QAAM,WAAW,MAAM,SAAS,QAAQ;AAExC,QAAM,iBACJ,WAAW,IACT;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,oBAAoB,IAAI;AAAA,MAC1B;AAAA,MACD;AAAA;AAAA,QACG;AAAA;AAAA;AAAA,EACJ,IACE;AAEN,MAAI,CAAC,aAAa;AAChB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,aAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAY;AAAA,QACZ,WAAW,GAAG,qBAAqB,eAAe,IAAI,GAAG,SAAS;AAAA,QACjE,GAAG;AAAA,QAEH;AAAA,kBAAQ,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,YAIZ,oBAAC,UAAmB,WAAW,GAAG,mCAAmC,gBAAgB,GACnF,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,KADjD,KAAK,EAEhB;AAAA,WACD;AAAA,UACA;AAAA;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,SACE,oBAAC,mBAAgB,eAAe,KAC9B;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,eAAe,IAAI,GAAG,SAAS;AAAA,MACjE,GAAG;AAAA,MAEH;AAAA,gBAAQ,IAAI,CAAC,SACZ,qBAAC,WACC;AAAA,8BAAC,kBAAe,SAAO,MAsBrB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,gBAAa,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAY,OAAO,QAAW;AAAA;AAAA,UAC9E,GACF;AAAA,UACA,qBAAC,kBACC;AAAA,gCAAC,UAAK,WAAU,eAAe,eAAK,MAAK;AAAA,YAIxC,KAAK,WAAW,oBAAC,UAAK,WAAU,4BAA4B,eAAK,SAAQ;AAAA,aAC5E;AAAA,aAvCY,KAAK,EAwCnB,CACD;AAAA,QAEA,WAAW,MACT,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA;AAAA,cACA,oBAAoB,IAAI;AAAA,YAC1B;AAAA,YACD;AAAA;AAAA,cACG;AAAA,cACF,oBAAC,UAAK,WAAU,WAAW,wBAAc,MAAM,MAAM,GAAE;AAAA;AAAA;AAAA,QACzD,IAEA;AAAA;AAAA;AAAA,EAEN,GACF;AAEJ;","names":[]}
|
|
@@ -28,6 +28,23 @@ interface StatusTone {
|
|
|
28
28
|
* `textColor` (`border-success-ink`), đừng hạ alpha xuống `/40`.
|
|
29
29
|
*/
|
|
30
30
|
borderColor?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Lớp tô CHẤM của `StatusPill variant="dot"` (tuỳ chọn) — một token TÔ NỀN viết thành lớp nền
|
|
33
|
+
* TĨNH (`"bg-severe"`), sơn làm `background-color` của chấm 6px. Không khai thì chấm giữ cách
|
|
34
|
+
* cũ: lấy `currentColor` của bậc ink trong `textColor`.
|
|
35
|
+
*
|
|
36
|
+
* Vì sao cần: bậc ink được chọn để đủ tối cho CHỮ, và ở cỡ chấm một bậc tối đọc ra một đốm xỉn
|
|
37
|
+
* — `--warning-ink` #7A5800 là "ô-liu" chứ không phải "gấp". Chấm là hình, không phải chữ, nên
|
|
38
|
+
* nó chỉ chịu ngưỡng 3:1 của WCAG 1.4.11 và dùng được chính bậc tô nền nếu bậc đó đủ đậm.
|
|
39
|
+
*
|
|
40
|
+
* ⚠ Token phải đạt **3:1 với nền trang ở CẢ hai theme** (--background/--card, và nên cả
|
|
41
|
+
* --muted vì chấm hay nằm trong dòng bảng). Bậc tô nền của các tone khác KHÔNG đạt: `--warning`
|
|
42
|
+
* 1,59 · `--primary` 1,86:1 trên nền trắng ✗; `--success` qua nền trắng (3,13) nhưng tụt trên
|
|
43
|
+
* --muted (2,92:1 ✗) — nên đừng thêm trường này cho tone nào chưa đo. Cổng tương phản KHÔNG
|
|
44
|
+
* thấy được cặp này: chuỗi lớp nằm ở bảng tone, còn phần tử chấm nằm ở `status-pill.tsx` — số đo
|
|
45
|
+
* ghi tay ở bảng dưới là bằng chứng duy nhất. Như mọi trường ở đây: lớp TĨNH, không `bg-${tone}`.
|
|
46
|
+
*/
|
|
47
|
+
dotColor?: string;
|
|
31
48
|
}
|
|
32
49
|
interface StatusConfig<T extends string = string> extends StatusTone {
|
|
33
50
|
value: T;
|
|
@@ -64,16 +81,33 @@ interface StatusConfig<T extends string = string> extends StatusTone {
|
|
|
64
81
|
* text-success / text-info trên nền TRANG (outline, dot) = 3,13:1 / 4,14:1 ✗
|
|
65
82
|
*
|
|
66
83
|
* Đi qua bậc ink thì đạt ở CẢ hai theme (`--x-ink` ở `.dark` vốn trỏ về chính `--x`, nên không
|
|
67
|
-
* cần biến thể `dark:` nào)
|
|
84
|
+
* cần biến thể `dark:` nào). Số của bảng TƯƠNG PHẢN CAO — bảng mà cổng chặn, như mọi số trong
|
|
85
|
+
* docs/hop-dong-mau.md:
|
|
68
86
|
*
|
|
69
87
|
* soft (trên tint của chính nó) outline · dot (trên nền trang)
|
|
70
88
|
* success 5,60:1 (L) · 7,44:1 (D) 6,52:1 (L) · 10,85:1 (D)
|
|
71
89
|
* info 5,54:1 (L) · 7,12:1 (D) 6,68:1 (L) · 10,16:1 (D)
|
|
72
90
|
* destructive 5,76:1 (L) · 5,71:1 (D) 7,15:1 (L) · 6,84:1 (D)
|
|
73
91
|
* warning 5,93:1 (L) · 7,19:1 (D) 6,51:1 (L) · 12,35:1 (D)
|
|
92
|
+
* severe 5,55:1 (L) · 6,09:1 (D) 6,66:1 (L) · 8,36:1 (D) ← chỉ outline; chấm: dưới
|
|
74
93
|
* neutral 4,89:1 (L) · 4,86:1 (D) 5,23:1 (L) · 5,70:1 (D)
|
|
75
94
|
* primary 5,77:1 (L) · 7,26:1 (D) 6,51:1 (L) · 12,67:1 (D)
|
|
76
95
|
*
|
|
96
|
+
* ─ TONE `severe` (cam) — nấc giữa `warning` và `destructive` ─
|
|
97
|
+
* Ưu tiên cao, SLA sắp trễ — nấc mà GitHub Primer gọi là "severe". Có vì `warning` tô chấm bằng
|
|
98
|
+
* `--warning-ink` #7A5800: đạt chuẩn (6,51:1) nhưng ở cỡ chấm đọc ra ô-liu xỉn, không ra "gấp".
|
|
99
|
+
* Nó là tone DUY NHẤT có `dotColor`: chấm tô bằng chính bậc tô nền `--severe` (ngưỡng 3:1, vì chấm
|
|
100
|
+
* là hình) thay cho bậc ink tối. Đo ở CẢ HAI bảng màu — ở tone này bảng mặc định cũng đạt hết:
|
|
101
|
+
*
|
|
102
|
+
* soft (ink trên /15) outline (ink trên nền trang) dot (FILL trên nền trang)
|
|
103
|
+
* mặc định L #AC440F 4,87:1 5,85:1 (--muted 5,47) #EA580C 3,56:1 (--muted 3,32)
|
|
104
|
+
* tương phản L #9D3F10 5,55:1 6,66:1 (--muted 6,22) #EA580C 3,56:1 (--muted 3,32)
|
|
105
|
+
* cả hai D #FB923C 6,09:1 8,36:1 (--card 7,85) #FB923C 8,36:1 (--card 7,85)
|
|
106
|
+
*
|
|
107
|
+
* `/15` như `success`/`info`/`destructive` (không cần nấc `/20` của `warning`/`primary` — đó là
|
|
108
|
+
* cho vàng): tint cam đã nổi 1,20:1 so với nền trắng, hơn cả `success/15` (1,16:1). Trần tint của
|
|
109
|
+
* `text-severe-ink` là `/21` ở bảng mặc định (light) — vẫn giữ trần chung `/20` của hợp đồng.
|
|
110
|
+
*
|
|
77
111
|
* `warning` không còn cần cặp `text-gold-500 dark:text-warning`: `--warning-ink` ĐÚNG BẰNG
|
|
78
112
|
* `--gold-500` ở light và `--yellow` ở dark — cùng một màu, ít hơn một class và một nơi để lệch.
|
|
79
113
|
*
|
|
@@ -120,6 +154,12 @@ declare const STATUS_TONES: {
|
|
|
120
154
|
readonly textColor: "text-warning-ink";
|
|
121
155
|
readonly borderColor: "border-warning-ink";
|
|
122
156
|
};
|
|
157
|
+
readonly severe: {
|
|
158
|
+
readonly bgColor: "bg-severe/15";
|
|
159
|
+
readonly textColor: "text-severe-ink";
|
|
160
|
+
readonly borderColor: "border-severe-ink";
|
|
161
|
+
readonly dotColor: "bg-severe";
|
|
162
|
+
};
|
|
123
163
|
readonly destructive: {
|
|
124
164
|
readonly bgColor: "bg-destructive/15";
|
|
125
165
|
readonly textColor: "text-destructive-ink";
|
|
@@ -7,6 +7,12 @@ const STATUS_TONES = {
|
|
|
7
7
|
primary: { bgColor: "bg-primary/20", textColor: "text-primary-ink", borderColor: "border-primary-ink" },
|
|
8
8
|
success: { bgColor: "bg-success/15", textColor: "text-success-ink", borderColor: "border-success-ink" },
|
|
9
9
|
warning: { bgColor: "bg-warning/20", textColor: "text-warning-ink", borderColor: "border-warning-ink" },
|
|
10
|
+
severe: {
|
|
11
|
+
bgColor: "bg-severe/15",
|
|
12
|
+
textColor: "text-severe-ink",
|
|
13
|
+
borderColor: "border-severe-ink",
|
|
14
|
+
dotColor: "bg-severe"
|
|
15
|
+
},
|
|
10
16
|
destructive: {
|
|
11
17
|
bgColor: "bg-destructive/15",
|
|
12
18
|
textColor: "text-destructive-ink",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/status-config.ts"],"sourcesContent":["/**\n * Cơ chế StatusConfig generic — app khai map \"trạng thái → lớp màu TOKEN\".\n *\n * Vì sao generic thay vì liệt kê sẵn trạng thái: mỗi app Comitor có bộ trạng thái riêng\n * (Tasks: todo/doing/done; CRM: lead/qualified/won/lost; HR: nộp/duyệt/từ chối). Package\n * chỉ cấp KIỂU và bảng tone, còn nhãn tiếng Việt là chuyện của app.\n *\n * Bắt buộc dùng class token (`bg-success/15 text-success-ink`), KHÔNG hex — hex sẽ chết ở dark mode.\n *\n * @example\n * const TASK_STATUSES: StatusConfig<TaskStatus>[] = [\n * { value: \"todo\", label: \"Cần làm\", shortLabel: \"CL\", ...STATUS_TONES.neutral },\n * { value: \"doing\", label: \"Đang làm\", shortLabel: \"ĐL\", ...STATUS_TONES.info },\n * { value: \"done\", label: \"Hoàn thành\", shortLabel: \"HT\", ...STATUS_TONES.success }\n * ];\n */\n\nexport interface StatusTone {\n /** Lớp nền tint — token TÔ NỀN, vd \"bg-success/15\" */\n bgColor: string;\n /**\n * Lớp chữ — phải là bậc **ink** (`text-success-ink`), KHÔNG phải token tô nền (`text-success`).\n * Chữ chip đứng trên tint của chính màu đó nên đo theo AA 4,5:1; bậc tô nền không đạt ở light.\n */\n textColor: string;\n /**\n * Lớp viền (tuỳ chọn) — dùng cho biến thể `outline`. Ở biến thể đó nền trong suốt nên viền là\n * thứ DUY NHẤT khoanh vùng chip → chịu ngưỡng 3:1 của WCAG 1.4.11. Dùng cùng bậc ink với\n * `textColor` (`border-success-ink`), đừng hạ alpha xuống `/40`.\n */\n borderColor?: string;\n}\n\nexport interface StatusConfig<T extends string = string> extends StatusTone {\n value: T;\n label: string;\n /** Nhãn rút gọn cho bảng/chip hẹp (vd \"HT\" thay \"Hoàn thành\") */\n shortLabel?: string;\n /** Ưu tiên sắp xếp (nhỏ = lên trước) */\n priority?: number;\n /**\n * Class thêm cho DÒNG bảng khi trạng thái cần gây chú ý (vd quá hạn).\n *\n * ⚠ ĐỪNG để đây một lớp nền có alpha (`bg-destructive/5`…) nếu bảng có cột GHIM hoặc có\n * `striped`. Ô cột ghim lấy nền bằng `bg-inherit` từ hàng nên nó thừa hưởng luôn alpha và\n * trở nên trong mờ; còn một class nền trần thì thua sọc của `striped` về độ đặc tả nên mất\n * hẳn ở hàng chẵn. Dùng `outline` (như `isRowActive` của `DataTable` đang làm) hoặc nói trạng\n * thái bằng chip + màu chữ.\n */\n rowClassName?: string;\n}\n\n/**\n * Bảng tone dựng sẵn theo token semantic — trải (`...STATUS_TONES.success`) vào StatusConfig.\n *\n * Mọi chuỗi ở đây là class TĨNH để Tailwind quét được từ `dist`; tuyệt đối không sinh động\n * kiểu `bg-${tone}/15` (class động bị loại khỏi bundle của app).\n *\n * ─ VÌ SAO CHỮ VÀ VIỀN ĐỀU LÀ BẬC `--x-ink`, KHÔNG PHẢI `--x` ─\n *\n * `--success/--warning/--info/--destructive` là token TÔ NỀN. Đặt chúng làm màu CHỮ thì bản light\n * hỏng ở cả ba biến thể của `StatusPill` (đo theo WCAG, nền tint đã trộn với `--card` trắng):\n *\n * text-success trên bg-success/15 (#E0F2E9) = 2,69:1 ✗ (dưới cả ngưỡng 3:1)\n * text-info trên bg-info/15 (#DDEDEE) = 3,44:1 ✗\n * text-destructive trên bg-destructive/15 = 4,06:1 ✗\n * text-success / text-info trên nền TRANG (outline, dot) = 3,13:1 / 4,14:1 ✗\n *\n * Đi qua bậc ink thì đạt ở CẢ hai theme (`--x-ink` ở `.dark` vốn trỏ về chính `--x`, nên không\n * cần biến thể `dark:` nào):\n *\n * soft (trên tint của chính nó) outline · dot (trên nền trang)\n * success 5,60:1 (L) · 7,44:1 (D) 6,52:1 (L) · 10,85:1 (D)\n * info 5,54:1 (L) · 7,12:1 (D) 6,68:1 (L) · 10,16:1 (D)\n * destructive 5,76:1 (L) · 5,71:1 (D) 7,15:1 (L) · 6,84:1 (D)\n * warning 5,93:1 (L) · 7,19:1 (D) 6,51:1 (L) · 12,35:1 (D)\n * neutral 4,89:1 (L) · 4,86:1 (D) 5,23:1 (L) · 5,70:1 (D)\n * primary 5,77:1 (L) · 7,26:1 (D) 6,51:1 (L) · 12,67:1 (D)\n *\n * `warning` không còn cần cặp `text-gold-500 dark:text-warning`: `--warning-ink` ĐÚNG BẰNG\n * `--gold-500` ở light và `--yellow` ở dark — cùng một màu, ít hơn một class và một nơi để lệch.\n *\n * ─ TONE `primary` ĐI THEO `--primary`, KHÔNG PHẢI THANG `gold-*` ─\n * Trước đây tone này chôn cứng `bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100`.\n * Tương phản thì ĐẠT (7,77:1 L · 11,85:1 D soft; 9,58:1 L · 15,33:1 D outline — bảng cũ ghi\n * 11,90:1 cho ô dark, đó là số của `--primary-ink` trên `--card`, chép nhầm sang), nhưng SAI VAI\n * TRÒ: thang `gold-*` không có bản dark nên phải viết hai lớp `dark:`, và app đổi `--primary` cho\n * sản phẩm của mình thì chip \"primary\" vẫn cứ vàng. `bg-primary/20` + `text-primary-ink` đi theo\n * token ở cả hai theme bằng MỘT lớp — hợp đồng §5. Alpha `/20` (không phải `/15` như các tone\n * khác) để giữ đúng độ nổi của `bg-gold-100` cũ: nền chip ⇄ nền trang 1,13:1 thay vì 1,10:1,\n * cùng mức với `warning` — vàng luôn cần thêm một nấc mới thấy được.\n *\n * ─ VIỀN: VÌ SAO BỎ HẲN ALPHA ─\n * Ở `variant=\"outline\"` nền trong suốt nên viền là ranh giới DUY NHẤT của chip → ngưỡng 3:1 của\n * WCAG 1.4.11. Không cách hạ alpha nào qua được; đo trên nền trang, cả hai cơ sở đều trượt:\n *\n * border-<tone>/40 (token NỀN) light 1,21–1,84:1 · dark 1,51–2,86:1 ✗\n * border-<tone>/70 (token NỀN) light 1,38–3,09:1 · dark 2,38–6,45:1 ✗\n * border-<tone>-ink/40 (bậc ink) light 1,88–1,99:1 · dark 2,02–2,86:1 ✗\n * border-<tone>-ink/70 (bậc ink) light 3,34–3,69:1 · dark 3,87–6,45:1 — hết biên an toàn\n *\n * (Bảng cũ trộn hai cơ sở: dải `/40` là của token NỀN, còn con số `/70 = 3,34:1` là của bậc ink.)\n * Viền full-strength bằng đúng màu chữ: 6,51–7,15:1 (light) · 6,84–12,67:1 (dark) ✓.\n */\nexport const STATUS_TONES = {\n neutral: {\n bgColor: \"bg-muted\",\n textColor: \"text-muted-foreground\",\n borderColor: \"border-muted-foreground\"\n },\n primary: { bgColor: \"bg-primary/20\", textColor: \"text-primary-ink\", borderColor: \"border-primary-ink\" },\n success: { bgColor: \"bg-success/15\", textColor: \"text-success-ink\", borderColor: \"border-success-ink\" },\n warning: { bgColor: \"bg-warning/20\", textColor: \"text-warning-ink\", borderColor: \"border-warning-ink\" },\n destructive: {\n bgColor: \"bg-destructive/15\",\n textColor: \"text-destructive-ink\",\n borderColor: \"border-destructive-ink\"\n },\n info: { bgColor: \"bg-info/15\", textColor: \"text-info-ink\", borderColor: \"border-info-ink\" }\n} as const satisfies Record<string, StatusTone>;\n\nexport type StatusToneName = keyof typeof STATUS_TONES;\n\n/** Tìm config theo value trong danh sách app cung cấp. */\nexport function getStatusConfig<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): StatusConfig<T> | undefined {\n if (value == null) return undefined;\n return configs.find((config) => config.value === value);\n}\n\n/** Lớp màu (nền tint + chữ) của một trạng thái; không tìm thấy → chuỗi rỗng. */\nexport function getStatusClasses<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return \"\";\n return `${config.bgColor} ${config.textColor}`;\n}\n\n/** Nhãn hiển thị của trạng thái; không tìm thấy → trả về chính `value` để không mất dữ liệu. */\nexport function getStatusLabel<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined,\n options: { short?: boolean } = {}\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return value ?? \"\";\n return options.short ? (config.shortLabel ?? config.label) : config.label;\n}\n\n/** Sắp xếp theo `priority` (thiếu priority thì xuống cuối, giữ thứ tự khai báo). */\nexport function sortByStatusPriority<T extends string>(configs: readonly StatusConfig<T>[]): StatusConfig<T>[] {\n return [...configs].sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));\n}\n"],"mappings":"AAwGO,MAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,aAAa;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,MAAM,EAAE,SAAS,cAAc,WAAW,iBAAiB,aAAa,kBAAkB;AAC5F;AAKO,SAAS,gBACd,SACA,OAC6B;AAC7B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACxD;AAGO,SAAS,iBACd,SACA,OACQ;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,GAAG,OAAO,OAAO,IAAI,OAAO,SAAS;AAC9C;AAGO,SAAS,eACd,SACA,OACA,UAA+B,CAAC,GACxB;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO,SAAS;AAC7B,SAAO,QAAQ,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO;AACtE;AAGO,SAAS,qBAAuC,SAAwD;AAC7G,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,qBAAqB,EAAE,YAAY,OAAO,iBAAiB;AACtH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/status-config.ts"],"sourcesContent":["/**\n * Cơ chế StatusConfig generic — app khai map \"trạng thái → lớp màu TOKEN\".\n *\n * Vì sao generic thay vì liệt kê sẵn trạng thái: mỗi app Comitor có bộ trạng thái riêng\n * (Tasks: todo/doing/done; CRM: lead/qualified/won/lost; HR: nộp/duyệt/từ chối). Package\n * chỉ cấp KIỂU và bảng tone, còn nhãn tiếng Việt là chuyện của app.\n *\n * Bắt buộc dùng class token (`bg-success/15 text-success-ink`), KHÔNG hex — hex sẽ chết ở dark mode.\n *\n * @example\n * const TASK_STATUSES: StatusConfig<TaskStatus>[] = [\n * { value: \"todo\", label: \"Cần làm\", shortLabel: \"CL\", ...STATUS_TONES.neutral },\n * { value: \"doing\", label: \"Đang làm\", shortLabel: \"ĐL\", ...STATUS_TONES.info },\n * { value: \"done\", label: \"Hoàn thành\", shortLabel: \"HT\", ...STATUS_TONES.success }\n * ];\n */\n\nexport interface StatusTone {\n /** Lớp nền tint — token TÔ NỀN, vd \"bg-success/15\" */\n bgColor: string;\n /**\n * Lớp chữ — phải là bậc **ink** (`text-success-ink`), KHÔNG phải token tô nền (`text-success`).\n * Chữ chip đứng trên tint của chính màu đó nên đo theo AA 4,5:1; bậc tô nền không đạt ở light.\n */\n textColor: string;\n /**\n * Lớp viền (tuỳ chọn) — dùng cho biến thể `outline`. Ở biến thể đó nền trong suốt nên viền là\n * thứ DUY NHẤT khoanh vùng chip → chịu ngưỡng 3:1 của WCAG 1.4.11. Dùng cùng bậc ink với\n * `textColor` (`border-success-ink`), đừng hạ alpha xuống `/40`.\n */\n borderColor?: string;\n /**\n * Lớp tô CHẤM của `StatusPill variant=\"dot\"` (tuỳ chọn) — một token TÔ NỀN viết thành lớp nền\n * TĨNH (`\"bg-severe\"`), sơn làm `background-color` của chấm 6px. Không khai thì chấm giữ cách\n * cũ: lấy `currentColor` của bậc ink trong `textColor`.\n *\n * Vì sao cần: bậc ink được chọn để đủ tối cho CHỮ, và ở cỡ chấm một bậc tối đọc ra một đốm xỉn\n * — `--warning-ink` #7A5800 là \"ô-liu\" chứ không phải \"gấp\". Chấm là hình, không phải chữ, nên\n * nó chỉ chịu ngưỡng 3:1 của WCAG 1.4.11 và dùng được chính bậc tô nền nếu bậc đó đủ đậm.\n *\n * ⚠ Token phải đạt **3:1 với nền trang ở CẢ hai theme** (--background/--card, và nên cả\n * --muted vì chấm hay nằm trong dòng bảng). Bậc tô nền của các tone khác KHÔNG đạt: `--warning`\n * 1,59 · `--primary` 1,86:1 trên nền trắng ✗; `--success` qua nền trắng (3,13) nhưng tụt trên\n * --muted (2,92:1 ✗) — nên đừng thêm trường này cho tone nào chưa đo. Cổng tương phản KHÔNG\n * thấy được cặp này: chuỗi lớp nằm ở bảng tone, còn phần tử chấm nằm ở `status-pill.tsx` — số đo\n * ghi tay ở bảng dưới là bằng chứng duy nhất. Như mọi trường ở đây: lớp TĨNH, không `bg-${tone}`.\n */\n dotColor?: string;\n}\n\nexport interface StatusConfig<T extends string = string> extends StatusTone {\n value: T;\n label: string;\n /** Nhãn rút gọn cho bảng/chip hẹp (vd \"HT\" thay \"Hoàn thành\") */\n shortLabel?: string;\n /** Ưu tiên sắp xếp (nhỏ = lên trước) */\n priority?: number;\n /**\n * Class thêm cho DÒNG bảng khi trạng thái cần gây chú ý (vd quá hạn).\n *\n * ⚠ ĐỪNG để đây một lớp nền có alpha (`bg-destructive/5`…) nếu bảng có cột GHIM hoặc có\n * `striped`. Ô cột ghim lấy nền bằng `bg-inherit` từ hàng nên nó thừa hưởng luôn alpha và\n * trở nên trong mờ; còn một class nền trần thì thua sọc của `striped` về độ đặc tả nên mất\n * hẳn ở hàng chẵn. Dùng `outline` (như `isRowActive` của `DataTable` đang làm) hoặc nói trạng\n * thái bằng chip + màu chữ.\n */\n rowClassName?: string;\n}\n\n/**\n * Bảng tone dựng sẵn theo token semantic — trải (`...STATUS_TONES.success`) vào StatusConfig.\n *\n * Mọi chuỗi ở đây là class TĨNH để Tailwind quét được từ `dist`; tuyệt đối không sinh động\n * kiểu `bg-${tone}/15` (class động bị loại khỏi bundle của app).\n *\n * ─ VÌ SAO CHỮ VÀ VIỀN ĐỀU LÀ BẬC `--x-ink`, KHÔNG PHẢI `--x` ─\n *\n * `--success/--warning/--info/--destructive` là token TÔ NỀN. Đặt chúng làm màu CHỮ thì bản light\n * hỏng ở cả ba biến thể của `StatusPill` (đo theo WCAG, nền tint đã trộn với `--card` trắng):\n *\n * text-success trên bg-success/15 (#E0F2E9) = 2,69:1 ✗ (dưới cả ngưỡng 3:1)\n * text-info trên bg-info/15 (#DDEDEE) = 3,44:1 ✗\n * text-destructive trên bg-destructive/15 = 4,06:1 ✗\n * text-success / text-info trên nền TRANG (outline, dot) = 3,13:1 / 4,14:1 ✗\n *\n * Đi qua bậc ink thì đạt ở CẢ hai theme (`--x-ink` ở `.dark` vốn trỏ về chính `--x`, nên không\n * cần biến thể `dark:` nào). Số của bảng TƯƠNG PHẢN CAO — bảng mà cổng chặn, như mọi số trong\n * docs/hop-dong-mau.md:\n *\n * soft (trên tint của chính nó) outline · dot (trên nền trang)\n * success 5,60:1 (L) · 7,44:1 (D) 6,52:1 (L) · 10,85:1 (D)\n * info 5,54:1 (L) · 7,12:1 (D) 6,68:1 (L) · 10,16:1 (D)\n * destructive 5,76:1 (L) · 5,71:1 (D) 7,15:1 (L) · 6,84:1 (D)\n * warning 5,93:1 (L) · 7,19:1 (D) 6,51:1 (L) · 12,35:1 (D)\n * severe 5,55:1 (L) · 6,09:1 (D) 6,66:1 (L) · 8,36:1 (D) ← chỉ outline; chấm: dưới\n * neutral 4,89:1 (L) · 4,86:1 (D) 5,23:1 (L) · 5,70:1 (D)\n * primary 5,77:1 (L) · 7,26:1 (D) 6,51:1 (L) · 12,67:1 (D)\n *\n * ─ TONE `severe` (cam) — nấc giữa `warning` và `destructive` ─\n * Ưu tiên cao, SLA sắp trễ — nấc mà GitHub Primer gọi là \"severe\". Có vì `warning` tô chấm bằng\n * `--warning-ink` #7A5800: đạt chuẩn (6,51:1) nhưng ở cỡ chấm đọc ra ô-liu xỉn, không ra \"gấp\".\n * Nó là tone DUY NHẤT có `dotColor`: chấm tô bằng chính bậc tô nền `--severe` (ngưỡng 3:1, vì chấm\n * là hình) thay cho bậc ink tối. Đo ở CẢ HAI bảng màu — ở tone này bảng mặc định cũng đạt hết:\n *\n * soft (ink trên /15) outline (ink trên nền trang) dot (FILL trên nền trang)\n * mặc định L #AC440F 4,87:1 5,85:1 (--muted 5,47) #EA580C 3,56:1 (--muted 3,32)\n * tương phản L #9D3F10 5,55:1 6,66:1 (--muted 6,22) #EA580C 3,56:1 (--muted 3,32)\n * cả hai D #FB923C 6,09:1 8,36:1 (--card 7,85) #FB923C 8,36:1 (--card 7,85)\n *\n * `/15` như `success`/`info`/`destructive` (không cần nấc `/20` của `warning`/`primary` — đó là\n * cho vàng): tint cam đã nổi 1,20:1 so với nền trắng, hơn cả `success/15` (1,16:1). Trần tint của\n * `text-severe-ink` là `/21` ở bảng mặc định (light) — vẫn giữ trần chung `/20` của hợp đồng.\n *\n * `warning` không còn cần cặp `text-gold-500 dark:text-warning`: `--warning-ink` ĐÚNG BẰNG\n * `--gold-500` ở light và `--yellow` ở dark — cùng một màu, ít hơn một class và một nơi để lệch.\n *\n * ─ TONE `primary` ĐI THEO `--primary`, KHÔNG PHẢI THANG `gold-*` ─\n * Trước đây tone này chôn cứng `bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100`.\n * Tương phản thì ĐẠT (7,77:1 L · 11,85:1 D soft; 9,58:1 L · 15,33:1 D outline — bảng cũ ghi\n * 11,90:1 cho ô dark, đó là số của `--primary-ink` trên `--card`, chép nhầm sang), nhưng SAI VAI\n * TRÒ: thang `gold-*` không có bản dark nên phải viết hai lớp `dark:`, và app đổi `--primary` cho\n * sản phẩm của mình thì chip \"primary\" vẫn cứ vàng. `bg-primary/20` + `text-primary-ink` đi theo\n * token ở cả hai theme bằng MỘT lớp — hợp đồng §5. Alpha `/20` (không phải `/15` như các tone\n * khác) để giữ đúng độ nổi của `bg-gold-100` cũ: nền chip ⇄ nền trang 1,13:1 thay vì 1,10:1,\n * cùng mức với `warning` — vàng luôn cần thêm một nấc mới thấy được.\n *\n * ─ VIỀN: VÌ SAO BỎ HẲN ALPHA ─\n * Ở `variant=\"outline\"` nền trong suốt nên viền là ranh giới DUY NHẤT của chip → ngưỡng 3:1 của\n * WCAG 1.4.11. Không cách hạ alpha nào qua được; đo trên nền trang, cả hai cơ sở đều trượt:\n *\n * border-<tone>/40 (token NỀN) light 1,21–1,84:1 · dark 1,51–2,86:1 ✗\n * border-<tone>/70 (token NỀN) light 1,38–3,09:1 · dark 2,38–6,45:1 ✗\n * border-<tone>-ink/40 (bậc ink) light 1,88–1,99:1 · dark 2,02–2,86:1 ✗\n * border-<tone>-ink/70 (bậc ink) light 3,34–3,69:1 · dark 3,87–6,45:1 — hết biên an toàn\n *\n * (Bảng cũ trộn hai cơ sở: dải `/40` là của token NỀN, còn con số `/70 = 3,34:1` là của bậc ink.)\n * Viền full-strength bằng đúng màu chữ: 6,51–7,15:1 (light) · 6,84–12,67:1 (dark) ✓.\n */\nexport const STATUS_TONES = {\n neutral: {\n bgColor: \"bg-muted\",\n textColor: \"text-muted-foreground\",\n borderColor: \"border-muted-foreground\"\n },\n primary: { bgColor: \"bg-primary/20\", textColor: \"text-primary-ink\", borderColor: \"border-primary-ink\" },\n success: { bgColor: \"bg-success/15\", textColor: \"text-success-ink\", borderColor: \"border-success-ink\" },\n warning: { bgColor: \"bg-warning/20\", textColor: \"text-warning-ink\", borderColor: \"border-warning-ink\" },\n severe: {\n bgColor: \"bg-severe/15\",\n textColor: \"text-severe-ink\",\n borderColor: \"border-severe-ink\",\n dotColor: \"bg-severe\"\n },\n destructive: {\n bgColor: \"bg-destructive/15\",\n textColor: \"text-destructive-ink\",\n borderColor: \"border-destructive-ink\"\n },\n info: { bgColor: \"bg-info/15\", textColor: \"text-info-ink\", borderColor: \"border-info-ink\" }\n} as const satisfies Record<string, StatusTone>;\n\nexport type StatusToneName = keyof typeof STATUS_TONES;\n\n/** Tìm config theo value trong danh sách app cung cấp. */\nexport function getStatusConfig<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): StatusConfig<T> | undefined {\n if (value == null) return undefined;\n return configs.find((config) => config.value === value);\n}\n\n/** Lớp màu (nền tint + chữ) của một trạng thái; không tìm thấy → chuỗi rỗng. */\nexport function getStatusClasses<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return \"\";\n return `${config.bgColor} ${config.textColor}`;\n}\n\n/** Nhãn hiển thị của trạng thái; không tìm thấy → trả về chính `value` để không mất dữ liệu. */\nexport function getStatusLabel<T extends string>(\n configs: readonly StatusConfig<T>[],\n value: T | null | undefined,\n options: { short?: boolean } = {}\n): string {\n const config = getStatusConfig(configs, value);\n if (!config) return value ?? \"\";\n return options.short ? (config.shortLabel ?? config.label) : config.label;\n}\n\n/** Sắp xếp theo `priority` (thiếu priority thì xuống cuối, giữ thứ tự khai báo). */\nexport function sortByStatusPriority<T extends string>(configs: readonly StatusConfig<T>[]): StatusConfig<T>[] {\n return [...configs].sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));\n}\n"],"mappings":"AA0IO,MAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,IACP,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,SAAS,EAAE,SAAS,iBAAiB,WAAW,oBAAoB,aAAa,qBAAqB;AAAA,EACtG,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,EACZ;AAAA,EACA,aAAa;AAAA,IACX,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,MAAM,EAAE,SAAS,cAAc,WAAW,iBAAiB,aAAa,kBAAkB;AAC5F;AAKO,SAAS,gBACd,SACA,OAC6B;AAC7B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,QAAQ,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACxD;AAGO,SAAS,iBACd,SACA,OACQ;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,GAAG,OAAO,OAAO,IAAI,OAAO,SAAS;AAC9C;AAGO,SAAS,eACd,SACA,OACA,UAA+B,CAAC,GACxB;AACR,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,MAAI,CAAC,OAAQ,QAAO,SAAS;AAC7B,SAAO,QAAQ,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO;AACtE;AAGO,SAAS,qBAAuC,SAAwD;AAC7G,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,qBAAqB,EAAE,YAAY,OAAO,iBAAiB;AACtH;","names":[]}
|
|
@@ -9,7 +9,10 @@ interface StatusPillProps<T extends string = string> extends Omit<ComponentProps
|
|
|
9
9
|
value: T | null | undefined;
|
|
10
10
|
/** Dùng shortLabel nếu có (bảng hẹp) */
|
|
11
11
|
short?: boolean;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* "soft" = nền tint (mặc định), "outline" = chỉ viền + chữ, "dot" = chấm màu + chữ thường (chấm
|
|
14
|
+
* tô bằng `dotColor` của tone nếu có, không thì bằng màu chữ của tone)
|
|
15
|
+
*/
|
|
13
16
|
variant?: StatusPillVariant;
|
|
14
17
|
/** Nhãn thay thế khi `value` không có trong `configs` */
|
|
15
18
|
fallbackLabel?: string;
|
|
@@ -15,7 +15,7 @@ function StatusPill({
|
|
|
15
15
|
const base = "inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-2 py-0.5 text-xs font-medium";
|
|
16
16
|
if (variant === "dot") {
|
|
17
17
|
return /* @__PURE__ */ jsxs("span", { "data-slot": "status-pill", className: cn(base, "px-0 text-foreground", className), ...props, children: [
|
|
18
|
-
/* @__PURE__ */ jsx(
|
|
18
|
+
config?.dotColor ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn("size-1.5 shrink-0 rounded-full", config.dotColor) }) : /* @__PURE__ */ jsx(
|
|
19
19
|
"span",
|
|
20
20
|
{
|
|
21
21
|
"aria-hidden": "true",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/status-pill.tsx"],"sourcesContent":["import type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport type { StatusConfig } from \"./status-config.js\";\nimport { getStatusConfig } from \"./status-config.js\";\n\nexport interface StatusPillProps<T extends string = string> extends Omit<ComponentProps<\"span\">, \"children\"> {\n /** Toàn bộ bảng trạng thái của app */\n configs: readonly StatusConfig<T>[];\n /** Trạng thái cần hiển thị */\n value: T | null | undefined;\n /** Dùng shortLabel nếu có (bảng hẹp) */\n short?: boolean;\n
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/status-pill.tsx"],"sourcesContent":["import type { ComponentProps } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport type { StatusConfig } from \"./status-config.js\";\nimport { getStatusConfig } from \"./status-config.js\";\n\nexport interface StatusPillProps<T extends string = string> extends Omit<ComponentProps<\"span\">, \"children\"> {\n /** Toàn bộ bảng trạng thái của app */\n configs: readonly StatusConfig<T>[];\n /** Trạng thái cần hiển thị */\n value: T | null | undefined;\n /** Dùng shortLabel nếu có (bảng hẹp) */\n short?: boolean;\n /**\n * \"soft\" = nền tint (mặc định), \"outline\" = chỉ viền + chữ, \"dot\" = chấm màu + chữ thường (chấm\n * tô bằng `dotColor` của tone nếu có, không thì bằng màu chữ của tone)\n */\n variant?: StatusPillVariant;\n /** Nhãn thay thế khi `value` không có trong `configs` */\n fallbackLabel?: string;\n}\n\nexport type StatusPillVariant = \"soft\" | \"outline\" | \"dot\";\n\n/**\n * Chip trạng thái tô theo StatusConfig của app (màu lấy từ token, không hex).\n *\n * Tĩnh hoàn toàn → dùng được trong React Server Component (bảng danh sách render phía server).\n * Trạng thái lạ (dữ liệu cũ, enum mới ở backend) KHÔNG bị nuốt: vẫn hiện nhãn ở tone neutral.\n */\nexport function StatusPill<T extends string = string>({\n configs,\n value,\n short = false,\n variant = \"soft\",\n fallbackLabel,\n className,\n ...props\n}: StatusPillProps<T>) {\n const config = getStatusConfig(configs, value);\n const label = config ? (short ? (config.shortLabel ?? config.label) : config.label) : (fallbackLabel ?? value ?? \"—\");\n\n const base = \"inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-2 py-0.5 text-xs font-medium\";\n\n if (variant === \"dot\") {\n // Tone có `dotColor` (hiện chỉ `severe`): chấm tô bằng token TÔ NỀN đó — đã đo 3:1 với nền trang,\n // vì ở cỡ 6px bậc ink (đủ tối cho CHỮ) đọc ra một đốm xỉn. Tone không khai thì giữ NGUYÊN phần tử\n // cũ: chấm lấy `currentColor` của bậc ink trong `textColor`.\n // Hai nhánh JSX tách hẳn chứ không gắn thuộc tính style theo điều kiện: style không phải object\n // literal là chỗ im lặng duy nhất của cổng tương phản (docs/hop-dong-mau.md §8, \"Còn lại\" mục 1).\n return (\n <span data-slot=\"status-pill\" className={cn(base, \"px-0 text-foreground\", className)} {...props}>\n {config?.dotColor ? (\n <span aria-hidden=\"true\" className={cn(\"size-1.5 shrink-0 rounded-full\", config.dotColor)} />\n ) : (\n <span\n aria-hidden=\"true\"\n className={cn(\"size-1.5 shrink-0 rounded-full\", config ? config.textColor : \"text-muted-foreground\")}\n style={{ backgroundColor: \"currentColor\" }}\n />\n )}\n {label}\n </span>\n );\n }\n\n if (variant === \"outline\") {\n return (\n <span\n data-slot=\"status-pill\"\n className={cn(\n base,\n \"border bg-transparent\",\n // Lui về `border-muted-foreground` chứ KHÔNG phải `border-border`: ở biến thể này nền\n // trong suốt nên viền là thứ DUY NHẤT khoanh vùng chip → ngưỡng 3:1. `--border` là bậc\n // TRANG TRÍ, chỉ 1,16:1 (L) · 1,17:1 (D) so với nền trang ✗. `--muted-foreground` đạt\n // 5,23:1 (L) · 5,70:1 (D) và khớp đúng màu chữ lui bên dưới.\n config?.borderColor ?? \"border-muted-foreground\",\n config?.textColor ?? \"text-muted-foreground\",\n className\n )}\n {...props}\n >\n {label}\n </span>\n );\n }\n\n return (\n <span\n data-slot=\"status-pill\"\n className={cn(base, config?.bgColor ?? \"bg-muted\", config?.textColor ?? \"text-muted-foreground\", className)}\n {...props}\n >\n {label}\n </span>\n );\n}\n"],"mappings":"AAkDM,SAEI,KAFJ;AAjDN,SAAS,UAAU;AAEnB,SAAS,uBAAuB;AA0BzB,SAAS,WAAsC;AAAA,EACpD;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAuB;AACrB,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,QAAM,QAAQ,SAAU,QAAS,OAAO,cAAc,OAAO,QAAS,OAAO,QAAU,iBAAiB,SAAS;AAEjH,QAAM,OAAO;AAEb,MAAI,YAAY,OAAO;AAMrB,WACE,qBAAC,UAAK,aAAU,eAAc,WAAW,GAAG,MAAM,wBAAwB,SAAS,GAAI,GAAG,OACvF;AAAA,cAAQ,WACP,oBAAC,UAAK,eAAY,QAAO,WAAW,GAAG,kCAAkC,OAAO,QAAQ,GAAG,IAE3F;AAAA,QAAC;AAAA;AAAA,UACC,eAAY;AAAA,UACZ,WAAW,GAAG,kCAAkC,SAAS,OAAO,YAAY,uBAAuB;AAAA,UACnG,OAAO,EAAE,iBAAiB,eAAe;AAAA;AAAA,MAC3C;AAAA,MAED;AAAA,OACH;AAAA,EAEJ;AAEA,MAAI,YAAY,WAAW;AACzB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA;AAAA;AAAA;AAAA;AAAA,UAKA,QAAQ,eAAe;AAAA,UACvB,QAAQ,aAAa;AAAA,UACrB;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QAEH;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,MAAM,QAAQ,WAAW,YAAY,QAAQ,aAAa,yBAAyB,SAAS;AAAA,MACzG,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -784,6 +784,7 @@ function SelectCell({
|
|
|
784
784
|
[isEditing, isFocused, initialValue, tableMeta]
|
|
785
785
|
);
|
|
786
786
|
const displayLabel = value ? optionByValue.get(value)?.label ?? value : null;
|
|
787
|
+
const renderValue = cellOpts?.variant === "select" ? cellOpts.renderValue : void 0;
|
|
787
788
|
return /* @__PURE__ */ jsx(
|
|
788
789
|
DataGridCellWrapper,
|
|
789
790
|
{
|
|
@@ -806,10 +807,14 @@ function SelectCell({
|
|
|
806
807
|
{
|
|
807
808
|
"aria-label": cell.column.columnDef.meta?.label ?? columnId,
|
|
808
809
|
className: "size-full items-start border-none bg-transparent p-0 shadow-none focus:ring-0 focus-visible:ring-0 [&>span]:line-clamp-none [&_svg]:hidden",
|
|
809
|
-
children: displayLabel ? (
|
|
810
|
+
children: displayLabel && !renderValue ? (
|
|
810
811
|
// span (không dùng <Badge> vì Badge render <div>, đặt trong <button> là HTML sai)
|
|
811
812
|
/* @__PURE__ */ jsx("span", { className: cn(badgeVariants({ variant: "secondary" }), "whitespace-pre-wrap px-1.5 py-px"), children: /* @__PURE__ */ jsx(SelectValue, {}) })
|
|
812
|
-
) :
|
|
813
|
+
) : (
|
|
814
|
+
// Có `renderValue` thì mỗi mục đã tự vẽ bằng nó, và `SelectValue` nhân bản ĐÚNG phần
|
|
815
|
+
// ItemText của mục đang chọn — bọc thêm Badge là hai lớp chip lồng nhau.
|
|
816
|
+
/* @__PURE__ */ jsx(SelectValue, {})
|
|
817
|
+
)
|
|
813
818
|
}
|
|
814
819
|
),
|
|
815
820
|
/* @__PURE__ */ jsx(
|
|
@@ -820,10 +825,15 @@ function SelectCell({
|
|
|
820
825
|
alignOffset: -8,
|
|
821
826
|
sideOffset: -8,
|
|
822
827
|
className: "min-w-[calc(var(--radix-select-trigger-width)+16px)]",
|
|
823
|
-
children: /* @__PURE__ */ jsx(SelectGroup, { children: options.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option.value, children: option.label }, option.value)) })
|
|
828
|
+
children: /* @__PURE__ */ jsx(SelectGroup, { children: options.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option.value, children: renderValue ? renderValue(option.value, option) : option.label }, option.value)) })
|
|
824
829
|
}
|
|
825
830
|
)
|
|
826
|
-
] }) :
|
|
831
|
+
] }) : value && renderValue ? (
|
|
832
|
+
// KHÔNG gắn `grid-cell-content`: wrapper áp `line-clamp-*` lên slot đó, tức
|
|
833
|
+
// `display: -webkit-box` — phá bố cục flex của component app đặt vào. `relative` giữ các
|
|
834
|
+
// bản đo đặt `absolute` (BadgeOverflow) ở trong ô thay vì bám vào dòng.
|
|
835
|
+
/* @__PURE__ */ jsx("div", { "data-slot": "grid-cell-value", className: "relative flex min-w-0 items-center overflow-hidden", children: renderValue(value, optionByValue.get(value) ?? { value, label: value }) })
|
|
836
|
+
) : displayLabel ? /* @__PURE__ */ jsx(Badge, { "data-slot": "grid-cell-content", variant: "secondary", className: "whitespace-pre-wrap px-1.5 py-px", children: displayLabel }) : null
|
|
827
837
|
}
|
|
828
838
|
);
|
|
829
839
|
}
|
|
@@ -854,6 +864,7 @@ function MultiSelectCell({
|
|
|
854
864
|
const cellOpts = cell.column.columnDef.meta?.cell;
|
|
855
865
|
const options = React.useMemo(() => cellOpts?.variant === "multi-select" ? cellOpts.options : [], [cellOpts]);
|
|
856
866
|
const optionByValue = React.useMemo(() => new Map(options.map((option) => [option.value, option])), [options]);
|
|
867
|
+
const renderValue = cellOpts?.variant === "multi-select" ? cellOpts.renderValue : void 0;
|
|
857
868
|
const sideOffset = -(containerRef.current?.clientHeight ?? 0);
|
|
858
869
|
const prevCellValueRef = React.useRef(cellValue);
|
|
859
870
|
if (cellValue !== prevCellValueRef.current) {
|
|
@@ -1055,7 +1066,14 @@ function MultiSelectCell({
|
|
|
1055
1066
|
}
|
|
1056
1067
|
)
|
|
1057
1068
|
] }) : null,
|
|
1058
|
-
|
|
1069
|
+
selectedValues.length > 0 && renderValue ? (
|
|
1070
|
+
// ⚠ Khác bản gốc (3.2.0): app vẽ cả tập giá trị (AvatarGroup, StatusPill…) — xem `CellOpts`.
|
|
1071
|
+
// Hook gập "+N" ở trên vẫn chạy (hook không được gọi có điều kiện), chỉ là không dùng.
|
|
1072
|
+
/* @__PURE__ */ jsx("div", { "data-slot": "grid-cell-value", className: "relative flex min-w-0 items-center overflow-hidden", children: renderValue(
|
|
1073
|
+
selectedValues,
|
|
1074
|
+
selectedValues.map((val) => optionByValue.get(val) ?? { value: val, label: val })
|
|
1075
|
+
) })
|
|
1076
|
+
) : displayLabels.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1 overflow-hidden", children: [
|
|
1059
1077
|
visibleLabels.map((label, index) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "px-1.5 py-px", children: label }, selectedValues[index])),
|
|
1060
1078
|
hiddenBadgeCount > 0 && /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "px-1.5 py-px text-muted-foreground", children: [
|
|
1061
1079
|
"+",
|