Compact Navigation & Menu Panels
<!-- rules:stamp -->
Rules from common-docs/systems/platform/ui-shell/FEATURE.md — edit there, never here (python3 common-docs/meta/scripts/one_home.py --write).
- Delete means archive, everywhere. A delete that does not archive is a defect.
- A menu regroup ships as a side-by-side demo page with a what-goes-where list, never as a
direct change.
- Every surface mounts the canonical menu. Use
NonEditableContextMenu or
EditableContextMenu from features/context-menu-v3/. Never use a bespoke DropdownMenu or a
raw contextmenu handler, and never keep both.
- A menu without a source is a defect. A menu with no
contentSource, entity or
surfaceName opens with every verb dark. Fix these before unwired surfaces.
- Lists built on
EntityListPage inherit the row menu through ItemContextMenu. They need
their own wiring only for surface-specific sections.
- A window or overlay mounts its own menu. It never delegates to the page under it.
- Reuse the domain's action registry. Never create a second one.
- Secret-bearing surfaces follow
VaultContextMenu. Pass getApplicationScope, force
selection: "", put names and field keys in the content (never values or notes), and pass no
entity.
- Done means the menu opens clean. The console shows no
INERT MENU or
VALUE MAPPING GAP scream.
- Chat is a universal agent host. Never bind a page-agent roster to it.
- Anything with an id shows it through the one canonical id component (display plus one-click copy).
- Flagship and first-run screens get a bespoke elevated stage, not the 28px app-chrome controls.
- Before a link goes to Arman, every screen on its path passes the Steve Jobs bar (walked and fixed by an Opus reviewer).
- Admin tables filter, search and sort on the server, and people are picked by name or email, never by id.
- Wasted vertical space is the worst sin; every status shown is backed by real state.
- A new output style or menu layout is a NEW function used only by the new path; never thin a shared builder.
- A page fetches only what it shows, each thing once; every removed fetch ships its on-engagement load.
<!-- rules:stamp-end -->
Look rules (type, color, components) come from the style guide: ../common-docs/systems/platform/ui-shell/STYLE-GUIDE.md — this skill only adds menu structure.
Canonical sizing and spacing rules for navigation sidebars, list panels, and compact menus across the project. Every value below is a hard default — deviate only with justification.
Sizing Tokens
Sidebar Container
| Property | Value | Notes |
|---|
| Width | w-36 to w-44 | Never wider than w-44 (~176px). Use w-36–w-40 when labels are short. |
| Background | bg-muted/30 | Subtle tint, not solid bg-card. |
| Border | border-r border-border/60 | 60% opacity, not full border-border. |
| Visibility | hidden md:flex | Always hidden on mobile; use MobileDock or stacked list instead. |
| Scroll | Wrap content in <ScrollArea className="h-full w-full"> | Never overflow-y-auto on the aside itself. |
Nav Item — Top-Level (Flat or Parent)
| Property | Value |
|---|
| Font | type-secondary |
| Padding | px-3 py-1.5 |
| Gap | gap-2 |
| Icon | h-3.5 w-3.5 |
| Active BG | bg-accent text-accent-foreground font-medium |
| Active icon | text-primary |
| Inactive | text-muted-foreground hover:text-foreground |
| Hover BG | hover:bg-muted/80 |
| Shape | No rounded corners on rows — full-bleed highlight. |
Nav Item — Child (Second Tier)
| Property | Value |
|---|
| Font | type-meta |
| Padding | pl-8 pr-3 py-1 |
| Gap | gap-1.5 |
| Icon | h-3 w-3 |
| Active | text-primary font-medium (no background) |
| Inactive icon | text-muted-foreground |
Section Headers (Grouped Lists)
| Property | Value |
|---|
| Font | type-meta uppercase tracking-wider — non-content overline chrome only; the scale has no 10px |
| Color | text-muted-foreground |
| Padding | px-3 pt-3 pb-1 |
| Separator | border-t border-border/40 above the header, not <Separator> components. |
Filter Bar / Chip Row
| Property | Value |
|---|
| Container | px-3 py-1.5 flex items-center gap-1.5 flex-wrap |
| Chip | Chip / Badge from @ai-matrx/design-system (never a hand-built pill); selected state via the component's tone |
| Search input | SearchField from @ai-matrx/design-system/controls |
| Select dropdown | Select from @ai-matrx/design-system/controls |
Footer / Status Bar
| Property | Value |
|---|
| Padding | px-4 py-1.5 or px-4 py-2 |
| Font | type-secondary (status text is content) |
| Background | bg-muted/20 |
| Border | border-t border-border/60 |
| Buttons | Button from @ai-matrx/design-system/controls |
Anti-Patterns
These are the exact mistakes to fix when refactoring existing sidebars:
| Bad | Fix |
|---|
w-52, w-64, or wider | Reduce to w-36–w-44 |
text-sm / text-xs / text-[11px] classes on nav items | Use type-secondary (child rows type-meta) |
py-2, py-3 on nav rows | Use py-1.5 (top-level) or py-1 (child) |
gap-3 between icon and label | Use gap-2 (top-level) or gap-1.5 (child) |
h-4 w-4 icons in nav | Use h-3.5 w-3.5 (top-level) or h-3 w-3 (child) |
rounded-md on nav rows | Remove — use full-bleed row highlights |
bg-primary text-primary-foreground active | Use bg-accent text-accent-foreground |
p-3, p-4 wrapper padding inside sidebar | Use py-1 with no horizontal padding on the wrapper; let rows handle px-3 |
space-y-6 between groups | Use border-t border-border/40 separator + tight header |
<Separator> component | Replace with a border-t on the section header |
bg-card sidebar background | Use bg-muted/30 |
border-border (full opacity) | Use border-border/60 |
text-lg sidebar title | Remove header entirely or use a type-meta uppercase section label |
Two-Tier Expandable Pattern
For sidebars where a top-level item reveals sub-items (like Settings > Preferences):
const isExpanded = isActive && !!item.children;
{/* Parent */}
<Link href={item.children ? `${item.href}?tab=${item.children[0].param}` : item.href}
className={cn(
'flex items-center gap-2 px-3 py-1.5 transition-colors type-secondary',
'hover:bg-muted/80',
isActive ? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:text-foreground',
)}
>
<span className={cn('shrink-0', isActive ? 'text-primary' : 'text-muted-foreground')}>
{item.icon}
</span>
<span className="truncate">{item.title}</span>
</Link>
{/* Children — only when expanded */}
{isExpanded && (
<div className="py-0.5">
{item.children.map(child => (
<Link key={child.param}
href={`${item.href}?tab=${child.param}`}
className={cn(
'flex items-center gap-1.5 pl-8 pr-3 py-1 transition-colors type-meta',
'hover:bg-muted/80',
isChildActive ? 'text-primary font-medium'
: 'text-muted-foreground hover:text-foreground',
)}
>
<span className={cn('shrink-0',
isChildActive ? 'text-primary' : 'text-muted-foreground',
)}>{child.icon}</span>
<span className="truncate">{child.title}</span>
</Link>
))}
</div>
)}
Grouped Sections Pattern
For sidebars with categorized groups (like a file manager):
<aside className="hidden md:flex w-40 shrink-0 border-r border-border/60 bg-muted/30">
<ScrollArea className="h-full w-full">
<nav>
{groups.map((group, i) => (
<div key={group.label}>
{/* Section header — acts as separator */}
<div className={cn(
'px-3 pt-3 pb-1 type-meta uppercase tracking-wider text-muted-foreground',
i > 0 && 'border-t border-border/40',
)}>
{group.label}
</div>
{group.items.map(item => (
<Link key={item.href} href={item.href}
className={cn(
'flex items-center gap-2 px-3 py-1.5 transition-colors type-secondary',
'hover:bg-muted/80',
isActive ? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:text-foreground',
)}
>
<item.icon className={cn('h-3.5 w-3.5 shrink-0',
isActive ? 'text-primary' : 'text-muted-foreground',
)} />
<span className="truncate">{item.label}</span>
</Link>
))}
</div>
))}
</nav>
</ScrollArea>
</aside>
Refactoring Checklist
When converting an existing sidebar to this system:
- Container:
w-36–w-44, bg-muted/30, border-r border-border/60
- Remove wrapper padding: No
p-3/p-4 on the scroll content wrapper
- Items:
type-secondary, px-3 py-1.5, gap-2, icons h-3.5 w-3.5
- Active state:
bg-accent text-accent-foreground font-medium (not bg-primary)
- No rounded rows: Remove
rounded-md from item classes
- Section separators: Replace
<Separator> and space-y-6 with border-t border-border/40 on headers
- Remove sidebar titles: Drop
text-lg font-semibold headers; use section labels if grouping is needed
- Icons: Lucide React only,
h-3.5 w-3.5 for top-level, h-3 w-3 for children
- ScrollArea: Wrap nav content in
<ScrollArea>, not overflow-y-auto
- Mobile:
hidden md:flex on the aside; provide MobileDock or stacked list alternative