@comitor/ui 0.3.3 → 0.5.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.
Files changed (74) hide show
  1. package/README.md +12 -8
  2. package/dist/components/composite/avatar-group.d.ts +13 -1
  3. package/dist/components/composite/avatar-group.js +19 -4
  4. package/dist/components/composite/avatar-group.js.map +1 -1
  5. package/dist/components/composite/combobox.js +169 -146
  6. package/dist/components/composite/combobox.js.map +1 -1
  7. package/dist/components/composite/data-table.d.ts +24 -1
  8. package/dist/components/composite/data-table.js +7 -1
  9. package/dist/components/composite/data-table.js.map +1 -1
  10. package/dist/components/composite/date-picker.js +46 -17
  11. package/dist/components/composite/date-picker.js.map +1 -1
  12. package/dist/components/composite/form-section.js +1 -1
  13. package/dist/components/composite/form-section.js.map +1 -1
  14. package/dist/components/composite/route-tabs.d.ts +75 -0
  15. package/dist/components/composite/route-tabs.js +50 -0
  16. package/dist/components/composite/route-tabs.js.map +1 -0
  17. package/dist/components/composite/stat-card.js +14 -10
  18. package/dist/components/composite/stat-card.js.map +1 -1
  19. package/dist/components/ui/calendar.d.ts +29 -24
  20. package/dist/components/ui/calendar.js +1 -1
  21. package/dist/components/ui/calendar.js.map +1 -1
  22. package/dist/components/ui/checkbox.js +2 -2
  23. package/dist/components/ui/checkbox.js.map +1 -1
  24. package/dist/components/ui/command.js +1 -1
  25. package/dist/components/ui/command.js.map +1 -1
  26. package/dist/components/ui/context-menu.js +4 -4
  27. package/dist/components/ui/context-menu.js.map +1 -1
  28. package/dist/components/ui/dropdown-menu.js +4 -4
  29. package/dist/components/ui/dropdown-menu.js.map +1 -1
  30. package/dist/components/ui/empty.js +1 -1
  31. package/dist/components/ui/empty.js.map +1 -1
  32. package/dist/components/ui/field.js +1 -1
  33. package/dist/components/ui/field.js.map +1 -1
  34. package/dist/components/ui/item.js +1 -1
  35. package/dist/components/ui/item.js.map +1 -1
  36. package/dist/components/ui/menubar.js +4 -4
  37. package/dist/components/ui/menubar.js.map +1 -1
  38. package/dist/components/ui/radio-group.js +1 -1
  39. package/dist/components/ui/radio-group.js.map +1 -1
  40. package/dist/components/ui/select.d.ts +35 -12
  41. package/dist/components/ui/select.js +1 -1
  42. package/dist/components/ui/select.js.map +1 -1
  43. package/dist/components/ui/switch.js +2 -2
  44. package/dist/components/ui/switch.js.map +1 -1
  45. package/dist/components/ui/table.js +2 -2
  46. package/dist/components/ui/table.js.map +1 -1
  47. package/dist/components/ui/tabs-variants.d.ts +21 -0
  48. package/dist/components/ui/tabs-variants.js +15 -0
  49. package/dist/components/ui/tabs-variants.js.map +1 -0
  50. package/dist/components/ui/tabs.d.ts +2 -10
  51. package/dist/components/ui/tabs.js +3 -14
  52. package/dist/components/ui/tabs.js.map +1 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/shell/app-header.js +47 -41
  57. package/dist/shell/app-header.js.map +1 -1
  58. package/dist/shell/mobile-menu.d.ts +6 -1
  59. package/dist/shell/mobile-menu.js +5 -5
  60. package/dist/shell/mobile-menu.js.map +1 -1
  61. package/dist/shell/notifications-button.js +31 -9
  62. package/dist/shell/notifications-button.js.map +1 -1
  63. package/dist/shell/sidebar.js +1 -1
  64. package/dist/shell/sidebar.js.map +1 -1
  65. package/dist/shell/types.d.ts +13 -0
  66. package/dist/shell/types.js +1 -0
  67. package/dist/shell/types.js.map +1 -1
  68. package/dist/shell/user-menu.d.ts +14 -2
  69. package/dist/shell/user-menu.js +3 -3
  70. package/dist/shell/user-menu.js.map +1 -1
  71. package/dist/shell/workspace-switcher.js +48 -34
  72. package/dist/shell/workspace-switcher.js.map +1 -1
  73. package/package.json +1 -1
  74. package/styles.css +95 -16
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/workspace-switcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { Building2, Check, ChevronsUpDown, LogOut, Plus, Search, Settings, UserPlus } from \"lucide-react\";\nimport { useMemo, useRef, useState } from \"react\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport type { Workspace } from \"./types.js\";\nimport { getVietnameseInitials, normalizeVi } from \"./utils.js\";\n\n/** Từ số workspace này trở lên thì hiện ô tìm kiếm trong danh sách. */\nconst SEARCH_THRESHOLD = 8;\n\n/**\n * Ảnh đại diện workspace: logo nếu có, không thì ô chữ cái tô màu deterministic.\n *\n * Seed là `workspace.name` chứ KHÔNG phải slug: app hiển thị workspace bằng\n * `<LetterAvatar name={workspace.name} square />` ở chỗ khác, seed lệch là màu lệch.\n */\nfunction WorkspaceAvatar({ workspace, className }: { workspace: Workspace; className?: string }) {\n return (\n <Avatar className={cn(\"size-6 rounded-md\", className)}>\n {workspace.logoUrl ? <AvatarImage src={workspace.logoUrl} alt=\"\" className=\"rounded-md\" /> : null}\n <AvatarFallback className={cn(\"rounded-md text-[10px] font-semibold\", getAvatarToneClasses(workspace.name))}>\n {getVietnameseInitials(workspace.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface WorkspaceSwitcherProps {\n className?: string;\n /** Sidebar thu gọn → chỉ hiện avatar + tooltip. */\n collapsed?: boolean;\n /** Ẩn khu \"Hành động\" (tạo workspace, cài đặt, mời thành viên, đăng xuất). */\n showActions?: boolean;\n /** Căn panel — mặc định \"start\" (mở xuôi theo mép trái sidebar). */\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 1 — đổi không gian làm việc.**\n *\n * Chỉ gọi `onWorkspaceChange(id)`; điều hướng/đổi subdomain là việc của app\n * (đổi workspace kéo theo đổi toàn bộ dữ liệu + quyền nên shell không tự quyết).\n * Có ≤1 workspace thì phần danh sách tự ẩn, chỉ còn nhãn workspace hiện tại.\n *\n * Dùng `Popover` chứ không `DropdownMenu`: menu của Radix có typeahead, gõ vào\n * ô tìm kiếm bên trong sẽ bị nó cướp phím.\n */\nexport function WorkspaceSwitcher({\n className,\n collapsed = false,\n showActions = true,\n align = \"start\"\n}: WorkspaceSwitcherProps) {\n const {\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n onLogout,\n labels\n } = useShell();\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const searchRef = useRef<HTMLInputElement>(null);\n\n const showList = workspaces.length > 1;\n const showSearch = workspaces.length >= SEARCH_THRESHOLD;\n\n const filtered = useMemo(() => {\n if (!query.trim()) return workspaces;\n const needle = normalizeVi(query);\n return workspaces.filter(\n (workspace) => normalizeVi(workspace.name).includes(needle) || normalizeVi(workspace.slug).includes(needle)\n );\n }, [workspaces, query]);\n\n const hasActions = showActions && Boolean(onCreateWorkspace || onWorkspaceSettings || onInviteMembers || onLogout);\n\n if (!currentWorkspace) return null;\n\n const handleSelect = (workspace: Workspace) => {\n setOpen(false);\n setQuery(\"\");\n if (workspace.id !== currentWorkspace.id) onWorkspaceChange?.(workspace.id);\n };\n\n /** Đóng panel rồi mới chạy hành động — tránh panel còn mở đè lên dialog app mở ra sau đó. */\n const closeThen = (action: () => void) => () => {\n setOpen(false);\n action();\n };\n\n const trigger = collapsed ? (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n </button>\n ) : (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-semibold text-sidebar-foreground\">{currentWorkspace.name}</span>\n {currentWorkspace.plan ? (\n // `text-muted-foreground` chứ không `text-sidebar-foreground/55`: đây là CHỮ (11px) nên\n // ngưỡng là AA 4,5:1, mà /55 trên `--sidebar` #FAFAF9 chỉ được 4,13:1 ✗ (dark 5,78:1 —\n // mỗi theme hỏng một nửa). `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò:\n // 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓.\n <span className=\"truncate text-[11px] text-muted-foreground\">{currentWorkspace.plan}</span>\n ) : null}\n </span>\n {/* Mũi tên MANG THÔNG TIN (nút này mở ra một panel chọn) nên chịu ngưỡng 3:1 của WCAG\n 1.4.11, mà `text-sidebar-foreground/45` chỉ được 3,02:1 trên `--sidebar` ở light và\n tụt xuống 3,01:1 khi hover đổi nền sang `--sidebar-accent` — sát/dưới vạch, và ở\n layout \"header-first\" nút này còn nằm trên nền `--background` khác nữa.\n `--muted-foreground` là bậc đã đo và dư biên ở mọi nền nó có thể đứng: 5,01:1 trên\n `--sidebar` · 4,78:1 trên `--sidebar-accent` · 5,23:1 trên `--background` (light);\n 5,35 / 4,86 / 5,70:1 (dark) ✓. (Dark của `/45` là 4,26 / 4,14:1 — chỉ light hỏng,\n nhưng bậc mờ theo alpha luôn lệch nhau như vậy giữa hai theme.) */}\n <ChevronsUpDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </button>\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {collapsed ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{currentWorkspace.name}</TooltipContent>\n </Tooltip>\n ) : (\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n )}\n\n <PopoverContent\n align={align}\n sideOffset={6}\n className=\"w-72 p-0\"\n onOpenAutoFocus={(event) => {\n // Mặc định Radix focus vào khung panel; có ô tìm thì đưa con trỏ vào đó luôn.\n if (!searchRef.current) return;\n event.preventDefault();\n searchRef.current.focus();\n }}\n >\n {showList ? (\n <>\n {showSearch ? (\n <div className=\"relative border-b border-border p-2\">\n <Search className=\"pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={labels.workspaceSearchPlaceholder}\n aria-label={labels.workspaceSearchPlaceholder}\n className={cn(\n \"h-8 w-full rounded-md border border-input bg-background pl-7 pr-2 text-sm\",\n // Focus dùng `--primary-ink`, KHÔNG `--ring` cũng không `--gold-300`. Cả hai\n // bậc kia ở light đều là gold #E8B824 — chỉ 1,73:1 trên nền trang, tức lúc\n // focus ô tìm MẤT ranh giới (`--ring` chỉ hợp cho vòng focus `ring-*`, thứ có\n // luật riêng, không phải cho viền control). `--primary-ink` giữ đúng sắc gold\n // mà đạt 6,09–6,51:1 (light) · 10,80–12,67:1 (dark) — hợp đồng màu §3.\n \"placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\"\n )}\n />\n </div>\n ) : null}\n\n {/*\n CỐ Ý không dùng role=\"listbox\"/\"option\": bộ ARIA đó hứa với trình đọc màn hình\n một widget điều khiển bằng ↑/↓/Home/End + roving tabindex + aria-activedescendant,\n mà hành vi thật ở đây chỉ là Tab qua từng nút trong Popover (Popover được chọn\n thay DropdownMenu vì menu của Radix cướp phím gõ của ô tìm kiếm — xem chú thích\n component). Khai đúng cái mình làm còn hơn khai sang trọng rồi lừa người dùng:\n danh sách nút thường + `aria-current` cho mục đang chọn.\n */}\n <div className=\"max-h-64 overflow-y-auto scrollbar-thin p-1\" role=\"group\" aria-label={labels.workspace}>\n {filtered.length === 0 ? (\n <p className=\"px-2 py-6 text-center text-sm text-muted-foreground\">{labels.workspaceEmpty}</p>\n ) : (\n filtered.map((workspace) => {\n const isCurrent = workspace.id === currentWorkspace.id;\n return (\n <button\n key={workspace.id}\n type=\"button\"\n aria-current={isCurrent ? \"true\" : undefined}\n onClick={() => handleSelect(workspace)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-accent\",\n isCurrent && \"bg-accent/60\"\n )}\n >\n <WorkspaceAvatar workspace={workspace} />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium text-foreground\">{workspace.name}</span>\n {workspace.role || workspace.memberCount !== undefined ? (\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {[\n workspace.role,\n workspace.memberCount === undefined\n ? undefined\n : `${workspace.memberCount} ${labels.members}`\n ]\n .filter(Boolean)\n .join(\" · \")}\n </span>\n ) : null}\n </span>\n {workspace.plan ? (\n <Badge variant=\"secondary\" size=\"sm\" className=\"shrink-0\">\n {workspace.plan}\n </Badge>\n ) : null}\n {/* KHÔNG dùng `text-app-accent`: accent mặc định (gold #E8B824) chỉ đạt\n 1,86:1 trên nền popover trắng — dưới ngưỡng 3:1 của WCAG 1.4.11 cho\n thành phần phi-văn-bản. `text-popover-foreground` đạt 18,9:1 (light)\n và 16,6:1 (dark). */}\n {isCurrent ? <Check className=\"size-4 shrink-0 text-popover-foreground\" /> : null}\n </button>\n );\n })\n )}\n </div>\n </>\n ) : (\n <div className=\"flex items-center gap-2 p-3\">\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{currentWorkspace.name}</span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {currentWorkspace.plan ?? currentWorkspace.slug}\n </span>\n </span>\n </div>\n )}\n\n {hasActions ? (\n <>\n <Separator />\n <div className=\"flex flex-col p-1\">\n {onCreateWorkspace ? (\n <ActionRow icon={Plus} label={labels.createWorkspace} onClick={closeThen(onCreateWorkspace)} />\n ) : null}\n {onWorkspaceSettings ? (\n <ActionRow\n icon={Settings}\n label={labels.workspaceSettings}\n onClick={closeThen(() => onWorkspaceSettings(currentWorkspace.id))}\n />\n ) : null}\n {onInviteMembers ? (\n <ActionRow\n icon={UserPlus}\n label={labels.inviteMembers}\n onClick={closeThen(() => onInviteMembers(currentWorkspace.id))}\n />\n ) : null}\n {onLogout ? (\n <ActionRow icon={LogOut} label={labels.logout} destructive onClick={closeThen(onLogout)} />\n ) : null}\n </div>\n </>\n ) : null}\n </PopoverContent>\n </Popover>\n );\n}\n\n/** Một dòng hành động trong panel (không phải mục chọn workspace). */\nfunction ActionRow({\n icon: Icon = Building2,\n label,\n onClick,\n destructive = false\n}: {\n icon?: typeof Building2;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n // Bậc ink chứ không `text-destructive`: dòng \"Đăng xuất\" là CHỮ trên nền `--popover`,\n // mà `--destructive` (token NỀN) chỉ được 3,53:1 trên `--popover` ở dark ✗. Ink đạt cả\n // hai: 7,15:1 (light) · 6,42:1 (dark). Icon ở `opacity-70` là thành phần phi-văn-bản\n // (ngưỡng 3:1): 3,69:1 / 3,73:1 lúc nghỉ, và 3,37:1 / 3,62:1 trên nền hover\n // `bg-destructive/10` — đó mới là tổ hợp chặt nhất, vẫn ✓. Chữ lúc hover: 6,18:1 / 5,94:1.\n destructive ? \"text-destructive-ink hover:bg-destructive/10\" : \"text-foreground hover:bg-accent\"\n )}\n >\n <Icon className=\"size-4 shrink-0 opacity-70\" />\n <span className=\"truncate\">{label}</span>\n </button>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/workspace-switcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { Building2, Check, ChevronsUpDown, LogOut, Plus, Search, Settings, UserPlus } from \"lucide-react\";\nimport { useMemo, useRef, useState } from \"react\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport type { Workspace } from \"./types.js\";\nimport { getVietnameseInitials, normalizeVi } from \"./utils.js\";\n\n/** Từ số workspace này trở lên thì hiện ô tìm kiếm trong danh sách. */\nconst SEARCH_THRESHOLD = 8;\n\n/**\n * Ảnh đại diện workspace: logo nếu có, không thì ô chữ cái tô màu deterministic.\n *\n * Seed là `workspace.name` chứ KHÔNG phải slug: app hiển thị workspace bằng\n * `<LetterAvatar name={workspace.name} square />` ở chỗ khác, seed lệch là màu lệch.\n */\nfunction WorkspaceAvatar({ workspace, className }: { workspace: Workspace; className?: string }) {\n return (\n <Avatar className={cn(\"size-6 rounded-md\", className)}>\n {workspace.logoUrl ? <AvatarImage src={workspace.logoUrl} alt=\"\" className=\"rounded-md\" /> : null}\n <AvatarFallback className={cn(\"rounded-md text-[10px] font-semibold\", getAvatarToneClasses(workspace.name))}>\n {getVietnameseInitials(workspace.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface WorkspaceSwitcherProps {\n className?: string;\n /** Sidebar thu gọn → chỉ hiện avatar + tooltip. */\n collapsed?: boolean;\n /** Ẩn khu \"Hành động\" (tạo workspace, cài đặt, mời thành viên, đăng xuất). */\n showActions?: boolean;\n /** Căn panel — mặc định \"start\" (mở xuôi theo mép trái sidebar). */\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 1 — đổi không gian làm việc.**\n *\n * Chỉ gọi `onWorkspaceChange(id)`; điều hướng/đổi subdomain là việc của app\n * (đổi workspace kéo theo đổi toàn bộ dữ liệu + quyền nên shell không tự quyết).\n * Có ≤1 workspace thì phần danh sách tự ẩn, chỉ còn nhãn workspace hiện tại.\n *\n * Dùng `Popover` chứ không `DropdownMenu`: menu của Radix có typeahead, gõ vào\n * ô tìm kiếm bên trong sẽ bị nó cướp phím.\n */\nexport function WorkspaceSwitcher({\n className,\n collapsed = false,\n showActions = true,\n align = \"start\"\n}: WorkspaceSwitcherProps) {\n const {\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n onLogout,\n labels\n } = useShell();\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const searchRef = useRef<HTMLInputElement>(null);\n\n const showList = workspaces.length > 1;\n const showSearch = workspaces.length >= SEARCH_THRESHOLD;\n\n const filtered = useMemo(() => {\n if (!query.trim()) return workspaces;\n const needle = normalizeVi(query);\n return workspaces.filter(\n (workspace) => normalizeVi(workspace.name).includes(needle) || normalizeVi(workspace.slug).includes(needle)\n );\n }, [workspaces, query]);\n\n const hasActions = showActions && Boolean(onCreateWorkspace || onWorkspaceSettings || onInviteMembers || onLogout);\n\n if (!currentWorkspace) return null;\n\n const handleSelect = (workspace: Workspace) => {\n setOpen(false);\n setQuery(\"\");\n if (workspace.id !== currentWorkspace.id) onWorkspaceChange?.(workspace.id);\n };\n\n /** Đóng panel rồi mới chạy hành động — tránh panel còn mở đè lên dialog app mở ra sau đó. */\n const closeThen = (action: () => void) => () => {\n setOpen(false);\n action();\n };\n\n const trigger = collapsed ? (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n </button>\n ) : (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n {/*\n `size-8` (32px) thay vì `size-6` (24px): avatar cao BẰNG khối chữ hai dòng bên cạnh\n (17,5px + 13,75px ≈ 31px sau khi cả hai dòng dùng `leading-tight`), nên hàng này đọc ra\n một khối chứ không phải một ô vuông nhỏ lửng lơ cạnh hai dòng chữ.\n ⚠ Chỉ đổi ở trigger dạng ĐẦY ĐỦ. Trigger lúc thanh bên thu gọn vẫn `size-6` trong một nút\n 36px — ở đó avatar là toàn bộ nội dung nên nó cần đệm quanh, không cần lấp đầy.\n */}\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n {/* `leading-tight` ở CẢ hai dòng: mặc định `text-sm` có line-height 20px và dòng gói\n 11px thừa hưởng line-height của body (~1,5) — cộng lại là một khoảng hở rộng hơn cả\n thân chữ, khiến tên gói trông như không thuộc về tên workspace. */}\n <span className=\"truncate text-sm font-semibold leading-tight text-sidebar-foreground\">\n {currentWorkspace.name}\n </span>\n {currentWorkspace.plan ? (\n // `text-muted-foreground` chứ không `text-sidebar-foreground/55`: đây là CHỮ (11px) nên\n // ngưỡng là AA 4,5:1, mà /55 trên `--sidebar` #FAFAF9 chỉ được 4,13:1 ✗ (dark 5,78:1 —\n // mỗi theme hỏng một nửa). `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò:\n // 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓.\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{currentWorkspace.plan}</span>\n ) : null}\n </span>\n {/* Mũi tên MANG THÔNG TIN (nút này mở ra một panel chọn) nên chịu ngưỡng 3:1 của WCAG\n 1.4.11, mà `text-sidebar-foreground/45` chỉ được 3,02:1 trên `--sidebar` ở light và\n tụt xuống 3,01:1 khi hover đổi nền sang `--sidebar-accent` — sát/dưới vạch, và ở\n layout \"header-first\" nút này còn nằm trên nền `--background` khác nữa.\n `--muted-foreground` là bậc đã đo và dư biên ở mọi nền nó có thể đứng: 5,01:1 trên\n `--sidebar` · 4,78:1 trên `--sidebar-accent` · 5,23:1 trên `--background` (light);\n 5,35 / 4,86 / 5,70:1 (dark) ✓. (Dark của `/45` là 4,26 / 4,14:1 — chỉ light hỏng,\n nhưng bậc mờ theo alpha luôn lệch nhau như vậy giữa hai theme.) */}\n <ChevronsUpDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </button>\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {collapsed ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{currentWorkspace.name}</TooltipContent>\n </Tooltip>\n ) : (\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n )}\n\n <PopoverContent\n align={align}\n sideOffset={6}\n /*\n * `w-80` (320px), không phải `w-72` (288px). Ở 288px dòng phụ của hàng ĐANG CHỌN bị cắt —\n * đo được thiếu đúng 4px cho \"Quản trị viên · 6 thành viên\". Hàng đang chọn là hàng chật\n * nhất vì nó mang THÊM huy hiệu gói và dấu check, hai thứ mà các hàng khác không có.\n * Không nới vừa đủ 4px: tên vai trò và tên workspace là dữ liệu của app, dài ngắn tuỳ nơi\n * dùng — chọn mức có dư thay vì mức vừa khít với đúng bộ dữ liệu mẫu này.\n */\n className=\"w-80 p-0\"\n onOpenAutoFocus={(event) => {\n // Mặc định Radix focus vào khung panel; có ô tìm thì đưa con trỏ vào đó luôn.\n if (!searchRef.current) return;\n event.preventDefault();\n searchRef.current.focus();\n }}\n >\n {showList ? (\n <>\n {showSearch ? (\n <div className=\"relative border-b border-border p-2\">\n <Search className=\"pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={labels.workspaceSearchPlaceholder}\n aria-label={labels.workspaceSearchPlaceholder}\n className={cn(\n \"h-8 w-full rounded-md border border-input bg-background pl-7 pr-2 text-sm\",\n // Focus dùng `--primary-ink`, KHÔNG `--ring` cũng không `--gold-300`. Cả hai\n // bậc kia ở light đều là gold #E8B824 — chỉ 1,73:1 trên nền trang, tức lúc\n // focus ô tìm MẤT ranh giới (`--ring` chỉ hợp cho vòng focus `ring-*`, thứ có\n // luật riêng, không phải cho viền control). `--primary-ink` giữ đúng sắc gold\n // mà đạt 6,09–6,51:1 (light) · 10,80–12,67:1 (dark) — hợp đồng màu §3.\n \"placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\"\n )}\n />\n </div>\n ) : null}\n\n {/*\n CỐ Ý không dùng role=\"listbox\"/\"option\": bộ ARIA đó hứa với trình đọc màn hình\n một widget điều khiển bằng ↑/↓/Home/End + roving tabindex + aria-activedescendant,\n mà hành vi thật ở đây chỉ là Tab qua từng nút trong Popover (Popover được chọn\n thay DropdownMenu vì menu của Radix cướp phím gõ của ô tìm kiếm — xem chú thích\n component). Khai đúng cái mình làm còn hơn khai sang trọng rồi lừa người dùng:\n danh sách nút thường + `aria-current` cho mục đang chọn.\n */}\n {/*\n `flex flex-col gap-1`: các hàng là khối `rounded-md` có nền khi hover/đang chọn —\n dính sát nhau thì hai nền bo góc chạm nhau và đọc thành một mảng liền, không ra ba\n mục riêng. 4px khớp đúng `p-1` của khung, nên đệm trong và khoảng cách giữa bằng nhau.\n */}\n <div\n className=\"flex max-h-64 flex-col gap-1 overflow-y-auto scrollbar-thin p-1\"\n role=\"group\"\n aria-label={labels.workspace}\n >\n {filtered.length === 0 ? (\n <p className=\"px-2 py-6 text-center text-sm text-muted-foreground\">{labels.workspaceEmpty}</p>\n ) : (\n filtered.map((workspace) => {\n const isCurrent = workspace.id === currentWorkspace.id;\n return (\n <button\n key={workspace.id}\n type=\"button\"\n aria-current={isCurrent ? \"true\" : undefined}\n onClick={() => handleSelect(workspace)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-accent\",\n isCurrent && \"bg-accent/60\"\n )}\n >\n {/* Cùng cách xử lý với trigger ở trên: avatar cao bằng khối chữ hai dòng,\n và cả hai dòng `leading-tight` để dòng phụ dính vào tên chứ không trôi\n lửng lơ. Trigger và danh sách là cùng một thứ ở hai chỗ — để hai kiểu\n khác nhau là người dùng thấy giao diện \"nhảy\" khi mở popover. */}\n <WorkspaceAvatar workspace={workspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">\n {workspace.name}\n </span>\n {workspace.role || workspace.memberCount !== undefined ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">\n {[\n workspace.role,\n workspace.memberCount === undefined\n ? undefined\n : `${workspace.memberCount} ${labels.members}`\n ]\n .filter(Boolean)\n .join(\" · \")}\n </span>\n ) : null}\n </span>\n {workspace.plan ? (\n <Badge variant=\"secondary\" size=\"sm\" className=\"shrink-0\">\n {workspace.plan}\n </Badge>\n ) : null}\n {/* Ô check LUÔN chiếm chỗ, chỉ ẩn nội dung khi không phải mục đang chọn —\n `invisible` chứ không phải render có điều kiện. Render có điều kiện làm\n badge gói thuê bao của DÒNG ĐANG CHỌN bị đẩy lệch 24px so với các dòng\n khác, nên cột badge không thẳng hàng.\n Cùng thủ pháp với `OptionRow` của `Combobox` trong gói.\n\n Và vì sao KHÔNG bỏ hẳn dấu check: mục đang chọn khi đó chỉ còn `bg-accent/60`\n để nhận biết — ~1,03:1 với nền popover, tức không dấu hiệu nào đạt ngưỡng, và\n trạng thái lại đi bằng MỖI màu (WCAG 1.4.1). `aria-current` chỉ cứu trình đọc\n màn hình, không cứu người nhìn.\n\n KHÔNG dùng `text-app-accent` cho dấu check: accent mặc định (gold #E8B824)\n chỉ đạt 1,86:1 trên nền popover trắng — dưới ngưỡng 3:1 của WCAG 1.4.11 cho\n thành phần phi-văn-bản. `text-popover-foreground` đạt 18,9:1 (light)\n và 16,6:1 (dark). */}\n <Check\n aria-hidden=\"true\"\n className={cn(\"size-4 shrink-0 text-popover-foreground\", !isCurrent && \"invisible\")}\n />\n </button>\n );\n })\n )}\n </div>\n </>\n ) : (\n <div className=\"flex items-center gap-2 p-3\">\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{currentWorkspace.name}</span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {currentWorkspace.plan ?? currentWorkspace.slug}\n </span>\n </span>\n </div>\n )}\n\n {hasActions ? (\n <>\n <Separator />\n {/* `gap-1` — cùng 4px với danh sách workspace ở trên và với khoảng cách mục của họ\n menu (`DropdownMenuItem`…). Thiếu nó thì bốn hàng hành động dính liền thành một\n khối, và vùng `hover:bg-accent` của chúng cũng liền một mảng. */}\n <div className=\"flex flex-col gap-1 p-1\">\n {onCreateWorkspace ? (\n <ActionRow icon={Plus} label={labels.createWorkspace} onClick={closeThen(onCreateWorkspace)} />\n ) : null}\n {onWorkspaceSettings ? (\n <ActionRow\n icon={Settings}\n label={labels.workspaceSettings}\n onClick={closeThen(() => onWorkspaceSettings(currentWorkspace.id))}\n />\n ) : null}\n {onInviteMembers ? (\n <ActionRow\n icon={UserPlus}\n label={labels.inviteMembers}\n onClick={closeThen(() => onInviteMembers(currentWorkspace.id))}\n />\n ) : null}\n {onLogout ? (\n <ActionRow icon={LogOut} label={labels.logout} destructive onClick={closeThen(onLogout)} />\n ) : null}\n </div>\n </>\n ) : null}\n </PopoverContent>\n </Popover>\n );\n}\n\n/** Một dòng hành động trong panel (không phải mục chọn workspace). */\nfunction ActionRow({\n icon: Icon = Building2,\n label,\n onClick,\n destructive = false\n}: {\n icon?: typeof Building2;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n // Bậc ink chứ không `text-destructive`: dòng \"Đăng xuất\" là CHỮ trên nền `--popover`,\n // mà `--destructive` (token NỀN) chỉ được 3,53:1 trên `--popover` ở dark ✗. Ink đạt cả\n // hai: 7,15:1 (light) · 6,42:1 (dark). Icon ở `opacity-70` là thành phần phi-văn-bản\n // (ngưỡng 3:1): 3,69:1 / 3,73:1 lúc nghỉ, và 3,37:1 / 3,62:1 trên nền hover\n // `bg-destructive/10` — đó mới là tổ hợp chặt nhất, vẫn ✓. Chữ lúc hover: 6,18:1 / 5,94:1.\n destructive ? \"text-destructive-ink hover:bg-destructive/10\" : \"text-foreground hover:bg-accent\"\n )}\n >\n <Icon className=\"size-4 shrink-0 opacity-70\" />\n <span className=\"truncate\">{label}</span>\n </button>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "0.3.3",
3
+ "version": "0.5.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
@@ -54,7 +54,8 @@
54
54
  Ghi ra để không ai tưởng là lỗi bỏ sót và "sửa" lại lần nữa — chỗ để sửa là bảng tương phản
55
55
  cao, không phải bảng mặc định. Cột cuối là ngưỡng WCAG mà khoản đó không đạt:
56
56
 
57
- viền control (--input / --control-edge) trên nền trang 1,16:1 (1.4.11 · 3:1)
57
+ viền ô nhập (--input / --control-edge) trên nền trang 1,16:1 (không chịu ngưỡng)
58
+ viền checkbox/radio (--choice-edge) trên nền trang 2,99:1 (1.4.11 · 3:1)
58
59
  rãnh Switch BẬT (--control-on) ⇄ nền trang và ⇄ núm 1,86:1 (1.4.11 · 3:1)
59
60
  gạch active của Tabs, viền focus ô nhập (gold) 1,86:1 (1.4.11 · 3:1)
60
61
  --muted-foreground trên --muted 3,58:1 (1.4.3 · 4,5:1)
@@ -218,6 +219,16 @@
218
219
  --secondary: #ede8d8;
219
220
  --secondary-foreground: #0e1116;
220
221
  --muted: #f7f7f8;
222
+ /* Sọc xen kẽ của bảng (`<Table striped>`, `<DataTable striped>`) — ĐỤC, cố ý.
223
+ Đúng bằng --muted phủ 30% lên --card, tức y hệt `bg-muted/30` mà bản trước dùng, chỉ khác là
224
+ đã hợp thành sẵn. PHẢI đục vì ô cột GHIM lấy `bg-inherit` từ hàng: nền hàng còn alpha thì ô
225
+ ghim trong mờ và chữ của các cột đang cuộn bên dưới lộ xuyên qua.
226
+ ⚠ Đổi --muted hoặc --card thì tính lại: color-mix không dùng được ở đây vì cổng tương phản
227
+ đọc token bằng hex (cùng lý do các bậc --*-ink đã đóng băng). */
228
+ --table-row-stripe: #fdfdfd;
229
+ /* Hàng đang rê chuột — cũng ĐỤC, cùng một lý do: = --muted phủ 50% lên --card (bản trước là
230
+ `hover:bg-muted/50`). Một màu chung cho cả hàng lẻ lẫn hàng sọc, đúng lối mọi bảng vẫn làm. */
231
+ --table-row-hover: #fbfbfc;
221
232
  /* Bậc chữ phụ (placeholder, mô tả, nhãn trục biểu đồ). Giá trị của comitor-ds, bằng đúng
222
233
  --ash-500. Số đo còn tồn (dưới AA 4,5:1 — ĐÃ BIẾT VÀ CHẤP NHẬN, xem khối NGUỒN SỰ THẬT ở
223
234
  đầu file): 3,83:1 trên --background · 3,58:1 trên --muted · 3,67:1 trên --sidebar. */
@@ -250,10 +261,20 @@
250
261
  light #EDEEF1 trên trắng = 1,16:1 · dark #1E2128 trên #0E1116 = 1,17:1
251
262
  dark #1E2128 trên --card #16181D = 1,10:1 · --sidebar-border/--sidebar = 1,11:1
252
263
 
253
- --control-edge (= --input) VIỀN CONTROL. TỪ 0.3.0 token này TRỎ VỀ --input, tức bằng
254
- đúng bậc trang trí của comitor-ds (#EDEEF1 light · #1E2128 dark). Bản duyệt vẽ viền ô
255
- nhập ở bậc đó và bản duyệt là nguồn sự thật; số đo còn tồn là 1,16:1, dưới 3:1 của
256
- WCAG 1.4.11 — xem khối NGUỒN SỰ THẬT ở đầu file.
264
+ --control-edge (= --input) VIỀN Ô NHẬP. Input/Textarea/Select/Combobox/OTP, ô tìm của
265
+ WorkspaceSwitcher, Button variant outline/secondary, Badge variant outline, rãnh Slider.
266
+ Ở light giữ đúng bậc của comitor-ds (#EDEEF1 = 1,16:1) — ô nhập còn được nhận ra bằng
267
+ kích thước, nhãn và placeholder nên viền nhạt vẫn đọc được, và bản duyệt là nguồn sự thật.
268
+ Ở dark thì KHÔNG giữ được: #1E2128 trên --card chỉ 1,10:1, ô nhập gần như tan vào nền.
269
+ Bảng mặc định dùng #434A5A (2,00:1) — rõ hẳn mà chưa nặng như #7E8290 (4,64:1) của bảng
270
+ tương phản cao.
271
+
272
+ --choice-edge VIỀN CHECKBOX / RADIO. Tách khỏi --control-edge vì ở đây viền
273
+ là TOÀN BỘ control khi chưa chọn: không có nhãn nằm trong, không có placeholder, không
274
+ có nền — mất viền là mất luôn ô. Vì thế nó chịu ngưỡng 1.4.11 còn viền ô nhập thì không.
275
+ #959597 (2,99:1) ở light · #5B6479 (3,00:1 trên --card) ở dark. Vẫn dưới 3:1 một chút,
276
+ cố ý: mức "nhìn rõ mà chưa nặng như bảng tương phản cao". Ở bảng tương phản cao nó trỏ
277
+ thẳng về --input nên cả hai loại viền cùng lên một bậc.
257
278
  Dùng cho: Input/Textarea/Select/Combobox, ô Checkbox chưa chọn, viền Radio, rãnh
258
279
  Slider, Button variant outline/secondary, Badge variant outline, ô tìm của
259
280
  WorkspaceSwitcher. (Rãnh Switch khi TẮT KHÔNG đi qua đây — comitor-ds dùng
@@ -267,7 +288,8 @@
267
288
  Control nào chỉ viết `border` mà không nêu màu sẽ lặng lẽ nhận bậc 1,16:1 — luôn phải nêu
268
289
  `border-input` / `border-control-edge` tường minh. Bảng đầy đủ: docs/hop-dong-mau.md §3. */
269
290
  --border: #edeef1;
270
- /* Viền ô nhập — giá trị của comitor-ds, cùng bậc với --border. Khối .dark khai lại (#1E2128). */
291
+ /* Viền ô nhập — giá trị của comitor-ds, cùng bậc với --border. Khối .dark KHÔNG giữ được bậc
292
+ này (xem khối "HAI VAI TRÒ" ngay trên) nên khai lại thành #434A5A. */
271
293
  --input: #edeef1;
272
294
  /* LỚP TÔ của ô nhập ở dark — comitor-ds viết `dark:bg-input/30`, và nó không phải trang trí:
273
295
  ở dark viền --input (#1E2128) trên --card (#16181D) chỉ 1,10:1, nên lớp tô mờ này là thứ
@@ -280,13 +302,19 @@
280
302
 
281
303
  /* Trỏ về --input, không còn là một bậc xám riêng. Token giữ lại vì component trong gói đang
282
304
  viết `border-control-edge` và vì app có thể ghi đè MỘT chỗ này để làm đậm viền control mà
283
- không đụng đường kẻ trang trí (--border). Số đo còn tồn: 1,16:1 light · 1,17:1 dark. */
305
+ không đụng đường kẻ trang trí (--border). Số đo: 1,16:1 light · 2,00:1 dark (trên --card). */
284
306
  --control-edge: var(--input);
285
- /* Hover/active của viền control — đúng cặp của comitor-ds: `hover:border-gray-300` (ash-300,
307
+ /* Hover/active của viền Ô NHẬP — đúng cặp của comitor-ds: `hover:border-gray-300` (ash-300,
286
308
  #C8CBD3) ở light và `dark:hover:border-gray-500` (ash-500, #7E8290) ở dark. Token hoá để
287
- Input/Select/Combobox/Checkbox khỏi chép lệch. */
309
+ Input/Select/Combobox khỏi chép lệch. Cả hai bậc đều ĐẬM HƠN lúc nghỉ, đúng chiều. */
288
310
  --control-edge-strong: var(--ash-300);
289
311
 
312
+ /* Viền checkbox/radio — xem khối "HAI VAI TRÒ" ở trên để biết vì sao nó KHÔNG dùng chung
313
+ token với ô nhập. Hover phải đậm hơn 2,99:1 nên ash-300 (1,62:1) không dùng được ở đây;
314
+ ash-500 đạt 3,83:1 (light) và 4,64:1 (dark). */
315
+ --choice-edge: #959597;
316
+ --choice-edge-strong: var(--ash-500);
317
+
290
318
  /* ─ CONTROL Ở TRẠNG THÁI BẬT / ĐÃ CHỌN ─
291
319
  Rãnh `Switch` khi BẬT, ô `Checkbox` đã chọn, chấm `Radio`, đoạn đã chọn của `Slider`.
292
320
 
@@ -462,6 +490,9 @@
462
490
  --secondary: #1e2128;
463
491
  --secondary-foreground: #f7f7f8;
464
492
  --muted: #1e2128;
493
+ /* = --muted 30% / 50% trên --card (#1E2128 trên #16181D). Xem chú thích ở bản light. */
494
+ --table-row-stripe: #181b20;
495
+ --table-row-hover: #1a1d23;
465
496
  /* Cùng một giá trị với bản light — comitor-ds không khai lại token này trong .dark. Số đo còn
466
497
  tồn (dưới AA, ĐÃ BIẾT VÀ CHẤP NHẬN): 4,21:1 trên --muted · 4,73:1 trên --card. Xem khối
467
498
  NGUỒN SỰ THẬT ở đầu file. */
@@ -475,8 +506,16 @@
475
506
  /* Viền ô nhập bản dark của comitor-ds. --control-edge trỏ vào --input nên tự theo; --control-on
476
507
  và --control-knob cũng không khai lại (chúng trỏ vào --primary — đã có bản dark ngay trên —
477
508
  và vào trắng cố định, đúng như núm Switch của comitor-ds). */
478
- --input: #1e2128;
509
+ /* KHÔNG phải #1E2128 của comitor-ds: bậc đó chỉ 1,10:1 trên --card, ô nhập gần như tan vào nền
510
+ ở dark (ở light bậc tương đương vẫn đọc được — mắt bù được trên nền sáng, trên nền tối thì
511
+ không). #434A5A = 2,00:1, còn cách xa #7E8290 (4,64:1) của bảng tương phản cao. */
512
+ --input: #434a5a;
513
+ /* Giữ nguyên cách của comitor-ds (`dark:bg-input/30`). Placeholder trên lớp tô này đo được
514
+ 3,90:1 — khá hơn 3,58:1 mà --muted-foreground trên --muted vốn đã chấp nhận. */
479
515
  --input-fill: color-mix(in srgb, var(--input) 30%, transparent);
516
+ /* Viền checkbox/radio bản dark — 3,00:1 trên --card. --choice-edge-strong không khai lại vì nó
517
+ trỏ vào ash-500, đạt 4,64:1 ở đây. */
518
+ --choice-edge: #5b6479;
480
519
  /* Hover viền control bản dark của comitor-ds là dark:hover:border-gray-500, tức ash-500. */
481
520
  --control-edge-strong: var(--ash-500);
482
521
  /* Rãnh Switch khi TẮT bản dark của comitor-ds (dark:bg-gray-700). --slider-track không khai lại
@@ -554,19 +593,50 @@
554
593
  /* Ngược chiều bảng duyệt: color-mix(--destructive, --ink 15%) — chữ trắng 6,19:1 */
555
594
  --destructive-hover: #ad3535;
556
595
  --destructive-ink: var(--red-ink);
557
- /* Viền control: 3,83:1 trên nền trắng (bảng duyệt #EDEEF1 chỉ 1,16:1).
558
- --control-edge trỏ vào --input nên tự theo. */
596
+ /* Viền control: 3,83:1 trên nền trắng — đủ chuẩn 1.4.11, trong khi bảng mặc định để viền Ô NHẬP
597
+ ở bậc nhạt của bản duyệt (1,16:1). --control-edge trỏ vào --input nên tự theo. */
559
598
  --input: #7e8290;
599
+ /* ĐƯỜNG KẺ TRANG TRÍ cũng lên theo — ở bảng mặc định nó cố ý nhạt (1,16:1) vì không mang thông
600
+ tin, nhưng bật "tương phản cao" mà chỉ viền ô nhập đậm lên còn khung Card, đường kẻ bảng và
601
+ vạch ngăn menu vẫn tàng hình thì chế độ này nhìn nửa vời.
602
+ 2,49:1 — hơn gấp đôi bậc cũ, nhưng CỐ Ý thấp hơn viền control (3,83:1) một bậc:
603
+ · --border viền MỌI thẻ, mọi dòng, mọi dấu phân cách. Kéo nó lên ngang viền control là mất
604
+ phân cấp "cái này thao tác được" ⇄ "cái này chỉ là đường kẻ", và cả trang thành lưới ô.
605
+ · Giữ nó dưới 3:1 cũng giữ đúng tiền đề của hợp đồng: --border là token KHÔNG chịu ngưỡng.
606
+ Cổng tương phản dựa vào đúng tiền đề đó để bắt lỗi "vẽ hình control bằng token trang trí"
607
+ (4 ca trong `test:cong-a11y`) — nâng --border lên 3,83:1 là bốn ca đó hết bắt được. */
608
+ --border: #a4a4a6;
609
+ --sidebar-border: #a4a4a6;
610
+ /* Ở bảng này viền ô nhập và viền checkbox/radio CÙNG một bậc — trỏ bằng var() để đổi --input
611
+ một chỗ là cả hai theo, không phải nhớ sửa hai số. */
612
+ --choice-edge: var(--input);
613
+ --choice-edge-strong: var(--control-edge-strong);
560
614
  /* Viền đã đủ đậm ở cả hai theme nên bỏ hẳn lớp tô — nó chỉ làm placeholder khó đọc. */
561
615
  --input-fill: transparent;
562
616
  --control-edge-strong: var(--ash-700);
563
- /* Control BẬT/ĐÃ CHỌN: rãnh ⇄ nền 6,51:1 và rãnh ⇄ núm 6,51:1 (bảng duyệt: 1,86 / 1,86) */
564
- --control-on: var(--gold-500);
617
+ /* Control BẬT/ĐÃ CHỌN: gold ĐẬM, KHÔNG phải gold-500.
618
+ gold-500 (#7A5800) đạt 6,51:1 nhưng nó đã rời khỏi sắc gold — mắt đọc ra nâu-ô-liu xỉn, mà
619
+ "tương phản cao" không có nghĩa là đổi màu nhận diện.
620
+ #B48700 = gold-400 tối đi 2%. Vì sao không dùng thẳng gold-400 (#B88A00): nó đạt 3,15:1 trên
621
+ nền trang nhưng chỉ 2,94:1 trên --muted, mà control có thể nằm trên nền --muted — cổng tương
622
+ phản bắt đúng chỗ đó. Bậc này đạt ở MỌI nền sáng: 3,28:1 (--background/--card) · 3,06:1
623
+ (--muted) · 3,14:1 (--sidebar); ⇄ núm trắng 3,28:1. Bảng duyệt chỉ 1,86:1 ở cả hai phép đo.
624
+ Dấu check quay về MỰC (5,77:1 trên nền gold mới) — cùng cách vẽ với bảng mặc định, nên hai
625
+ bảng giờ chỉ khác nhau ở ĐỘ ĐẬM của gold chứ không khác ở cách vẽ control.
626
+ ⚠ --control-knob (núm Switch) vẫn phải khai lại là TRẮNG: nó là hình tròn đặc trên rãnh, đo
627
+ 3,28:1 — đạt ngưỡng phi-văn-bản; để nó thành mực thì núm chìm vào rãnh. */
628
+ --control-on: #b48700;
565
629
  --control-knob: var(--background);
566
- --control-check: var(--control-knob);
630
+ --control-check: var(--ink);
567
631
  --switch-track: var(--control-edge);
568
632
  --slider-track: var(--control-edge);
569
- /* Brand khi phải ĐI LÊN nền trang: viền focus, gạch active của Tabs, ngày chọn của Calendar */
633
+ /* Brand khi phải ĐI LÊN nền trang: viền focus, gạch active của Tabs, ngày chọn của Calendar,
634
+ thanh Progress, nhãn trạng thái, link.
635
+ ⚠ ĐỪNG kéo token này sáng theo --control-on. Đã thử #B48700 và cổng bắt đúng: nó là CHỮ ở
636
+ `status-config.ts` (nhãn trạng thái) và `[&>a:hover]` của Field/Item/Empty (ngưỡng 4,5:1 —
637
+ ra 3,06:1 ✗), còn ở `StatCard` nó là icon đứng trên tint `bg-primary/20` của chính nó nên
638
+ tụt xuống 2,73:1, thủng cả ngưỡng 3:1. --control-on thì chỉ đứng trên NỀN TRANG nên mới nới
639
+ sáng được. Hai token, hai vai trò — đúng hợp đồng màu §1. */
570
640
  --primary-ink: var(--gold-500);
571
641
  --link-ink: var(--gold-500);
572
642
  --link-ink-hover: var(--gold-600);
@@ -576,6 +646,11 @@
576
646
  /* Sáng hơn bản light: chữ phụ ở dark hay nằm trên --card/--muted chứ ít khi trên nền trang.
577
647
  5,70:1 trên --background · 5,35:1 trên --card · 4,86:1 trên --muted */
578
648
  --muted-foreground: #888d9b;
649
+ /* Đường kẻ trang trí bản dark của bảng này — #A4A4A6 của khối light là XÁM SÁNG, đặt lên nền
650
+ #16181D thì thành đường kẻ chói. #50586B cho ra đúng 2,49:1 như bản light, tức cùng một mức
651
+ "thấy rõ mà chưa ngang viền control". */
652
+ --border: #50586b;
653
+ --sidebar-border: #50586b;
579
654
  /* Viền control ghim MỘT giá trị cho cả hai theme: 4,94:1 trên nền #0E1116 */
580
655
  --input: #7e8290;
581
656
  --control-edge-strong: var(--ash-300);
@@ -624,6 +699,8 @@
624
699
  --color-secondary: var(--secondary);
625
700
  --color-secondary-foreground: var(--secondary-foreground);
626
701
  --color-muted: var(--muted);
702
+ --color-table-row-stripe: var(--table-row-stripe);
703
+ --color-table-row-hover: var(--table-row-hover);
627
704
  --color-muted-foreground: var(--muted-foreground);
628
705
  --color-accent: var(--accent);
629
706
  --color-accent-foreground: var(--accent-foreground);
@@ -640,6 +717,8 @@
640
717
  --color-input-fill: var(--input-fill);
641
718
  --color-control-edge: var(--control-edge);
642
719
  --color-control-edge-strong: var(--control-edge-strong);
720
+ --color-choice-edge: var(--choice-edge);
721
+ --color-choice-edge-strong: var(--choice-edge-strong);
643
722
  --color-control-on: var(--control-on);
644
723
  --color-control-knob: var(--control-knob);
645
724
  --color-control-check: var(--control-check);