Admin Side-Nav — Design
Date: 2026-07-06 · Status: Approved (brainstorm; user picked side-nav over two-level tabs / merging)
Scope: web only — a presentation refactor of the Administration page. No backend, no routes, no i18n leaf-label changes.
Problem
Admin has grown to 13 flat Carbon tabs (Users … Backups). Past ~7 a flat TabList overflows
into scroll arrows and finding a setting means scanning 13 labels. Every tab is functionally
justified (SCIM was already folded into Directory), so nothing is deleted — the fix is
grouped presentation that scales with future features.
Design
Replace the Tabs/TabList/TabPanels in AdminPage.tsx with a left rail + content pane:
- Rail: a
<nav aria-label>with four group headers and the 13 items beneath them.
Groups (header → items): - People & access → Users, Org & positions, Roles & permissions, Directory
- Documents → Classifications, Document IDs, Retention
- Correspondence → Correspondence, Letterhead
- System → Licensing, AI, Observability, Backups
- Selection:
useState<ItemKey>('users')(matches today's default of the first tab; the
current Tabs keep no URL state either, so parity is preserved). Items are<button>s with
aria-current="page"on the active one. - Content: the selected item's existing component renders unchanged on the right. The 13
leaf components (ClassificationsTab…BackupsTab) are NOT touched; the inline Users
panel stays inline in AdminPage. - A data-driven
NAV_GROUPSarray ({titleKey, items:[{key, labelKey, render}]}) replaces
the hand-listed Tab/TabPanel pairs — adding a future page is one array entry.
Styling
New .admin-nav block in app.css using Carbon theme tokens (--cds-text-secondary group
headers in caps, --cds-layer-* hover/active, --cds-link-primary/border active accent) so
light/dark themes both work — same approach as the existing .obs-*/.admin-split blocks.
Rail is fixed-width (~13rem) and sticky; below the existing 900px breakpoint it collapses to
a wrapping horizontal list above the content (no horizontal scroll on mobile).
i18n
Reuse the existing admin.tabs.* keys for item labels (en/id already translated). Add only
four group-header keys admin.nav.people|documents|correspondence|system (en + id).
Testing
npx tsc --noEmit + npx vite build; deploy web; click-through: default lands on Users,
every one of the 13 items renders its page, active state + group headers visible in light
and dark, narrow viewport stacks. Demo untouched (no backend change).
Out of scope (YAGNI)
URL/deep-link state for the selected item (the tabs never had it; can add later), collapsing
groups, icons per item, touching any leaf tab component, app-shell SideNav integration.