@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.
- package/README.md +303 -43
- package/dist/chart/chart.d.ts +35 -3
- package/dist/chart/chart.js.map +1 -1
- package/dist/chart/comitor-chart.d.ts +16 -3
- package/dist/chart/comitor-chart.js.map +1 -1
- package/dist/chart/index.d.ts +2 -2
- package/dist/components/composite/avatar-group.js +1 -1
- package/dist/components/composite/avatar-group.js.map +1 -1
- package/dist/components/composite/combobox.d.ts +9 -2
- package/dist/components/composite/combobox.js +9 -2
- package/dist/components/composite/combobox.js.map +1 -1
- package/dist/components/composite/confirm-dialog.js +1 -1
- package/dist/components/composite/confirm-dialog.js.map +1 -1
- package/dist/components/composite/copy-button.js +1 -1
- package/dist/components/composite/copy-button.js.map +1 -1
- package/dist/components/composite/data-table.d.ts +17 -8
- package/dist/components/composite/data-table.js +8 -8
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/filter-chips.js +6 -5
- package/dist/components/composite/filter-chips.js.map +1 -1
- package/dist/components/composite/form-field.js +3 -1
- package/dist/components/composite/form-field.js.map +1 -1
- package/dist/components/composite/form-section.js +5 -5
- package/dist/components/composite/form-section.js.map +1 -1
- package/dist/components/composite/inline-edit.js +1 -1
- package/dist/components/composite/inline-edit.js.map +1 -1
- package/dist/components/composite/keyboard-hint.d.ts +16 -0
- package/dist/components/composite/keyboard-hint.js +8 -6
- package/dist/components/composite/keyboard-hint.js.map +1 -1
- package/dist/components/composite/letter-avatar.d.ts +10 -18
- package/dist/components/composite/letter-avatar.js +6 -17
- package/dist/components/composite/letter-avatar.js.map +1 -1
- package/dist/components/composite/search-filter-bar.d.ts +41 -3
- package/dist/components/composite/search-filter-bar.js +27 -15
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/stat-card.d.ts +16 -1
- package/dist/components/composite/stat-card.js +16 -9
- package/dist/components/composite/stat-card.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +69 -16
- package/dist/components/composite/status-config.js +10 -10
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/composite/status-pill.js +5 -1
- package/dist/components/composite/status-pill.js.map +1 -1
- package/dist/components/composite/table-pagination.d.ts +46 -4
- package/dist/components/composite/table-pagination.js +27 -24
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/components/composite/text-utils.d.ts +12 -11
- package/dist/components/composite/text-utils.js +7 -16
- package/dist/components/composite/text-utils.js.map +1 -1
- package/dist/components/composite/toast.js +16 -6
- package/dist/components/composite/toast.js.map +1 -1
- package/dist/components/ui/alert.js +1 -1
- package/dist/components/ui/alert.js.map +1 -1
- package/dist/components/ui/badge.js +4 -4
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/button.js +3 -3
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/calendar.d.ts +41 -0
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/ui/card.d.ts +35 -2
- package/dist/components/ui/card.js +17 -3
- package/dist/components/ui/card.js.map +1 -1
- package/dist/components/ui/checkbox.js +3 -3
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/command.js +1 -1
- package/dist/components/ui/command.js.map +1 -1
- package/dist/components/ui/context-menu.js +1 -1
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/components/ui/drawer.js +1 -1
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/dropdown-menu.js +1 -1
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/empty.js +1 -1
- package/dist/components/ui/empty.js.map +1 -1
- package/dist/components/ui/field.js +4 -4
- package/dist/components/ui/field.js.map +1 -1
- package/dist/components/ui/form.js +8 -2
- package/dist/components/ui/form.js.map +1 -1
- package/dist/components/ui/input-group.js +3 -3
- package/dist/components/ui/input-group.js.map +1 -1
- package/dist/components/ui/input-otp.js +1 -1
- package/dist/components/ui/input-otp.js.map +1 -1
- package/dist/components/ui/input.js +3 -3
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/item.js +1 -1
- package/dist/components/ui/item.js.map +1 -1
- package/dist/components/ui/menubar.js +1 -1
- package/dist/components/ui/menubar.js.map +1 -1
- package/dist/components/ui/navigation-menu.js +1 -1
- package/dist/components/ui/navigation-menu.js.map +1 -1
- package/dist/components/ui/pagination.js +1 -1
- package/dist/components/ui/pagination.js.map +1 -1
- package/dist/components/ui/progress.d.ts +26 -0
- package/dist/components/ui/progress.js +5 -2
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/radio-group.js +2 -2
- package/dist/components/ui/radio-group.js.map +1 -1
- package/dist/components/ui/resizable.d.ts +14 -0
- package/dist/components/ui/resizable.js +2 -2
- package/dist/components/ui/resizable.js.map +1 -1
- package/dist/components/ui/scroll-area.js +1 -1
- package/dist/components/ui/scroll-area.js.map +1 -1
- package/dist/components/ui/select.d.ts +33 -0
- package/dist/components/ui/select.js +3 -3
- package/dist/components/ui/select.js.map +1 -1
- package/dist/components/ui/slider.d.ts +19 -0
- package/dist/components/ui/slider.js +3 -3
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/sonner.d.ts +21 -3
- package/dist/components/ui/sonner.js +1 -2
- package/dist/components/ui/sonner.js.map +1 -1
- package/dist/components/ui/switch.js +2 -2
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/components/ui/table.d.ts +16 -0
- package/dist/components/ui/table.js +1 -1
- package/dist/components/ui/table.js.map +1 -1
- package/dist/components/ui/tabs.js +1 -1
- package/dist/components/ui/tabs.js.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/components/ui/toggle.js +1 -1
- package/dist/components/ui/toggle.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/avatar-tone.d.ts +126 -0
- package/dist/lib/avatar-tone.js +68 -0
- package/dist/lib/avatar-tone.js.map +1 -0
- package/dist/shell/app-header.js +12 -2
- package/dist/shell/app-header.js.map +1 -1
- package/dist/shell/app-launcher.js +2 -2
- package/dist/shell/app-launcher.js.map +1 -1
- package/dist/shell/app-shell.d.ts +2 -0
- package/dist/shell/index.d.ts +3 -3
- package/dist/shell/shell-context.d.ts +29 -8
- package/dist/shell/shell-context.js +6 -20
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar-group.js +5 -3
- package/dist/shell/sidebar-group.js.map +1 -1
- package/dist/shell/sidebar-menu-item.js +15 -7
- package/dist/shell/sidebar-menu-item.js.map +1 -1
- package/dist/shell/types.d.ts +17 -1
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/user-menu.js +3 -2
- package/dist/shell/user-menu.js.map +1 -1
- package/dist/shell/utils.d.ts +36 -24
- package/dist/shell/utils.js +55 -20
- package/dist/shell/utils.js.map +1 -1
- package/dist/shell/workspace-switcher.js +23 -8
- package/dist/shell/workspace-switcher.js.map +1 -1
- package/dist/tokens.d.ts +165 -17
- package/dist/tokens.js +122 -25
- package/dist/tokens.js.map +1 -1
- package/package.json +8 -5
- 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
|
|
44
|
-
* bản `light`
|
|
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
|
-
|
|
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: "#
|
|
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
|
-
|
|
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
|
-
|
|
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: "#
|
|
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
|
-
|
|
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
|
-
/**
|
|
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: "#
|
|
255
|
+
readonly successForeground: "#0E1116";
|
|
122
256
|
readonly warning: "#F2C94C";
|
|
123
257
|
readonly warningForeground: "#0E1116";
|
|
124
258
|
readonly info: "#1F8A8B";
|
|
125
|
-
readonly infoForeground: "#
|
|
126
|
-
readonly destructive: "#
|
|
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: "#
|
|
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
|
-
/**
|
|
146
|
-
|
|
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,
|
|
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
|
-
|
|
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: "#
|
|
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
|
-
|
|
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
|
-
|
|
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: "#
|
|
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
|
-
|
|
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: "#
|
|
173
|
+
successForeground: "#0E1116",
|
|
99
174
|
warning: accent.light.yellow,
|
|
100
175
|
warningForeground: "#0E1116",
|
|
101
176
|
info: accent.light.teal,
|
|
102
|
-
infoForeground: "#
|
|
103
|
-
destructive: "#
|
|
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: "#
|
|
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,
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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,
|