@comitor/ui 0.9.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -858
- package/dist/components/composite/avatar-group.d.ts +18 -2
- package/dist/components/composite/avatar-group.js +2 -5
- package/dist/components/composite/avatar-group.js.map +1 -1
- package/dist/components/composite/combobox.d.ts +31 -2
- package/dist/components/composite/combobox.js +12 -9
- package/dist/components/composite/combobox.js.map +1 -1
- package/dist/components/composite/command-palette-constants.d.ts +35 -0
- package/dist/components/composite/command-palette-constants.js +8 -0
- package/dist/components/composite/command-palette-constants.js.map +1 -0
- package/dist/components/composite/command-palette.d.ts +4 -1
- package/dist/components/composite/command-palette.js +5 -2
- package/dist/components/composite/command-palette.js.map +1 -1
- package/dist/components/composite/confirm-dialog.d.ts +11 -2
- package/dist/components/composite/confirm-dialog.js +2 -1
- package/dist/components/composite/confirm-dialog.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/shell/contrast-provider.d.ts +10 -2
- package/dist/shell/contrast-provider.js +12 -4
- package/dist/shell/contrast-provider.js.map +1 -1
- package/dist/shell/display-axis-provider.d.ts +30 -4
- package/dist/shell/display-axis-provider.js +12 -4
- package/dist/shell/display-axis-provider.js.map +1 -1
- package/dist/shell/mobile-menu.js +1 -0
- package/dist/shell/mobile-menu.js.map +1 -1
- package/dist/shell/types.d.ts +8 -1
- package/dist/shell/types.js +1 -1
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/workspace-switcher.js +1 -1
- package/dist/shell/workspace-switcher.js.map +1 -1
- package/package.json +3 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/workspace-switcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { Building2, Check, ChevronsUpDown, LogOut, Plus, Search, Settings, UserPlus } from \"lucide-react\";\nimport { useMemo, useRef, useState } from \"react\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport type { Workspace } from \"./types.js\";\nimport { getVietnameseInitials, normalizeVi } from \"./utils.js\";\n\n/** Từ số workspace này trở lên thì hiện ô tìm kiếm trong danh sách. */\nconst SEARCH_THRESHOLD = 8;\n\n/**\n * Ảnh đại diện workspace: logo nếu có, không thì ô chữ cái tô màu deterministic.\n *\n * Seed là `workspace.name` chứ KHÔNG phải slug: app hiển thị workspace bằng\n * `<LetterAvatar name={workspace.name} square />` ở chỗ khác, seed lệch là màu lệch.\n */\nfunction WorkspaceAvatar({ workspace, className }: { workspace: Workspace; className?: string }) {\n return (\n <Avatar className={cn(\"size-6 rounded-md\", className)}>\n {workspace.logoUrl ? <AvatarImage src={workspace.logoUrl} alt=\"\" className=\"rounded-md\" /> : null}\n <AvatarFallback className={cn(\"rounded-md text-[10px] font-semibold\", getAvatarToneClasses(workspace.name))}>\n {getVietnameseInitials(workspace.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface WorkspaceSwitcherProps {\n className?: string;\n /** Sidebar thu gọn → chỉ hiện avatar + tooltip. */\n collapsed?: boolean;\n /** Ẩn khu \"Hành động\" (tạo workspace, cài đặt, mời thành viên, đăng xuất). */\n showActions?: boolean;\n /** Căn panel — mặc định \"start\" (mở xuôi theo mép trái sidebar). */\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 1 — đổi không gian làm việc.**\n *\n * Chỉ gọi `onWorkspaceChange(id)`; điều hướng/đổi subdomain là việc của app\n * (đổi workspace kéo theo đổi toàn bộ dữ liệu + quyền nên shell không tự quyết).\n * Có ≤1 workspace thì phần danh sách tự ẩn, chỉ còn nhãn workspace hiện tại.\n *\n * Dùng `Popover` chứ không `DropdownMenu`: menu của Radix có typeahead, gõ vào\n * ô tìm kiếm bên trong sẽ bị nó cướp phím.\n */\nexport function WorkspaceSwitcher({\n className,\n collapsed = false,\n showActions = true,\n align = \"start\"\n}: WorkspaceSwitcherProps) {\n const {\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n onLogout,\n labels\n } = useShell();\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const searchRef = useRef<HTMLInputElement>(null);\n\n const showList = workspaces.length > 1;\n const showSearch = workspaces.length >= SEARCH_THRESHOLD;\n\n const filtered = useMemo(() => {\n if (!query.trim()) return workspaces;\n const needle = normalizeVi(query);\n return workspaces.filter(\n (workspace) => normalizeVi(workspace.name).includes(needle) || normalizeVi(workspace.slug).includes(needle)\n );\n }, [workspaces, query]);\n\n const hasActions = showActions && Boolean(onCreateWorkspace || onWorkspaceSettings || onInviteMembers || onLogout);\n\n if (!currentWorkspace) return null;\n\n const handleSelect = (workspace: Workspace) => {\n setOpen(false);\n setQuery(\"\");\n if (workspace.id !== currentWorkspace.id) onWorkspaceChange?.(workspace.id);\n };\n\n /** Đóng panel rồi mới chạy hành động — tránh panel còn mở đè lên dialog app mở ra sau đó. */\n const closeThen = (action: () => void) => () => {\n setOpen(false);\n action();\n };\n\n const trigger = collapsed ? (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n </button>\n ) : (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n {/*\n `size-8` (32px) thay vì `size-6` (24px): avatar cao BẰNG khối chữ hai dòng bên cạnh\n (17,5px + 13,75px ≈ 31px sau khi cả hai dòng dùng `leading-tight`), nên hàng này đọc ra\n một khối chứ không phải một ô vuông nhỏ lửng lơ cạnh hai dòng chữ.\n ⚠ Chỉ đổi ở trigger dạng ĐẦY ĐỦ. Trigger lúc thanh bên thu gọn vẫn `size-6` trong một nút\n 36px — ở đó avatar là toàn bộ nội dung nên nó cần đệm quanh, không cần lấp đầy.\n */}\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n {/* `leading-tight` ở CẢ hai dòng: mặc định `text-sm` có line-height 20px và dòng gói\n 11px thừa hưởng line-height của body (~1,5) — cộng lại là một khoảng hở rộng hơn cả\n thân chữ, khiến tên gói trông như không thuộc về tên workspace. */}\n <span className=\"truncate text-sm font-semibold leading-tight text-sidebar-foreground\">\n {currentWorkspace.name}\n </span>\n {currentWorkspace.plan ? (\n // `text-muted-foreground` chứ không `text-sidebar-foreground/55`: đây là CHỮ (11px) nên\n // ngưỡng là AA 4,5:1, mà /55 trên `--sidebar` #FAFAF9 chỉ được 4,13:1 ✗ (dark 5,78:1 —\n // mỗi theme hỏng một nửa). `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò:\n // 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓.\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{currentWorkspace.plan}</span>\n ) : null}\n </span>\n {/* Mũi tên MANG THÔNG TIN (nút này mở ra một panel chọn) nên chịu ngưỡng 3:1 của WCAG\n 1.4.11, mà `text-sidebar-foreground/45` chỉ được 3,02:1 trên `--sidebar` ở light và\n tụt xuống 3,01:1 khi hover đổi nền sang `--sidebar-accent` — sát/dưới vạch, và ở\n layout \"header-first\" nút này còn nằm trên nền `--background` khác nữa.\n `--muted-foreground` là bậc đã đo và dư biên ở mọi nền nó có thể đứng: 5,01:1 trên\n `--sidebar` · 4,78:1 trên `--sidebar-accent` · 5,23:1 trên `--background` (light);\n 5,35 / 4,86 / 5,70:1 (dark) ✓. (Dark của `/45` là 4,26 / 4,14:1 — chỉ light hỏng,\n nhưng bậc mờ theo alpha luôn lệch nhau như vậy giữa hai theme.) */}\n <ChevronsUpDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </button>\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {collapsed ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{currentWorkspace.name}</TooltipContent>\n </Tooltip>\n ) : (\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n )}\n\n <PopoverContent\n align={align}\n sideOffset={6}\n /*\n * `w-80` (320px), không phải `w-72` (288px). Ở 288px dòng phụ của hàng ĐANG CHỌN bị cắt —\n * đo được thiếu đúng 4px cho \"Quản trị viên · 6 thành viên\". Hàng đang chọn là hàng chật\n * nhất vì nó mang THÊM huy hiệu gói và dấu check, hai thứ mà các hàng khác không có.\n * Không nới vừa đủ 4px: tên vai trò và tên workspace là dữ liệu của app, dài ngắn tuỳ nơi\n * dùng — chọn mức có dư thay vì mức vừa khít với đúng bộ dữ liệu mẫu này.\n */\n className=\"w-80 p-0\"\n onOpenAutoFocus={(event) => {\n // Mặc định Radix focus vào khung panel; có ô tìm thì đưa con trỏ vào đó luôn.\n if (!searchRef.current) return;\n event.preventDefault();\n searchRef.current.focus();\n }}\n >\n {showList ? (\n <>\n {showSearch ? (\n <div className=\"relative border-b border-border p-2\">\n <Search className=\"pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={labels.workspaceSearchPlaceholder}\n aria-label={labels.workspaceSearchPlaceholder}\n className={cn(\n \"h-8 w-full rounded-md border border-input bg-background pl-7 pr-2 text-sm\",\n // Focus dùng `--primary-ink`, KHÔNG `--ring` cũng không `--gold-300`. Cả hai\n // bậc kia ở light đều là gold #E8B824 — chỉ 1,73:1 trên nền trang, tức lúc\n // focus ô tìm MẤT ranh giới (`--ring` chỉ hợp cho vòng focus `ring-*`, thứ có\n // luật riêng, không phải cho viền control). `--primary-ink` giữ đúng sắc gold\n // mà đạt 6,09–6,51:1 (light) · 10,80–12,67:1 (dark) — hợp đồng màu §3.\n \"placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\"\n )}\n />\n </div>\n ) : null}\n\n {/*\n CỐ Ý không dùng role=\"listbox\"/\"option\": bộ ARIA đó hứa với trình đọc màn hình\n một widget điều khiển bằng ↑/↓/Home/End + roving tabindex + aria-activedescendant,\n mà hành vi thật ở đây chỉ là Tab qua từng nút trong Popover (Popover được chọn\n thay DropdownMenu vì menu của Radix cướp phím gõ của ô tìm kiếm — xem chú thích\n component). Khai đúng cái mình làm còn hơn khai sang trọng rồi lừa người dùng:\n danh sách nút thường + `aria-current` cho mục đang chọn.\n */}\n {/*\n `flex flex-col gap-1`: các hàng là khối `rounded-md` có nền khi hover/đang chọn —\n dính sát nhau thì hai nền bo góc chạm nhau và đọc thành một mảng liền, không ra ba\n mục riêng. 4px khớp đúng `p-1` của khung, nên đệm trong và khoảng cách giữa bằng nhau.\n */}\n <div\n className=\"flex max-h-64 flex-col gap-1 overflow-y-auto scrollbar-thin p-1\"\n role=\"group\"\n aria-label={labels.workspace}\n >\n {filtered.length === 0 ? (\n <p className=\"px-2 py-6 text-center text-sm text-muted-foreground\">{labels.workspaceEmpty}</p>\n ) : (\n filtered.map((workspace) => {\n const isCurrent = workspace.id === currentWorkspace.id;\n return (\n <button\n key={workspace.id}\n type=\"button\"\n aria-current={isCurrent ? \"true\" : undefined}\n onClick={() => handleSelect(workspace)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-accent\",\n isCurrent && \"bg-accent/60\"\n )}\n >\n {/* Cùng cách xử lý với trigger ở trên: avatar cao bằng khối chữ hai dòng,\n và cả hai dòng `leading-tight` để dòng phụ dính vào tên chứ không trôi\n lửng lơ. Trigger và danh sách là cùng một thứ ở hai chỗ — để hai kiểu\n khác nhau là người dùng thấy giao diện \"nhảy\" khi mở popover. */}\n <WorkspaceAvatar workspace={workspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">\n {workspace.name}\n </span>\n {workspace.role || workspace.memberCount !== undefined ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">\n {[\n workspace.role,\n workspace.memberCount === undefined\n ? undefined\n : `${workspace.memberCount} ${labels.members}`\n ]\n .filter(Boolean)\n .join(\" · \")}\n </span>\n ) : null}\n </span>\n {workspace.plan ? (\n <Badge variant=\"secondary\" size=\"sm\" className=\"shrink-0\">\n {workspace.plan}\n </Badge>\n ) : null}\n {/* Ô check LUÔN chiếm chỗ, chỉ ẩn nội dung khi không phải mục đang chọn —\n `invisible` chứ không phải render có điều kiện. Render có điều kiện làm\n badge gói thuê bao của DÒNG ĐANG CHỌN bị đẩy lệch 24px so với các dòng\n khác, nên cột badge không thẳng hàng.\n Cùng thủ pháp với `OptionRow` của `Combobox` trong gói.\n\n Và vì sao KHÔNG bỏ hẳn dấu check: mục đang chọn khi đó chỉ còn `bg-accent/60`\n để nhận biết — ~1,03:1 với nền popover, tức không dấu hiệu nào đạt ngưỡng, và\n trạng thái lại đi bằng MỖI màu (WCAG 1.4.1). `aria-current` chỉ cứu trình đọc\n màn hình, không cứu người nhìn.\n\n KHÔNG dùng `text-app-accent` cho dấu check: accent mặc định (gold #E8B824)\n chỉ đạt 1,86:1 trên nền popover trắng — dưới ngưỡng 3:1 của WCAG 1.4.11 cho\n thành phần phi-văn-bản. `text-popover-foreground` đạt 18,9:1 (light)\n và 16,6:1 (dark). */}\n <Check\n aria-hidden=\"true\"\n className={cn(\"size-4 shrink-0 text-popover-foreground\", !isCurrent && \"invisible\")}\n />\n </button>\n );\n })\n )}\n </div>\n </>\n ) : (\n <div className=\"flex items-center gap-2 p-3\">\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{currentWorkspace.name}</span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {currentWorkspace.plan ?? currentWorkspace.slug}\n </span>\n </span>\n </div>\n )}\n\n {hasActions ? (\n <>\n <Separator />\n {/* `gap-1` — cùng 4px với danh sách workspace ở trên và với khoảng cách mục của họ\n menu (`DropdownMenuItem`…). Thiếu nó thì bốn hàng hành động dính liền thành một\n khối, và vùng `hover:bg-accent` của chúng cũng liền một mảng. */}\n <div className=\"flex flex-col gap-1 p-1\">\n {onCreateWorkspace ? (\n <ActionRow icon={Plus} label={labels.createWorkspace} onClick={closeThen(onCreateWorkspace)} />\n ) : null}\n {onWorkspaceSettings ? (\n <ActionRow\n icon={Settings}\n label={labels.workspaceSettings}\n onClick={closeThen(() => onWorkspaceSettings(currentWorkspace.id))}\n />\n ) : null}\n {onInviteMembers ? (\n <ActionRow\n icon={UserPlus}\n label={labels.inviteMembers}\n onClick={closeThen(() => onInviteMembers(currentWorkspace.id))}\n />\n ) : null}\n {onLogout ? (\n <ActionRow icon={LogOut} label={labels.logout} destructive onClick={closeThen(onLogout)} />\n ) : null}\n </div>\n </>\n ) : null}\n </PopoverContent>\n </Popover>\n );\n}\n\n/** Một dòng hành động trong panel (không phải mục chọn workspace). */\nfunction ActionRow({\n icon: Icon = Building2,\n label,\n onClick,\n destructive = false\n}: {\n icon?: typeof Building2;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n // Bậc ink chứ không `text-destructive`: dòng \"Đăng xuất\" là CHỮ trên nền `--popover`,\n // mà `--destructive` (token NỀN) chỉ được 3,53:1 trên `--popover` ở dark ✗. Ink đạt cả\n // hai: 7,15:1 (light) · 6,42:1 (dark). Icon ở `opacity-70` là thành phần phi-văn-bản\n // (ngưỡng 3:1): 3,69:1 / 3,73:1 lúc nghỉ, và 3,37:1 / 3,62:1 trên nền hover\n // `bg-destructive/10` — đó mới là tổ hợp chặt nhất, vẫn ✓. Chữ lúc hover: 6,18:1 / 5,94:1.\n destructive ? \"text-destructive-ink hover:bg-destructive/10\" : \"text-foreground hover:bg-accent\"\n )}\n >\n <Icon className=\"size-4 shrink-0 opacity-70\" />\n <span className=\"truncate\">{label}</span>\n </button>\n );\n}\n"],"mappings":";AA0BI,SAsKM,UArKiB,KADvB;AAxBJ,SAAS,WAAW,OAAO,gBAAgB,QAAQ,MAAM,QAAQ,UAAU,gBAAgB;AAC3F,SAAS,SAAS,QAAQ,gBAAgB;AAC1C,SAAS,4BAA4B;AACrC,SAAS,QAAQ,gBAAgB,mBAAmB;AACpD,SAAS,aAAa;AACtB,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,iBAAiB;AAC1B,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AAEzB,SAAS,uBAAuB,mBAAmB;AAGnD,MAAM,mBAAmB;AAQzB,SAAS,gBAAgB,EAAE,WAAW,UAAU,GAAiD;AAC/F,SACE,qBAAC,UAAO,WAAW,GAAG,qBAAqB,SAAS,GACjD;AAAA,cAAU,UAAU,oBAAC,eAAY,KAAK,UAAU,SAAS,KAAI,IAAG,WAAU,cAAa,IAAK;AAAA,IAC7F,oBAAC,kBAAe,WAAW,GAAG,wCAAwC,qBAAqB,UAAU,IAAI,CAAC,GACvG,gCAAsB,UAAU,IAAI,GACvC;AAAA,KACF;AAEJ;AAsBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,QAAQ;AACV,GAA2B;AACzB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,SAAS;AAEb,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,YAAY,OAAyB,IAAI;AAE/C,QAAM,WAAW,WAAW,SAAS;AACrC,QAAM,aAAa,WAAW,UAAU;AAExC,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,CAAC,MAAM,KAAK,EAAG,QAAO;AAC1B,UAAM,SAAS,YAAY,KAAK;AAChC,WAAO,WAAW;AAAA,MAChB,CAAC,cAAc,YAAY,UAAU,IAAI,EAAE,SAAS,MAAM,KAAK,YAAY,UAAU,IAAI,EAAE,SAAS,MAAM;AAAA,IAC5G;AAAA,EACF,GAAG,CAAC,YAAY,KAAK,CAAC;AAEtB,QAAM,aAAa,eAAe,QAAQ,qBAAqB,uBAAuB,mBAAmB,QAAQ;AAEjH,MAAI,CAAC,iBAAkB,QAAO;AAE9B,QAAM,eAAe,CAAC,cAAyB;AAC7C,YAAQ,KAAK;AACb,aAAS,EAAE;AACX,QAAI,UAAU,OAAO,iBAAiB,GAAI,qBAAoB,UAAU,EAAE;AAAA,EAC5E;AAGA,QAAM,YAAY,CAAC,WAAuB,MAAM;AAC9C,YAAQ,KAAK;AACb,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,YACd;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,OAAO;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,mBAAgB,WAAW,kBAAkB;AAAA;AAAA,EAChD,IAEA;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY,OAAO;AAAA,MACnB,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MASA;AAAA,4BAAC,mBAAgB,WAAW,kBAAkB,WAAU,UAAS;AAAA,QACjE,qBAAC,UAAK,WAAU,gCAId;AAAA,8BAAC,UAAK,WAAU,wEACb,2BAAiB,MACpB;AAAA,UACC,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,YAKhB,oBAAC,UAAK,WAAU,4DAA4D,2BAAiB,MAAK;AAAA,cAChG;AAAA,WACN;AAAA,QASA,oBAAC,kBAAe,WAAU,2CAA0C;AAAA;AAAA;AAAA,EACtE;AAGF,SACE,qBAAC,WAAQ,MAAY,cAAc,SAChC;AAAA,gBACC,qBAAC,WACC;AAAA,0BAAC,kBAAe,SAAO,MACrB,8BAAC,kBAAe,SAAO,MAAE,mBAAQ,GACnC;AAAA,MACA,oBAAC,kBAAe,MAAK,SAAS,2BAAiB,MAAK;AAAA,OACtD,IAEA,oBAAC,kBAAe,SAAO,MAAE,mBAAQ;AAAA,IAGnC;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,YAAY;AAAA,QAQZ,WAAU;AAAA,QACV,iBAAiB,CAAC,UAAU;AAE1B,cAAI,CAAC,UAAU,QAAS;AACxB,gBAAM,eAAe;AACrB,oBAAU,QAAQ,MAAM;AAAA,QAC1B;AAAA,QAEC;AAAA,qBACC,iCACG;AAAA,yBACC,qBAAC,SAAI,WAAU,uCACb;AAAA,kCAAC,UAAO,WAAU,+FAA8F;AAAA,cAChH;AAAA,gBAAC;AAAA;AAAA,kBACC,KAAK;AAAA,kBACL,MAAK;AAAA,kBACL,OAAO;AAAA,kBACP,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,kBAChD,aAAa,OAAO;AAAA,kBACpB,cAAY,OAAO;AAAA,kBACnB,WAAW;AAAA,oBACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAMA;AAAA,kBACF;AAAA;AAAA,cACF;AAAA,eACF,IACE;AAAA,YAeJ;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,MAAK;AAAA,gBACL,cAAY,OAAO;AAAA,gBAElB,mBAAS,WAAW,IACnB,oBAAC,OAAE,WAAU,uDAAuD,iBAAO,gBAAe,IAE1F,SAAS,IAAI,CAAC,cAAc;AAC1B,wBAAM,YAAY,UAAU,OAAO,iBAAiB;AACpD,yBACE;AAAA,oBAAC;AAAA;AAAA,sBAEC,MAAK;AAAA,sBACL,gBAAc,YAAY,SAAS;AAAA,sBACnC,SAAS,MAAM,aAAa,SAAS;AAAA,sBACrC,WAAW;AAAA,wBACT;AAAA,wBACA;AAAA,wBACA,aAAa;AAAA,sBACf;AAAA,sBAMA;AAAA,4CAAC,mBAAgB,WAAsB,WAAU,UAAS;AAAA,wBAC1D,qBAAC,UAAK,WAAU,gCACd;AAAA,8CAAC,UAAK,WAAU,8DACb,oBAAU,MACb;AAAA,0BACC,UAAU,QAAQ,UAAU,gBAAgB,SAC3C,oBAAC,UAAK,WAAU,4DACb;AAAA,4BACC,UAAU;AAAA,4BACV,UAAU,gBAAgB,SACtB,SACA,GAAG,UAAU,WAAW,IAAI,OAAO,OAAO;AAAA,0BAChD,EACG,OAAO,OAAO,EACd,KAAK,QAAK,GACf,IACE;AAAA,2BACN;AAAA,wBACC,UAAU,OACT,oBAAC,SAAM,SAAQ,aAAY,MAAK,MAAK,WAAU,YAC5C,oBAAU,MACb,IACE;AAAA,wBAgBJ;AAAA,0BAAC;AAAA;AAAA,4BACC,eAAY;AAAA,4BACZ,WAAW,GAAG,2CAA2C,CAAC,aAAa,WAAW;AAAA;AAAA,wBACpF;AAAA;AAAA;AAAA,oBAvDK,UAAU;AAAA,kBAwDjB;AAAA,gBAEJ,CAAC;AAAA;AAAA,YAEL;AAAA,aACF,IAEA,qBAAC,SAAI,WAAU,+BACb;AAAA,gCAAC,mBAAgB,WAAW,kBAAkB,WAAU,UAAS;AAAA,YACjE,qBAAC,UAAK,WAAU,yBACd;AAAA,kCAAC,UAAK,WAAU,kDAAkD,2BAAiB,MAAK;AAAA,cACxF,oBAAC,UAAK,WAAU,8CACb,2BAAiB,QAAQ,iBAAiB,MAC7C;AAAA,eACF;AAAA,aACF;AAAA,UAGD,aACC,iCACE;AAAA,gCAAC,aAAU;AAAA,YAIX,qBAAC,SAAI,WAAU,2BACZ;AAAA,kCACC,oBAAC,aAAU,MAAM,MAAM,OAAO,OAAO,iBAAiB,SAAS,UAAU,iBAAiB,GAAG,IAC3F;AAAA,cACH,sBACC;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM;AAAA,kBACN,OAAO,OAAO;AAAA,kBACd,SAAS,UAAU,MAAM,oBAAoB,iBAAiB,EAAE,CAAC;AAAA;AAAA,cACnE,IACE;AAAA,cACH,kBACC;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM;AAAA,kBACN,OAAO,OAAO;AAAA,kBACd,SAAS,UAAU,MAAM,gBAAgB,iBAAiB,EAAE,CAAC;AAAA;AAAA,cAC/D,IACE;AAAA,cACH,WACC,oBAAC,aAAU,MAAM,QAAQ,OAAO,OAAO,QAAQ,aAAW,MAAC,SAAS,UAAU,QAAQ,GAAG,IACvF;AAAA,eACN;AAAA,aACF,IACE;AAAA;AAAA;AAAA,IACN;AAAA,KACF;AAEJ;AAGA,SAAS,UAAU;AAAA,EACjB,MAAM,OAAO;AAAA,EACb;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GAKG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMA,cAAc,iDAAiD;AAAA,MACjE;AAAA,MAEA;AAAA,4BAAC,QAAK,WAAU,8BAA6B;AAAA,QAC7C,oBAAC,UAAK,WAAU,YAAY,iBAAM;AAAA;AAAA;AAAA,EACpC;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/shell/workspace-switcher.tsx"],"sourcesContent":["\"use client\";\n\nimport { Building2, Check, ChevronsUpDown, LogOut, Plus, Search, Settings, UserPlus } from \"lucide-react\";\nimport { useMemo, useRef, useState } from \"react\";\nimport { getAvatarToneClasses } from \"../components/composite/letter-avatar.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../components/ui/avatar.js\";\nimport { Badge } from \"../components/ui/badge.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../components/ui/popover.js\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useShell } from \"./shell-context.js\";\nimport type { Workspace } from \"./types.js\";\nimport { getVietnameseInitials, normalizeVi } from \"./utils.js\";\n\n/** Từ số workspace này trở lên thì hiện ô tìm kiếm trong danh sách. */\nconst SEARCH_THRESHOLD = 8;\n\n/**\n * Ảnh đại diện workspace: logo nếu có, không thì ô chữ cái tô màu deterministic.\n *\n * Seed là `workspace.name` chứ KHÔNG phải slug: app hiển thị workspace bằng\n * `<LetterAvatar name={workspace.name} square />` ở chỗ khác, seed lệch là màu lệch.\n */\nfunction WorkspaceAvatar({ workspace, className }: { workspace: Workspace; className?: string }) {\n return (\n <Avatar className={cn(\"size-6 rounded-md\", className)}>\n {workspace.logoUrl ? <AvatarImage src={workspace.logoUrl} alt=\"\" className=\"rounded-md\" /> : null}\n <AvatarFallback className={cn(\"rounded-md text-[10px] font-semibold\", getAvatarToneClasses(workspace.name))}>\n {getVietnameseInitials(workspace.name)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\nexport interface WorkspaceSwitcherProps {\n className?: string;\n /** Sidebar thu gọn → chỉ hiện avatar + tooltip. */\n collapsed?: boolean;\n /** Ẩn khu \"Hành động\" (tạo workspace, cài đặt, mời thành viên, đăng xuất). */\n showActions?: boolean;\n /** Căn panel — mặc định \"start\" (mở xuôi theo mép trái sidebar). */\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * **Trục 1 — đổi không gian làm việc.**\n *\n * Chỉ gọi `onWorkspaceChange(id)`; điều hướng/đổi subdomain là việc của app\n * (đổi workspace kéo theo đổi toàn bộ dữ liệu + quyền nên shell không tự quyết).\n * Có ≤1 workspace thì phần danh sách tự ẩn, chỉ còn nhãn workspace hiện tại.\n *\n * Dùng `Popover` chứ không `DropdownMenu`: menu của Radix có typeahead, gõ vào\n * ô tìm kiếm bên trong sẽ bị nó cướp phím.\n */\nexport function WorkspaceSwitcher({\n className,\n collapsed = false,\n showActions = true,\n align = \"start\"\n}: WorkspaceSwitcherProps) {\n const {\n workspaces,\n currentWorkspace,\n onWorkspaceChange,\n onCreateWorkspace,\n onWorkspaceSettings,\n onInviteMembers,\n onLogout,\n labels\n } = useShell();\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const searchRef = useRef<HTMLInputElement>(null);\n\n const showList = workspaces.length > 1;\n const showSearch = workspaces.length >= SEARCH_THRESHOLD;\n\n const filtered = useMemo(() => {\n if (!query.trim()) return workspaces;\n const needle = normalizeVi(query);\n return workspaces.filter(\n (workspace) => normalizeVi(workspace.name).includes(needle) || normalizeVi(workspace.slug).includes(needle)\n );\n }, [workspaces, query]);\n\n const hasActions = showActions && Boolean(onCreateWorkspace || onWorkspaceSettings || onInviteMembers || onLogout);\n\n if (!currentWorkspace) return null;\n\n const handleSelect = (workspace: Workspace) => {\n setOpen(false);\n setQuery(\"\");\n if (workspace.id !== currentWorkspace.id) onWorkspaceChange?.(workspace.id);\n };\n\n /** Đóng panel rồi mới chạy hành động — tránh panel còn mở đè lên dialog app mở ra sau đó. */\n const closeThen = (action: () => void) => () => {\n setOpen(false);\n action();\n };\n\n const trigger = collapsed ? (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n <WorkspaceAvatar workspace={currentWorkspace} />\n </button>\n ) : (\n <button\n type=\"button\"\n aria-label={labels.workspaceSwitcherLabel}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-sidebar-accent\",\n className\n )}\n >\n {/*\n `size-8` (32px) thay vì `size-6` (24px): avatar cao BẰNG khối chữ hai dòng bên cạnh\n (17,5px + 13,75px ≈ 31px sau khi cả hai dòng dùng `leading-tight`), nên hàng này đọc ra\n một khối chứ không phải một ô vuông nhỏ lửng lơ cạnh hai dòng chữ.\n ⚠ Chỉ đổi ở trigger dạng ĐẦY ĐỦ. Trigger lúc thanh bên thu gọn vẫn `size-6` trong một nút\n 36px — ở đó avatar là toàn bộ nội dung nên nó cần đệm quanh, không cần lấp đầy.\n */}\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n {/* `leading-tight` ở CẢ hai dòng: mặc định `text-sm` có line-height 20px và dòng gói\n 11px thừa hưởng line-height của body (~1,5) — cộng lại là một khoảng hở rộng hơn cả\n thân chữ, khiến tên gói trông như không thuộc về tên workspace. */}\n <span className=\"truncate text-sm font-semibold leading-tight text-sidebar-foreground\">\n {currentWorkspace.name}\n </span>\n {currentWorkspace.plan ? (\n // `text-muted-foreground` chứ không `text-sidebar-foreground/55`: đây là CHỮ (11px) nên\n // ngưỡng là AA 4,5:1, mà /55 trên `--sidebar` #FAFAF9 chỉ được 4,13:1 ✗ (dark 5,78:1 —\n // mỗi theme hỏng một nửa). `--muted-foreground` là bậc chữ phụ đã đo cho đúng vai trò:\n // 5,01:1 (light) · 5,35:1 (dark) trên `--sidebar` ✓.\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">{currentWorkspace.plan}</span>\n ) : null}\n </span>\n {/* Mũi tên MANG THÔNG TIN (nút này mở ra một panel chọn) nên chịu ngưỡng 3:1 của WCAG\n 1.4.11, mà `text-sidebar-foreground/45` chỉ được 3,02:1 trên `--sidebar` ở light và\n tụt xuống 3,01:1 khi hover đổi nền sang `--sidebar-accent` — sát/dưới vạch, và ở\n layout \"header-first\" nút này còn nằm trên nền `--background` khác nữa.\n `--muted-foreground` là bậc đã đo và dư biên ở mọi nền nó có thể đứng: 5,01:1 trên\n `--sidebar` · 4,78:1 trên `--sidebar-accent` · 5,23:1 trên `--background` (light);\n 5,35 / 4,86 / 5,70:1 (dark) ✓. (Dark của `/45` là 4,26 / 4,14:1 — chỉ light hỏng,\n nhưng bậc mờ theo alpha luôn lệch nhau như vậy giữa hai theme.) */}\n <ChevronsUpDown className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </button>\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {collapsed ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{currentWorkspace.name}</TooltipContent>\n </Tooltip>\n ) : (\n <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n )}\n\n <PopoverContent\n align={align}\n sideOffset={6}\n /*\n * `w-80` (320px), không phải `w-72` (288px). Ở 288px dòng phụ của hàng ĐANG CHỌN bị cắt —\n * đo được thiếu đúng 4px cho \"Quản trị viên · 6 thành viên\". Hàng đang chọn là hàng chật\n * nhất vì nó mang THÊM huy hiệu gói và dấu check, hai thứ mà các hàng khác không có.\n * Không nới vừa đủ 4px: tên vai trò và tên workspace là dữ liệu của app, dài ngắn tuỳ nơi\n * dùng — chọn mức có dư thay vì mức vừa khít với đúng bộ dữ liệu mẫu này.\n */\n className=\"w-80 p-0\"\n onOpenAutoFocus={(event) => {\n // Mặc định Radix focus vào khung panel; có ô tìm thì đưa con trỏ vào đó luôn.\n if (!searchRef.current) return;\n event.preventDefault();\n searchRef.current.focus();\n }}\n >\n {showList ? (\n <>\n {showSearch ? (\n <div className=\"relative border-b border-border p-2\">\n <Search className=\"pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n ref={searchRef}\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder={labels.workspaceSearchPlaceholder}\n aria-label={labels.workspaceSearchPlaceholder}\n className={cn(\n \"h-8 w-full rounded-md border border-input bg-background pl-7 pr-2 text-sm\",\n // Focus dùng `--primary-ink`, KHÔNG `--ring` cũng không `--gold-300`. Cả hai\n // bậc kia ở light đều là gold #E8B824 — chỉ 1,73:1 trên nền trang, tức lúc\n // focus ô tìm MẤT ranh giới (`--ring` chỉ hợp cho vòng focus `ring-*`, thứ có\n // luật riêng, không phải cho viền control). `--primary-ink` giữ đúng sắc gold\n // mà đạt 6,09–6,51:1 (light) · 10,80–12,67:1 (dark) — hợp đồng màu §3.\n \"placeholder:text-muted-foreground focus:border-primary-ink focus:outline-none focus:ring-2 focus:ring-ring/50\"\n )}\n />\n </div>\n ) : null}\n\n {/*\n CỐ Ý không dùng role=\"listbox\"/\"option\": bộ ARIA đó hứa với trình đọc màn hình\n một widget điều khiển bằng ↑/↓/Home/End + roving tabindex + aria-activedescendant,\n mà hành vi thật ở đây chỉ là Tab qua từng nút trong Popover (Popover được chọn\n thay DropdownMenu vì menu của Radix cướp phím gõ của ô tìm kiếm — xem chú thích\n component). Khai đúng cái mình làm còn hơn khai sang trọng rồi lừa người dùng:\n danh sách nút thường + `aria-current` cho mục đang chọn.\n */}\n {/*\n `flex flex-col gap-1`: các hàng là khối `rounded-md` có nền khi hover/đang chọn —\n dính sát nhau thì hai nền bo góc chạm nhau và đọc thành một mảng liền, không ra ba\n mục riêng. 4px khớp đúng `p-1` của khung, nên đệm trong và khoảng cách giữa bằng nhau.\n */}\n <div\n className=\"flex max-h-64 flex-col gap-1 overflow-y-auto scrollbar-thin p-1\"\n role=\"group\"\n aria-label={labels.workspace}\n >\n {filtered.length === 0 ? (\n <p className=\"px-2 py-6 text-center text-sm text-muted-foreground\">{labels.workspaceEmpty}</p>\n ) : (\n filtered.map((workspace) => {\n const isCurrent = workspace.id === currentWorkspace.id;\n return (\n <button\n key={workspace.id}\n type=\"button\"\n aria-current={isCurrent ? \"true\" : undefined}\n onClick={() => handleSelect(workspace)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left\",\n \"transition-colors duration-150 ease-out hover:bg-accent\",\n isCurrent && \"bg-accent/60\"\n )}\n >\n {/* Cùng cách xử lý với trigger ở trên: avatar cao bằng khối chữ hai dòng,\n và cả hai dòng `leading-tight` để dòng phụ dính vào tên chứ không trôi\n lửng lơ. Trigger và danh sách là cùng một thứ ở hai chỗ — để hai kiểu\n khác nhau là người dùng thấy giao diện \"nhảy\" khi mở popover. */}\n <WorkspaceAvatar workspace={workspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium leading-tight text-foreground\">\n {workspace.name}\n </span>\n {workspace.role || workspace.memberCount !== undefined ? (\n <span className=\"truncate text-[11px] leading-tight text-muted-foreground\">\n {[\n workspace.role,\n workspace.memberCount === undefined ? undefined : labels.members(workspace.memberCount)\n ]\n .filter(Boolean)\n .join(\" · \")}\n </span>\n ) : null}\n </span>\n {workspace.plan ? (\n <Badge variant=\"secondary\" size=\"sm\" className=\"shrink-0\">\n {workspace.plan}\n </Badge>\n ) : null}\n {/* Ô check LUÔN chiếm chỗ, chỉ ẩn nội dung khi không phải mục đang chọn —\n `invisible` chứ không phải render có điều kiện. Render có điều kiện làm\n badge gói thuê bao của DÒNG ĐANG CHỌN bị đẩy lệch 24px so với các dòng\n khác, nên cột badge không thẳng hàng.\n Cùng thủ pháp với `OptionRow` của `Combobox` trong gói.\n\n Và vì sao KHÔNG bỏ hẳn dấu check: mục đang chọn khi đó chỉ còn `bg-accent/60`\n để nhận biết — ~1,03:1 với nền popover, tức không dấu hiệu nào đạt ngưỡng, và\n trạng thái lại đi bằng MỖI màu (WCAG 1.4.1). `aria-current` chỉ cứu trình đọc\n màn hình, không cứu người nhìn.\n\n KHÔNG dùng `text-app-accent` cho dấu check: accent mặc định (gold #E8B824)\n chỉ đạt 1,86:1 trên nền popover trắng — dưới ngưỡng 3:1 của WCAG 1.4.11 cho\n thành phần phi-văn-bản. `text-popover-foreground` đạt 18,9:1 (light)\n và 16,6:1 (dark). */}\n <Check\n aria-hidden=\"true\"\n className={cn(\"size-4 shrink-0 text-popover-foreground\", !isCurrent && \"invisible\")}\n />\n </button>\n );\n })\n )}\n </div>\n </>\n ) : (\n <div className=\"flex items-center gap-2 p-3\">\n <WorkspaceAvatar workspace={currentWorkspace} className=\"size-8\" />\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{currentWorkspace.name}</span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {currentWorkspace.plan ?? currentWorkspace.slug}\n </span>\n </span>\n </div>\n )}\n\n {hasActions ? (\n <>\n <Separator />\n {/* `gap-1` — cùng 4px với danh sách workspace ở trên và với khoảng cách mục của họ\n menu (`DropdownMenuItem`…). Thiếu nó thì bốn hàng hành động dính liền thành một\n khối, và vùng `hover:bg-accent` của chúng cũng liền một mảng. */}\n <div className=\"flex flex-col gap-1 p-1\">\n {onCreateWorkspace ? (\n <ActionRow icon={Plus} label={labels.createWorkspace} onClick={closeThen(onCreateWorkspace)} />\n ) : null}\n {onWorkspaceSettings ? (\n <ActionRow\n icon={Settings}\n label={labels.workspaceSettings}\n onClick={closeThen(() => onWorkspaceSettings(currentWorkspace.id))}\n />\n ) : null}\n {onInviteMembers ? (\n <ActionRow\n icon={UserPlus}\n label={labels.inviteMembers}\n onClick={closeThen(() => onInviteMembers(currentWorkspace.id))}\n />\n ) : null}\n {onLogout ? (\n <ActionRow icon={LogOut} label={labels.logout} destructive onClick={closeThen(onLogout)} />\n ) : null}\n </div>\n </>\n ) : null}\n </PopoverContent>\n </Popover>\n );\n}\n\n/** Một dòng hành động trong panel (không phải mục chọn workspace). */\nfunction ActionRow({\n icon: Icon = Building2,\n label,\n onClick,\n destructive = false\n}: {\n icon?: typeof Building2;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm\",\n \"transition-colors duration-150 ease-out\",\n // Bậc ink chứ không `text-destructive`: dòng \"Đăng xuất\" là CHỮ trên nền `--popover`,\n // mà `--destructive` (token NỀN) chỉ được 3,53:1 trên `--popover` ở dark ✗. Ink đạt cả\n // hai: 7,15:1 (light) · 6,42:1 (dark). Icon ở `opacity-70` là thành phần phi-văn-bản\n // (ngưỡng 3:1): 3,69:1 / 3,73:1 lúc nghỉ, và 3,37:1 / 3,62:1 trên nền hover\n // `bg-destructive/10` — đó mới là tổ hợp chặt nhất, vẫn ✓. Chữ lúc hover: 6,18:1 / 5,94:1.\n destructive ? \"text-destructive-ink hover:bg-destructive/10\" : \"text-foreground hover:bg-accent\"\n )}\n >\n <Icon className=\"size-4 shrink-0 opacity-70\" />\n <span className=\"truncate\">{label}</span>\n </button>\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comitor/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
-
"
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/comitor-ai/comitor-ui.git"
|
|
13
|
-
},
|
|
14
|
-
"homepage": "https://github.com/comitor-ai/comitor-ui#readme",
|
|
10
|
+
"homepage": "https://design-system.comitor.ai",
|
|
15
11
|
"bugs": {
|
|
16
|
-
"
|
|
12
|
+
"email": "contact@comitor.ai"
|
|
17
13
|
},
|
|
18
14
|
"keywords": [
|
|
19
15
|
"comitor",
|