@comitor/ui 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +303 -43
  2. package/dist/chart/chart.d.ts +35 -3
  3. package/dist/chart/chart.js.map +1 -1
  4. package/dist/chart/comitor-chart.d.ts +16 -3
  5. package/dist/chart/comitor-chart.js.map +1 -1
  6. package/dist/chart/index.d.ts +2 -2
  7. package/dist/components/composite/avatar-group.js +1 -1
  8. package/dist/components/composite/avatar-group.js.map +1 -1
  9. package/dist/components/composite/combobox.d.ts +9 -2
  10. package/dist/components/composite/combobox.js +9 -2
  11. package/dist/components/composite/combobox.js.map +1 -1
  12. package/dist/components/composite/confirm-dialog.js +1 -1
  13. package/dist/components/composite/confirm-dialog.js.map +1 -1
  14. package/dist/components/composite/copy-button.js +1 -1
  15. package/dist/components/composite/copy-button.js.map +1 -1
  16. package/dist/components/composite/data-table.d.ts +17 -8
  17. package/dist/components/composite/data-table.js +8 -8
  18. package/dist/components/composite/data-table.js.map +1 -1
  19. package/dist/components/composite/filter-chips.js +6 -5
  20. package/dist/components/composite/filter-chips.js.map +1 -1
  21. package/dist/components/composite/form-field.js +3 -1
  22. package/dist/components/composite/form-field.js.map +1 -1
  23. package/dist/components/composite/form-section.js +5 -5
  24. package/dist/components/composite/form-section.js.map +1 -1
  25. package/dist/components/composite/inline-edit.js +1 -1
  26. package/dist/components/composite/inline-edit.js.map +1 -1
  27. package/dist/components/composite/keyboard-hint.d.ts +16 -0
  28. package/dist/components/composite/keyboard-hint.js +8 -6
  29. package/dist/components/composite/keyboard-hint.js.map +1 -1
  30. package/dist/components/composite/letter-avatar.d.ts +10 -18
  31. package/dist/components/composite/letter-avatar.js +6 -17
  32. package/dist/components/composite/letter-avatar.js.map +1 -1
  33. package/dist/components/composite/search-filter-bar.d.ts +41 -3
  34. package/dist/components/composite/search-filter-bar.js +27 -15
  35. package/dist/components/composite/search-filter-bar.js.map +1 -1
  36. package/dist/components/composite/stat-card.d.ts +16 -1
  37. package/dist/components/composite/stat-card.js +16 -9
  38. package/dist/components/composite/stat-card.js.map +1 -1
  39. package/dist/components/composite/status-config.d.ts +69 -16
  40. package/dist/components/composite/status-config.js +10 -10
  41. package/dist/components/composite/status-config.js.map +1 -1
  42. package/dist/components/composite/status-pill.js +5 -1
  43. package/dist/components/composite/status-pill.js.map +1 -1
  44. package/dist/components/composite/table-pagination.d.ts +46 -4
  45. package/dist/components/composite/table-pagination.js +27 -24
  46. package/dist/components/composite/table-pagination.js.map +1 -1
  47. package/dist/components/composite/text-utils.d.ts +12 -11
  48. package/dist/components/composite/text-utils.js +7 -16
  49. package/dist/components/composite/text-utils.js.map +1 -1
  50. package/dist/components/composite/toast.js +16 -6
  51. package/dist/components/composite/toast.js.map +1 -1
  52. package/dist/components/ui/alert.js +1 -1
  53. package/dist/components/ui/alert.js.map +1 -1
  54. package/dist/components/ui/badge.js +4 -4
  55. package/dist/components/ui/badge.js.map +1 -1
  56. package/dist/components/ui/button.js +3 -3
  57. package/dist/components/ui/button.js.map +1 -1
  58. package/dist/components/ui/calendar.d.ts +41 -0
  59. package/dist/components/ui/calendar.js +1 -1
  60. package/dist/components/ui/calendar.js.map +1 -1
  61. package/dist/components/ui/card.d.ts +35 -2
  62. package/dist/components/ui/card.js +17 -3
  63. package/dist/components/ui/card.js.map +1 -1
  64. package/dist/components/ui/checkbox.js +3 -3
  65. package/dist/components/ui/checkbox.js.map +1 -1
  66. package/dist/components/ui/command.js +1 -1
  67. package/dist/components/ui/command.js.map +1 -1
  68. package/dist/components/ui/context-menu.js +1 -1
  69. package/dist/components/ui/context-menu.js.map +1 -1
  70. package/dist/components/ui/drawer.js +1 -1
  71. package/dist/components/ui/drawer.js.map +1 -1
  72. package/dist/components/ui/dropdown-menu.js +1 -1
  73. package/dist/components/ui/dropdown-menu.js.map +1 -1
  74. package/dist/components/ui/empty.js +1 -1
  75. package/dist/components/ui/empty.js.map +1 -1
  76. package/dist/components/ui/field.js +4 -4
  77. package/dist/components/ui/field.js.map +1 -1
  78. package/dist/components/ui/form.js +8 -2
  79. package/dist/components/ui/form.js.map +1 -1
  80. package/dist/components/ui/input-group.js +3 -3
  81. package/dist/components/ui/input-group.js.map +1 -1
  82. package/dist/components/ui/input-otp.js +1 -1
  83. package/dist/components/ui/input-otp.js.map +1 -1
  84. package/dist/components/ui/input.js +3 -3
  85. package/dist/components/ui/input.js.map +1 -1
  86. package/dist/components/ui/item.js +1 -1
  87. package/dist/components/ui/item.js.map +1 -1
  88. package/dist/components/ui/menubar.js +1 -1
  89. package/dist/components/ui/menubar.js.map +1 -1
  90. package/dist/components/ui/navigation-menu.js +1 -1
  91. package/dist/components/ui/navigation-menu.js.map +1 -1
  92. package/dist/components/ui/pagination.js +1 -1
  93. package/dist/components/ui/pagination.js.map +1 -1
  94. package/dist/components/ui/progress.d.ts +26 -0
  95. package/dist/components/ui/progress.js +5 -2
  96. package/dist/components/ui/progress.js.map +1 -1
  97. package/dist/components/ui/radio-group.js +2 -2
  98. package/dist/components/ui/radio-group.js.map +1 -1
  99. package/dist/components/ui/resizable.d.ts +14 -0
  100. package/dist/components/ui/resizable.js +2 -2
  101. package/dist/components/ui/resizable.js.map +1 -1
  102. package/dist/components/ui/scroll-area.js +1 -1
  103. package/dist/components/ui/scroll-area.js.map +1 -1
  104. package/dist/components/ui/select.d.ts +33 -0
  105. package/dist/components/ui/select.js +3 -3
  106. package/dist/components/ui/select.js.map +1 -1
  107. package/dist/components/ui/slider.d.ts +19 -0
  108. package/dist/components/ui/slider.js +3 -3
  109. package/dist/components/ui/slider.js.map +1 -1
  110. package/dist/components/ui/sonner.d.ts +21 -3
  111. package/dist/components/ui/sonner.js +1 -2
  112. package/dist/components/ui/sonner.js.map +1 -1
  113. package/dist/components/ui/switch.js +2 -2
  114. package/dist/components/ui/switch.js.map +1 -1
  115. package/dist/components/ui/table.d.ts +16 -0
  116. package/dist/components/ui/table.js +1 -1
  117. package/dist/components/ui/table.js.map +1 -1
  118. package/dist/components/ui/tabs.js +1 -1
  119. package/dist/components/ui/tabs.js.map +1 -1
  120. package/dist/components/ui/textarea.js +1 -1
  121. package/dist/components/ui/textarea.js.map +1 -1
  122. package/dist/components/ui/toggle.js +1 -1
  123. package/dist/components/ui/toggle.js.map +1 -1
  124. package/dist/index.d.ts +6 -5
  125. package/dist/index.js +17 -2
  126. package/dist/index.js.map +1 -1
  127. package/dist/lib/avatar-tone.d.ts +126 -0
  128. package/dist/lib/avatar-tone.js +68 -0
  129. package/dist/lib/avatar-tone.js.map +1 -0
  130. package/dist/shell/app-header.js +12 -2
  131. package/dist/shell/app-header.js.map +1 -1
  132. package/dist/shell/app-launcher.js +2 -2
  133. package/dist/shell/app-launcher.js.map +1 -1
  134. package/dist/shell/app-shell.d.ts +2 -0
  135. package/dist/shell/index.d.ts +3 -3
  136. package/dist/shell/shell-context.d.ts +29 -8
  137. package/dist/shell/shell-context.js +6 -20
  138. package/dist/shell/shell-context.js.map +1 -1
  139. package/dist/shell/sidebar-group.js +5 -3
  140. package/dist/shell/sidebar-group.js.map +1 -1
  141. package/dist/shell/sidebar-menu-item.js +15 -7
  142. package/dist/shell/sidebar-menu-item.js.map +1 -1
  143. package/dist/shell/types.d.ts +17 -1
  144. package/dist/shell/types.js.map +1 -1
  145. package/dist/shell/user-menu.js +3 -2
  146. package/dist/shell/user-menu.js.map +1 -1
  147. package/dist/shell/utils.d.ts +36 -24
  148. package/dist/shell/utils.js +55 -20
  149. package/dist/shell/utils.js.map +1 -1
  150. package/dist/shell/workspace-switcher.js +23 -8
  151. package/dist/shell/workspace-switcher.js.map +1 -1
  152. package/dist/tokens.d.ts +165 -17
  153. package/dist/tokens.js +122 -25
  154. package/dist/tokens.js.map +1 -1
  155. package/package.json +8 -5
  156. package/styles.css +312 -22
@@ -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 { 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 { getAvatarToneClass, 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\", getAvatarToneClass(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 <span className=\"truncate text-[11px] text-sidebar-foreground/55\">{currentWorkspace.plan}</span>\n ) : null}\n </span>\n <ChevronsUpDown className=\"size-3.5 shrink-0 text-sidebar-foreground/45\" />\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 // `border-ring` chứ không `border-gold-300`: token semantic đổi theo theme\n // (gold-300 giữ nguyên ở dark), và đây là quy tắc màu của package.\n \"placeholder:text-muted-foreground focus:border-ring 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 destructive ? \"text-destructive 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 <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":[]}
package/dist/tokens.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { AvatarToneColors } from './lib/avatar-tone.js';
2
+ export { getAvatarToneIndex } from './lib/avatar-tone.js';
3
+
1
4
  /**
2
5
  * Design token dạng TS object — dành cho nơi KHÔNG có Tailwind: canvas, biểu đồ tuỳ biến,
3
6
  * email HTML, xuất PDF, (sau này) React Native.
@@ -7,7 +10,18 @@
7
10
  *
8
11
  * Trong môi trường có Tailwind, LUÔN dùng class token (`bg-primary`, `text-ash-500`),
9
12
  * đừng đọc hex từ đây rồi nhét vào style inline: làm vậy là mất dark mode.
13
+ *
14
+ * Hợp đồng ba vai trò của màu (giống hệt `styles.css`; bảng đầy đủ ở docs/hop-dong-mau.md),
15
+ * đọc tên là biết dùng ở đâu:
16
+ * `x` → tô NỀN (fill đặc hoặc tint /5–/20 — trần chung an toàn cho mọi tone)
17
+ * `xForeground` → chữ/icon NẰM TRÊN nền `x` ĐẶC — đo với `x`, ngưỡng 4,5:1
18
+ * `xInk` → chính màu đó khi ĐI LÊN nền trang hoặc lên tint của chính nó (chữ, icon
19
+ * rời, viền, vạch active) — đo với nền đó
20
+ *
21
+ * Cố ý KHÔNG mirror (xem docs/quyet-dinh-tich-hop.md §21): `--elevation-*` (chuỗi box-shadow chỉ
22
+ * có nghĩa trong CSS), `--app-accent*` (giá trị thuộc về APP, không phải gói), `--font-*`.
10
23
  */
24
+
11
25
  /** Thang GOLD 7 bậc — trục màu chính của brand Comitor. */
12
26
  declare const gold: {
13
27
  readonly 50: "#FBF5DC";
@@ -40,8 +54,12 @@ declare const ash: {
40
54
  readonly 900: "#16181D";
41
55
  };
42
56
  /**
43
- * Màu phụ. Bản `dark` được nâng sáng để đạt tương phản AA khi làm CHỮ trên nền #0E1116 —
44
- * bản `light` quá tối cho việc đó.
57
+ * Màu phụ — vai trò TÔ NỀN (fill đặc hoặc tint). Bản `dark` được nâng sáng vì trên nền #0E1116
58
+ * bản `light` chìm nghỉm.
59
+ *
60
+ * ⚠ ĐỪNG dùng bảng này làm màu CHỮ ở light: nó được chọn để tô nền nên quá nhạt — dùng
61
+ * `accentInk` ngay dưới. `accent.dark.navy` giữ nguyên #0F1E3D chỉ để mirror đúng `styles.css`
62
+ * (khối `.dark` không override `--navy`); vai trò navy ở dark do `accentInk.dark.navy` gánh.
45
63
  */
46
64
  declare const accent: {
47
65
  readonly light: {
@@ -59,6 +77,65 @@ declare const accent: {
59
77
  readonly yellow: "#FACC15";
60
78
  };
61
79
  };
80
+ /**
81
+ * Bậc INK của màu phụ — mirror `--teal-ink` / `--green-ink` / `--red-ink` / `--yellow-ink` /
82
+ * `--navy-ink`. Đây là màu phụ khi nó làm **CHỮ/ICON/VIỀN trên nền trang** (hoặc trên tint
83
+ * /5–/20 của chính nó), tức vai trò NGƯỢC với `accent`.
84
+ *
85
+ * Hợp đồng đặt tên của cả gói: `--x` tô nền · `--x-foreground` là chữ NẰM TRÊN nền `--x` ·
86
+ * `--x-ink` là chính màu đó ĐI LÊN nền trang.
87
+ *
88
+ * Bản `light` là bậc tối đã đóng băng (kết quả của `color-mix(in srgb, --<màu>, --ink 30/40%)`
89
+ * mà trước đây mỗi component tự tính lấy); bản `dark` chính là `accent.dark` vì bên đó màu phụ
90
+ * đã đủ sáng để làm chữ. Đo trên tint 15% của chính nó — nền thật mà chữ initials avatar và
91
+ * badge tonal đứng lên: teal 5,54 · green 5,60 · red 5,83:1 ở light (bản chưa có ink: 3,44 /
92
+ * 2,69 / 3,57:1 ✗); 7,12 / 7,44 / 5,18:1 ở dark.
93
+ */
94
+ declare const accentInk: {
95
+ readonly light: {
96
+ readonly navy: "#0F1E3D";
97
+ readonly teal: "#1A6668";
98
+ readonly red: "#9A3537";
99
+ readonly green: "#216A48";
100
+ /** = gold[500]: vàng không có bậc tối riêng, mượn thang gold cùng sắc. */
101
+ readonly yellow: "#7A5800";
102
+ };
103
+ readonly dark: {
104
+ /** navy nguyên bản chìm vào nền dark (1,15:1) → bậc sáng riêng; cũng là `--chart-3` ở dark. */
105
+ readonly navy: "#7EA2E8";
106
+ readonly teal: "#2DD4BF";
107
+ readonly red: "#F87171";
108
+ readonly green: "#4ADE80";
109
+ readonly yellow: "#FACC15";
110
+ };
111
+ };
112
+ /**
113
+ * Bậc FOREGROUND của màu phụ — mirror `--teal-foreground` / `--green-foreground` /
114
+ * `--yellow-foreground` / `--navy-foreground`. Đây là chữ/icon nằm **TRÊN nền `accent.*` ĐẶC**,
115
+ * tức vai trò thứ ba, khác hẳn `accentInk` (màu đó đi lên nền TRANG).
116
+ *
117
+ * Cả ba sắc teal/green/yellow đều sáng ở light và còn sáng hơn ở dark → chữ là MỰC ở cả hai
118
+ * theme, một giá trị đủ dùng: teal 4,56:1 (light) / 10,16:1 (dark) · green 6,04 / 10,85 ·
119
+ * yellow 11,92 / 12,35. Navy thì ngược lại — chữ TRẮNG, 16,49:1 ở cả hai theme.
120
+ *
121
+ * ⚠ CỐ Ý KHÔNG có `red`: nền `accent.light.red` #D64545 đặc không mang được chữ ở light — trắng
122
+ * 4,38:1 ✗ và mực 4,32:1 ✗. Nền đỏ đặc có chữ phải dùng `status.*.destructive` (#C93B3B) cùng
123
+ * `destructiveForeground` = 5,03:1 ✓. `accent.*.red` chỉ để tô TINT và làm màu chart.
124
+ */
125
+ declare const accentForeground: {
126
+ readonly light: {
127
+ readonly navy: "#FFFFFF";
128
+ readonly teal: "#0E1116";
129
+ readonly green: "#0E1116";
130
+ readonly yellow: "#0E1116";
131
+ };
132
+ readonly dark: {
133
+ readonly navy: "#FFFFFF";
134
+ readonly teal: "#0E1116";
135
+ readonly green: "#0E1116";
136
+ readonly yellow: "#0E1116";
137
+ };
138
+ };
62
139
  /** Semantic token của shadcn — mirror khối `:root` / `.dark` trong styles.css. */
63
140
  declare const semantic: {
64
141
  readonly light: {
@@ -70,17 +147,43 @@ declare const semantic: {
70
147
  readonly popoverForeground: "#0E1116";
71
148
  readonly primary: "#E8B824";
72
149
  readonly primaryForeground: "#0E1116";
150
+ /** Bậc INK của brand — gold khi phải ĐI LÊN nền trang (rãnh Switch khi BẬT, ô Checkbox đã
151
+ * chọn). `primary` #E8B824 chỉ 1,86:1 với nền trắng ✗; bậc này = `gold[500]` = 6,51:1 ✓. */
152
+ readonly primaryInk: "#7A5800";
73
153
  readonly secondary: "#EDE8D8";
74
154
  readonly secondaryForeground: "#0E1116";
75
155
  readonly muted: "#F7F7F8";
76
- readonly mutedForeground: "#7E8290";
156
+ /** Bậc chữ phụ — là CHỮ nên ngưỡng AA 4,5:1: 5,23:1 trên nền trắng, 4,89:1 trên `muted`.
157
+ * KHÔNG còn bằng `ash[500]` (#7E8290 chỉ 3,83:1 ✗) — bậc bảng màu và token semantic tách nhau. */
158
+ readonly mutedForeground: "#686C7A";
77
159
  readonly accent: "#FBF5DC";
78
160
  readonly accentForeground: "#0E1116";
79
- readonly destructive: "#D64545";
161
+ readonly destructive: "#C93B3B";
80
162
  readonly destructiveForeground: "#FFFFFF";
163
+ /** Chữ báo lỗi trên nền trang — KHÁC `destructive` (nền) và `destructiveForeground` (chữ
164
+ * trắng trên nền đó). 7,15:1 trên nền trắng. */
165
+ readonly destructiveInk: "#9A3537";
166
+ /** RANH GIỚI TRANG TRÍ (vạch chia, viền thẻ nghỉ) — 1,16:1, cố ý: 1.4.11 không đòi tương
167
+ * phản cho thứ không mang thông tin. Ranh giới của CONTROL đi qua `controlEdge`. */
81
168
  readonly border: "#EDEEF1";
82
- readonly input: "#EDEEF1";
169
+ /** = `controlEdge`: viền ô nhập là ranh giới nhận dạng control (1.4.11) → 3,83:1 ✓. */
170
+ readonly input: "#7E8290";
83
171
  readonly ring: "#E8B824";
172
+ /** Ranh giới của control lúc NGHỈ (viền Input, ô Checkbox chưa chọn, rãnh Switch khi TẮT) —
173
+ * WCAG 1.4.11, ràng buộc HAI PHÍA. Một giá trị cho cả hai theme: 3,83:1 với nền trắng và
174
+ * 3,83:1 với `controlKnob` (trắng ở light); 4,94:1 với nền dark và với knob #0E1116. */
175
+ readonly controlEdge: "#7E8290";
176
+ /** Cùng vai trò nhưng ở HOVER/ACTIVE — phải ĐẬM hơn lúc nghỉ. = `ash[700]`, 10,55:1 trên nền
177
+ * trắng · 9,86:1 trên `muted`. */
178
+ readonly controlEdgeStrong: "#3A3F4A";
179
+ /** Nền control khi BẬT/ĐÃ CHỌN (rãnh Switch bật, ô Checkbox đã chọn, chấm Radio) = `primaryInk`.
180
+ * Ràng buộc hai phía: 6,51:1 với nền trang VÀ 6,51:1 với `controlKnob` ở light. `primary`
181
+ * #E8B824 chỉ 1,86:1 cả hai phía ✗. */
182
+ readonly controlOn: "#7A5800";
183
+ /** Núm Switch / dấu check / chấm Radio — mang màu NỀN TRANG nên luôn tương phản đúng chiều
184
+ * với cả rãnh BẬT (6,51:1) lẫn rãnh TẮT (3,83:1). KHÔNG dùng `primaryForeground` (mực): mực
185
+ * trên `controlOn` #7A5800 chỉ 1,86:1 ✗. */
186
+ readonly controlKnob: "#FFFFFF";
84
187
  readonly sidebar: "#FAFAF9";
85
188
  readonly sidebarForeground: "#0E1116";
86
189
  readonly sidebarBorder: "#EDEEF1";
@@ -96,17 +199,33 @@ declare const semantic: {
96
199
  readonly popoverForeground: "#F7F7F8";
97
200
  readonly primary: "#F5D060";
98
201
  readonly primaryForeground: "#0E1116";
202
+ /** Ở dark `primary` vốn đã là bậc sáng (12,67:1 trên nền `background`) nên ink = chính nó. */
203
+ readonly primaryInk: "#F5D060";
99
204
  readonly secondary: "#1E2128";
100
205
  readonly secondaryForeground: "#F7F7F8";
101
206
  readonly muted: "#1E2128";
102
- readonly mutedForeground: "#7E8290";
207
+ /** Sáng hơn bản light: chữ phụ ở dark hay nằm trên `card`/`muted`. 5,35:1 trên `card`,
208
+ * 4,86:1 trên `muted` (giá trị cũ #7E8290 chỉ 4,21:1 trên `muted` ✗). */
209
+ readonly mutedForeground: "#888D9B";
103
210
  readonly accent: "#1E2128";
104
211
  readonly accentForeground: "#F7F7F8";
105
- readonly destructive: "#D64545";
212
+ readonly destructive: "#C93B3B";
106
213
  readonly destructiveForeground: "#FFFFFF";
214
+ /** 6,42:1 trên `card` — `destructive` dùng làm chữ ở dark chỉ được 3,53:1 ✗. */
215
+ readonly destructiveInk: "#F87171";
216
+ /** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */
107
217
  readonly border: "#1E2128";
108
- readonly input: "#1E2128";
218
+ /** = `controlEdge`, ghim một giá trị cho cả hai theme — 4,94:1 trên `background` ✓. */
219
+ readonly input: "#7E8290";
109
220
  readonly ring: "#F5D060";
221
+ /** Ghim đúng giá trị của light — xem chú thích ở nhánh light. */
222
+ readonly controlEdge: "#7E8290";
223
+ /** = `ash[300]` (lật chiều so với light): 11,65:1 trên `background` · 10,94:1 trên `card`. */
224
+ readonly controlEdgeStrong: "#C8CBD3";
225
+ /** = `primaryInk` của dark; 12,67:1 với nền trang VÀ với `controlKnob` (#0E1116). */
226
+ readonly controlOn: "#F5D060";
227
+ /** = `background` của dark. Núm trắng cứng sẽ chỉ được 1,49:1 với rãnh BẬT ✗. */
228
+ readonly controlKnob: "#0E1116";
110
229
  readonly sidebar: "#16181D";
111
230
  readonly sidebarForeground: "#F7F7F8";
112
231
  readonly sidebarBorder: "#1E2128";
@@ -114,17 +233,36 @@ declare const semantic: {
114
233
  readonly sidebarAccentForeground: "#F7F7F8";
115
234
  };
116
235
  };
117
- /** Alias trạng thái — success/warning/info trỏ vào màu phụ, destructive giữ nguyên 2 chế độ. */
236
+ /**
237
+ * Alias trạng thái — success/warning/info trỏ vào màu phụ, destructive giữ nguyên 2 chế độ.
238
+ *
239
+ * Ba lớp KHÁC nhau, đừng gộp:
240
+ * • `destructive` (#C93B3B) — màu NỀN (nút xoá), đủ đậm cho chữ trắng ở cả hai theme (5,03:1);
241
+ * • `destructiveForeground` (#FFFFFF) — chữ NẰM TRÊN nền đó;
242
+ * • `destructiveInk` — màu CHỮ đỏ trên nền trang (dòng lỗi). Dùng nhầm `destructive` cho vai
243
+ * trò này thì ở dark chỉ được 3,53:1, dùng `accent.light.red` thì ở light chỉ 4,38:1.
244
+ *
245
+ * `successForeground` / `warningForeground` / `infoForeground` là MỰC ở CẢ hai theme (không phải
246
+ * trắng ở light rồi lật ở dark như bản 0.1.0): ba nền trạng thái đều là sắc sáng.
247
+ * success mực 6,04:1 (light) · 10,85:1 (dark) [trắng chỉ 3,13 / 1,74:1 ✗]
248
+ * info mực 4,56:1 (light) · 10,16:1 (dark) [trắng chỉ 4,14 / 1,86:1 ✗]
249
+ * warning mực 11,92:1 (light) · 12,35:1 (dark) [trắng chỉ 1,59 / 1,53:1 ✗]
250
+ * ⚠ `info` ở light là cặp chặt nhất gói (4,56:1, dư 0,06) — đổi `accent.light.teal` là phải đo lại.
251
+ */
118
252
  declare const status: {
119
253
  readonly light: {
120
254
  readonly success: "#2EA56A";
121
- readonly successForeground: "#FFFFFF";
255
+ readonly successForeground: "#0E1116";
122
256
  readonly warning: "#F2C94C";
123
257
  readonly warningForeground: "#0E1116";
124
258
  readonly info: "#1F8A8B";
125
- readonly infoForeground: "#FFFFFF";
126
- readonly destructive: "#D64545";
259
+ readonly infoForeground: "#0E1116";
260
+ readonly destructive: "#C93B3B";
127
261
  readonly destructiveForeground: "#FFFFFF";
262
+ readonly successInk: "#216A48";
263
+ readonly warningInk: "#7A5800";
264
+ readonly infoInk: "#1A6668";
265
+ readonly destructiveInk: "#9A3537";
128
266
  };
129
267
  readonly dark: {
130
268
  readonly success: "#4ADE80";
@@ -133,8 +271,12 @@ declare const status: {
133
271
  readonly warningForeground: "#0E1116";
134
272
  readonly info: "#2DD4BF";
135
273
  readonly infoForeground: "#0E1116";
136
- readonly destructive: "#D64545";
274
+ readonly destructive: "#C93B3B";
137
275
  readonly destructiveForeground: "#FFFFFF";
276
+ readonly successInk: "#4ADE80";
277
+ readonly warningInk: "#FACC15";
278
+ readonly infoInk: "#2DD4BF";
279
+ readonly destructiveInk: "#F87171";
138
280
  };
139
281
  };
140
282
  /** Bảng màu biểu đồ (chart-1…chart-5). Thứ tự = thứ tự series mặc định. */
@@ -142,8 +284,15 @@ declare const chart: {
142
284
  readonly light: readonly ["#E8B824", "#1F8A8B", "#0F1E3D", "#2EA56A", "#D64545"];
143
285
  readonly dark: readonly ["#F5D060", "#2DD4BF", "#7EA2E8", "#4ADE80", "#F87171"];
144
286
  };
145
- /** Màu avatar chữ cái — chọn deterministic theo tên, lấy từ token nên luôn hợp tông brand. */
146
- declare const avatarPalette: readonly ["#B88A00", "#5C4006", "#1F8A8B", "#0F1E3D", "#2EA56A", "#D64545", "#7E8290", "#3A3F4A"];
287
+ /**
288
+ * Cặp nền/chữ (hex) của avatar cho một tên, theo theme.
289
+ *
290
+ * Tên hàm nói đúng thứ nó trả về: `...Colors` = hex (canvas/email/PDF), còn bản trả về chuỗi
291
+ * class Tailwind cho DOM là `getAvatarToneClasses` ở `@comitor/ui`. Cùng một tên người luôn ra
292
+ * cùng một tone ở cả hai đường vì chỉ số do MỘT hàm tính (`getAvatarToneIndex`).
293
+ */
294
+ declare function getAvatarToneColors(name: string, mode?: ThemeMode): AvatarToneColors;
295
+
147
296
  /** Số đo khung ứng dụng — mirror --sidebar-width / --sidebar-width-icon / --header-height. */
148
297
  declare const layout: {
149
298
  /** px — --header-height 3.5rem */
@@ -159,7 +308,6 @@ declare const layout: {
159
308
  readonly tablet: 1024;
160
309
  readonly desktop: 1280;
161
310
  };
162
- readonly pageSizes: readonly [20, 50, 100];
163
311
  /** px — kích thước tối thiểu của vùng chạm trên mobile (WCAG 2.5.5) */
164
312
  readonly minTapTarget: 44;
165
313
  };
@@ -170,4 +318,4 @@ declare const typography: {
170
318
  };
171
319
  type ThemeMode = "light" | "dark";
172
320
 
173
- export { type ThemeMode, accent, ash, avatarPalette, brandNeutral, chart, gold, layout, semantic, status, typography };
321
+ export { AvatarToneColors, type ThemeMode, accent, accentForeground, accentInk, ash, brandNeutral, chart, getAvatarToneColors, gold, layout, semantic, status, typography };
package/dist/tokens.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { AVATAR_TONES, getAvatarToneIndex } from "./lib/avatar-tone.js";
1
2
  const gold = {
2
3
  50: "#FBF5DC",
3
4
  100: "#F8E89D",
@@ -38,6 +39,38 @@ const accent = {
38
39
  yellow: "#FACC15"
39
40
  }
40
41
  };
42
+ const accentInk = {
43
+ light: {
44
+ navy: accent.light.navy,
45
+ teal: "#1A6668",
46
+ red: "#9A3537",
47
+ green: "#216A48",
48
+ /** = gold[500]: vàng không có bậc tối riêng, mượn thang gold cùng sắc. */
49
+ yellow: gold[500]
50
+ },
51
+ dark: {
52
+ /** navy nguyên bản chìm vào nền dark (1,15:1) → bậc sáng riêng; cũng là `--chart-3` ở dark. */
53
+ navy: "#7EA2E8",
54
+ teal: accent.dark.teal,
55
+ red: accent.dark.red,
56
+ green: accent.dark.green,
57
+ yellow: accent.dark.yellow
58
+ }
59
+ };
60
+ const accentForeground = {
61
+ light: {
62
+ navy: "#FFFFFF",
63
+ teal: "#0E1116",
64
+ green: "#0E1116",
65
+ yellow: "#0E1116"
66
+ },
67
+ dark: {
68
+ navy: "#FFFFFF",
69
+ teal: "#0E1116",
70
+ green: "#0E1116",
71
+ yellow: "#0E1116"
72
+ }
73
+ };
41
74
  const semantic = {
42
75
  light: {
43
76
  background: "#FFFFFF",
@@ -48,17 +81,43 @@ const semantic = {
48
81
  popoverForeground: "#0E1116",
49
82
  primary: "#E8B824",
50
83
  primaryForeground: "#0E1116",
84
+ /** Bậc INK của brand — gold khi phải ĐI LÊN nền trang (rãnh Switch khi BẬT, ô Checkbox đã
85
+ * chọn). `primary` #E8B824 chỉ 1,86:1 với nền trắng ✗; bậc này = `gold[500]` = 6,51:1 ✓. */
86
+ primaryInk: gold[500],
51
87
  secondary: "#EDE8D8",
52
88
  secondaryForeground: "#0E1116",
53
89
  muted: "#F7F7F8",
54
- mutedForeground: "#7E8290",
90
+ /** Bậc chữ phụ — là CHỮ nên ngưỡng AA 4,5:1: 5,23:1 trên nền trắng, 4,89:1 trên `muted`.
91
+ * KHÔNG còn bằng `ash[500]` (#7E8290 chỉ 3,83:1 ✗) — bậc bảng màu và token semantic tách nhau. */
92
+ mutedForeground: "#686C7A",
55
93
  accent: "#FBF5DC",
56
94
  accentForeground: "#0E1116",
57
- destructive: "#D64545",
95
+ destructive: "#C93B3B",
58
96
  destructiveForeground: "#FFFFFF",
97
+ /** Chữ báo lỗi trên nền trang — KHÁC `destructive` (nền) và `destructiveForeground` (chữ
98
+ * trắng trên nền đó). 7,15:1 trên nền trắng. */
99
+ destructiveInk: accentInk.light.red,
100
+ /** RANH GIỚI TRANG TRÍ (vạch chia, viền thẻ nghỉ) — 1,16:1, cố ý: 1.4.11 không đòi tương
101
+ * phản cho thứ không mang thông tin. Ranh giới của CONTROL đi qua `controlEdge`. */
59
102
  border: "#EDEEF1",
60
- input: "#EDEEF1",
103
+ /** = `controlEdge`: viền ô nhập là ranh giới nhận dạng control (1.4.11) → 3,83:1 ✓. */
104
+ input: "#7E8290",
61
105
  ring: "#E8B824",
106
+ /** Ranh giới của control lúc NGHỈ (viền Input, ô Checkbox chưa chọn, rãnh Switch khi TẮT) —
107
+ * WCAG 1.4.11, ràng buộc HAI PHÍA. Một giá trị cho cả hai theme: 3,83:1 với nền trắng và
108
+ * 3,83:1 với `controlKnob` (trắng ở light); 4,94:1 với nền dark và với knob #0E1116. */
109
+ controlEdge: "#7E8290",
110
+ /** Cùng vai trò nhưng ở HOVER/ACTIVE — phải ĐẬM hơn lúc nghỉ. = `ash[700]`, 10,55:1 trên nền
111
+ * trắng · 9,86:1 trên `muted`. */
112
+ controlEdgeStrong: ash[700],
113
+ /** Nền control khi BẬT/ĐÃ CHỌN (rãnh Switch bật, ô Checkbox đã chọn, chấm Radio) = `primaryInk`.
114
+ * Ràng buộc hai phía: 6,51:1 với nền trang VÀ 6,51:1 với `controlKnob` ở light. `primary`
115
+ * #E8B824 chỉ 1,86:1 cả hai phía ✗. */
116
+ controlOn: gold[500],
117
+ /** Núm Switch / dấu check / chấm Radio — mang màu NỀN TRANG nên luôn tương phản đúng chiều
118
+ * với cả rãnh BẬT (6,51:1) lẫn rãnh TẮT (3,83:1). KHÔNG dùng `primaryForeground` (mực): mực
119
+ * trên `controlOn` #7A5800 chỉ 1,86:1 ✗. */
120
+ controlKnob: "#FFFFFF",
62
121
  sidebar: "#FAFAF9",
63
122
  sidebarForeground: "#0E1116",
64
123
  sidebarBorder: "#EDEEF1",
@@ -74,17 +133,33 @@ const semantic = {
74
133
  popoverForeground: "#F7F7F8",
75
134
  primary: "#F5D060",
76
135
  primaryForeground: "#0E1116",
136
+ /** Ở dark `primary` vốn đã là bậc sáng (12,67:1 trên nền `background`) nên ink = chính nó. */
137
+ primaryInk: "#F5D060",
77
138
  secondary: "#1E2128",
78
139
  secondaryForeground: "#F7F7F8",
79
140
  muted: "#1E2128",
80
- mutedForeground: "#7E8290",
141
+ /** Sáng hơn bản light: chữ phụ ở dark hay nằm trên `card`/`muted`. 5,35:1 trên `card`,
142
+ * 4,86:1 trên `muted` (giá trị cũ #7E8290 chỉ 4,21:1 trên `muted` ✗). */
143
+ mutedForeground: "#888D9B",
81
144
  accent: "#1E2128",
82
145
  accentForeground: "#F7F7F8",
83
- destructive: "#D64545",
146
+ destructive: "#C93B3B",
84
147
  destructiveForeground: "#FFFFFF",
148
+ /** 6,42:1 trên `card` — `destructive` dùng làm chữ ở dark chỉ được 3,53:1 ✗. */
149
+ destructiveInk: accentInk.dark.red,
150
+ /** TRANG TRÍ, 1,17:1 trên `background` và 1,10:1 trên `card` — cố ý, xem nhánh light. */
85
151
  border: "#1E2128",
86
- input: "#1E2128",
152
+ /** = `controlEdge`, ghim một giá trị cho cả hai theme — 4,94:1 trên `background` ✓. */
153
+ input: "#7E8290",
87
154
  ring: "#F5D060",
155
+ /** Ghim đúng giá trị của light — xem chú thích ở nhánh light. */
156
+ controlEdge: "#7E8290",
157
+ /** = `ash[300]` (lật chiều so với light): 11,65:1 trên `background` · 10,94:1 trên `card`. */
158
+ controlEdgeStrong: ash[300],
159
+ /** = `primaryInk` của dark; 12,67:1 với nền trang VÀ với `controlKnob` (#0E1116). */
160
+ controlOn: "#F5D060",
161
+ /** = `background` của dark. Núm trắng cứng sẽ chỉ được 1,49:1 với rãnh BẬT ✗. */
162
+ controlKnob: "#0E1116",
88
163
  sidebar: "#16181D",
89
164
  sidebarForeground: "#F7F7F8",
90
165
  sidebarBorder: "#1E2128",
@@ -95,13 +170,17 @@ const semantic = {
95
170
  const status = {
96
171
  light: {
97
172
  success: accent.light.green,
98
- successForeground: "#FFFFFF",
173
+ successForeground: "#0E1116",
99
174
  warning: accent.light.yellow,
100
175
  warningForeground: "#0E1116",
101
176
  info: accent.light.teal,
102
- infoForeground: "#FFFFFF",
103
- destructive: "#D64545",
104
- destructiveForeground: "#FFFFFF"
177
+ infoForeground: "#0E1116",
178
+ destructive: "#C93B3B",
179
+ destructiveForeground: "#FFFFFF",
180
+ successInk: accentInk.light.green,
181
+ warningInk: accentInk.light.yellow,
182
+ infoInk: accentInk.light.teal,
183
+ destructiveInk: accentInk.light.red
105
184
  },
106
185
  dark: {
107
186
  success: accent.dark.green,
@@ -110,24 +189,40 @@ const status = {
110
189
  warningForeground: "#0E1116",
111
190
  info: accent.dark.teal,
112
191
  infoForeground: "#0E1116",
113
- destructive: "#D64545",
114
- destructiveForeground: "#FFFFFF"
192
+ destructive: "#C93B3B",
193
+ destructiveForeground: "#FFFFFF",
194
+ successInk: accentInk.dark.green,
195
+ warningInk: accentInk.dark.yellow,
196
+ infoInk: accentInk.dark.teal,
197
+ destructiveInk: accentInk.dark.red
115
198
  }
116
199
  };
117
200
  const chart = {
118
201
  light: [gold[300], accent.light.teal, accent.light.navy, accent.light.green, accent.light.red],
119
- dark: [gold[200], accent.dark.teal, "#7EA2E8", accent.dark.green, accent.dark.red]
202
+ dark: [gold[200], accent.dark.teal, accentInk.dark.navy, accent.dark.green, accent.dark.red]
203
+ };
204
+ const avatarTonePalette = {
205
+ light: {
206
+ gold,
207
+ ash,
208
+ brandNeutral,
209
+ accent: accent.light,
210
+ accentInk: accentInk.light,
211
+ card: semantic.light.card
212
+ },
213
+ dark: {
214
+ gold,
215
+ ash,
216
+ brandNeutral,
217
+ accent: accent.dark,
218
+ accentInk: accentInk.dark,
219
+ card: semantic.dark.card
220
+ }
120
221
  };
121
- const avatarPalette = [
122
- gold[400],
123
- gold[600],
124
- accent.light.teal,
125
- accent.light.navy,
126
- accent.light.green,
127
- accent.light.red,
128
- ash[500],
129
- brandNeutral.inkSoft
130
- ];
222
+ function getAvatarToneColors(name, mode = "light") {
223
+ const tone = AVATAR_TONES[getAvatarToneIndex(name)] ?? AVATAR_TONES[0];
224
+ return tone[mode](avatarTonePalette[mode]);
225
+ }
131
226
  const layout = {
132
227
  /** px — --header-height 3.5rem */
133
228
  headerHeight: 56,
@@ -138,7 +233,6 @@ const layout = {
138
233
  /** px — --radius 0.375rem */
139
234
  radius: 6,
140
235
  breakpoints: { mobile: 768, tablet: 1024, desktop: 1280 },
141
- pageSizes: [20, 50, 100],
142
236
  /** px — kích thước tối thiểu của vùng chạm trên mobile (WCAG 2.5.5) */
143
237
  minTapTarget: 44
144
238
  };
@@ -148,10 +242,13 @@ const typography = {
148
242
  };
149
243
  export {
150
244
  accent,
245
+ accentForeground,
246
+ accentInk,
151
247
  ash,
152
- avatarPalette,
153
248
  brandNeutral,
154
249
  chart,
250
+ getAvatarToneColors,
251
+ getAvatarToneIndex,
155
252
  gold,
156
253
  layout,
157
254
  semantic,