diff --git a/apps/frontend/scripts/token-lint-baseline.json b/apps/frontend/scripts/token-lint-baseline.json index 938a1b64..67525ef2 100644 --- a/apps/frontend/scripts/token-lint-baseline.json +++ b/apps/frontend/scripts/token-lint-baseline.json @@ -36,7 +36,7 @@ "src/features/projects/components/SourceFileViewer.tsx": 1, "src/features/projects/components/VulnerabilityStatusBadge.tsx": 3, "src/features/scan/SbomConformancePanel.tsx": 2, - "src/features/scan/ScanProgress.tsx": 5, + "src/features/scan/ScanProgress.tsx": 3, "src/features/scan/ToolLogLine.tsx": 6, "src/features/scans/ScanCancelButton.tsx": 1, "src/pages/auth/DemoCredentialsHint.tsx": 12, diff --git a/apps/frontend/src/components/AppShell.tsx b/apps/frontend/src/components/AppShell.tsx index 645de637..aec43bda 100644 --- a/apps/frontend/src/components/AppShell.tsx +++ b/apps/frontend/src/components/AppShell.tsx @@ -10,6 +10,7 @@ import { Scale, ScanLine, UserCircle2, + ChevronDown, Activity, Building2, ClipboardList, @@ -27,15 +28,24 @@ import { useCommandMenuShortcut, } from "@/components/CommandMenu"; import { BrandMark } from "@/components/BrandMark"; -import { BrandWordmark } from "@/components/BrandWordmark"; import { DemoBanner } from "@/components/DemoBanner"; import { HeaderBell } from "@/components/HeaderBell"; import { LanguageToggle } from "@/components/LanguageToggle"; import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import { deriveInitials } from "@/lib/initials"; import { cn } from "@/lib/utils"; import { useAuthStore } from "@/stores/authStore"; +import { useActiveTeam } from "@/hooks/useActiveTeam"; import { useUIStore } from "@/stores/uiStore"; interface NavItem { @@ -47,48 +57,69 @@ interface NavItem { end?: boolean; } -const MAIN_NAV: NavItem[] = [ - { - // W9-#50 — dedicated Dashboard at "/". Lives above Projects so it's the - // first landing point for authenticated users, matching the commercial - // SCA tool convention of opening on a portfolio overview rather - // than a list view. `end` keeps the active state from spilling onto every - // /projects/* descendant route. - to: "/", - labelKey: "nav.dashboard", - icon: LayoutDashboard, - testId: "nav-dashboard", - end: true, - }, - { - to: "/projects", - labelKey: "nav.projects", - icon: FolderOpen, - testId: "nav-projects", - }, - { - to: "/scans", - labelKey: "nav.scans", - icon: ScanLine, - testId: "nav-scans", - }, - { - to: "/approvals", - labelKey: "nav.approvals", - icon: ClipboardCheck, - testId: "nav-approvals", - }, +/** + * Grouped navigation (W14). + * + * The flat list did not say what kind of place each destination was. Under + * headings, the sidebar states the product's shape on sight: a portfolio you + * survey, operations you run, and administration. That reading is the thing + * a single-scan tool cannot borrow — its left rail is the table of contents + * for one result. + */ +interface NavGroup { + labelKey: string; + items: NavItem[]; +} + +const MAIN_NAV: NavGroup[] = [ { - to: "/policies", - labelKey: "nav.policies", - icon: Scale, - testId: "nav-policies", + labelKey: "nav.group.portfolio", + items: [ + { + // W9-#50 — dedicated Dashboard at "/". `end` keeps the active state + // from spilling onto every /projects/* descendant route. + to: "/", + labelKey: "nav.dashboard", + icon: LayoutDashboard, + testId: "nav-dashboard", + end: true, + }, + { + to: "/projects", + labelKey: "nav.projects", + icon: FolderOpen, + testId: "nav-projects", + }, + ], }, { - to: "/integrations", - labelKey: "nav.integrations", - icon: KeyRound, - testId: "nav-integrations", + labelKey: "nav.group.operations", + items: [ + { + to: "/scans", + labelKey: "nav.scans", + icon: ScanLine, + testId: "nav-scans", + }, + { + to: "/approvals", + labelKey: "nav.approvals", + icon: ClipboardCheck, + testId: "nav-approvals", + }, + { + to: "/policies", + labelKey: "nav.policies", + icon: Scale, + testId: "nav-policies", + }, + { + to: "/integrations", + labelKey: "nav.integrations", + icon: KeyRound, + testId: "nav-integrations", + }, + ], }, ]; @@ -164,16 +195,32 @@ function NavItemLink({ // W11-F polish — sidebar nav hover/active transitions land on the // W11-A 150 ms ease-out-soft tokens for parity with every other // hoverable affordance (buttons, dropdown items, tabs). - "flex items-center rounded-md py-2 text-sm font-medium transition-colors duration-fast ease-out-soft", + "relative flex items-center rounded-md py-2 text-sm font-medium transition-colors duration-fast ease-out-soft", collapsed ? "justify-center px-2" : "gap-2 px-3", "hover:bg-accent hover:text-accent-foreground", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", - isActive ? "bg-primary/10 text-primary" : "text-foreground", + // W14 — the active row is the brand's one job in the sidebar: a + // teal indicator bar plus a tinted ground. The label stays ink + // rather than teal, so legibility never depends on the accent + // and colour is not the only thing marking the row. + isActive + ? "bg-brand-subtle text-foreground before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-full before:bg-brand" + : "text-foreground", ) } > - - {collapsed ? null : {label}} + {({ isActive }) => ( + <> + + {collapsed ? null : {label}} + + )} ); @@ -200,38 +247,33 @@ function SidebarNav({ const { t } = useTranslation(); return ( <> -
- {collapsed ? ( - <> - - {t("app.name")} - - ) : ( - - - - - )} -
- -