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")}
- >
- ) : (
-
-
-
-
- )}
-
-
-