compact-nav-menus
Sizing tokens for compact sidebars, nav panels, and menu lists. Use when building or refactoring a sidebar, settings menu, category list, filter bar, or expandable nav tree, or when a sidebar looks to
SKILL.md.claude/skills/compact-nav-menus/SKILL.md
GitHubCompact 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
NonEditableContextMenuorEditableContextMenufromfeatures/context-menu-v3/. Never use a bespokeDropdownMenuor a rawcontextmenuhandler, and never keep both. - A menu without a source is a defect. A menu with no
contentSource,entityorsurfaceNameopens with every verb dark. Fix these before unwired surfaces. - Lists built on
EntityListPageinherit the row menu throughItemContextMenu. 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. PassgetApplicationScope, forceselection: "", put names and field keys in the content (never values or notes), and pass noentity. - Done means the menu opens clean. The console shows no
INERT MENUorVALUE MAPPING GAPscream. - 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-4on the scroll content wrapper - Items:
type-secondary,px-3 py-1.5,gap-2, iconsh-3.5 w-3.5 - Active state:
bg-accent text-accent-foreground font-medium(notbg-primary) - No rounded rows: Remove
rounded-mdfrom item classes - Section separators: Replace
<Separator>andspace-y-6withborder-t border-border/40on headers - Remove sidebar titles: Drop
text-lg font-semiboldheaders; use section labels if grouping is needed - Icons: Lucide React only,
h-3.5 w-3.5for top-level,h-3 w-3for children - ScrollArea: Wrap nav content in
<ScrollArea>, notoverflow-y-auto - Mobile:
hidden md:flexon the aside; provideMobileDockor stacked list alternative