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

GitHub

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).

  1. Delete means archive, everywhere. A delete that does not archive is a defect.
  2. A menu regroup ships as a side-by-side demo page with a what-goes-where list, never as a direct change.
  3. 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.
  4. 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.
  5. Lists built on EntityListPage inherit the row menu through ItemContextMenu. They need their own wiring only for surface-specific sections.
  6. A window or overlay mounts its own menu. It never delegates to the page under it.
  7. Reuse the domain's action registry. Never create a second one.
  8. 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.
  9. Done means the menu opens clean. The console shows no INERT MENU or VALUE MAPPING GAP scream.
  10. Chat is a universal agent host. Never bind a page-agent roster to it.
  11. Anything with an id shows it through the one canonical id component (display plus one-click copy).
  12. Flagship and first-run screens get a bespoke elevated stage, not the 28px app-chrome controls.
  13. Before a link goes to Arman, every screen on its path passes the Steve Jobs bar (walked and fixed by an Opus reviewer).
  14. Admin tables filter, search and sort on the server, and people are picked by name or email, never by id.
  15. Wasted vertical space is the worst sin; every status shown is backed by real state.
  16. A new output style or menu layout is a NEW function used only by the new path; never thin a shared builder.
  17. 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:

  1. Container: w-36–w-44, bg-muted/30, border-r border-border/60
  2. Remove wrapper padding: No p-3/p-4 on the scroll content wrapper
  3. Items: type-secondary, px-3 py-1.5, gap-2, icons h-3.5 w-3.5
  4. Active state: bg-accent text-accent-foreground font-medium (not bg-primary)
  5. No rounded rows: Remove rounded-md from item classes
  6. Section separators: Replace <Separator> and space-y-6 with border-t border-border/40 on headers
  7. Remove sidebar titles: Drop text-lg font-semibold headers; use section labels if grouping is needed
  8. Icons: Lucide React only, h-3.5 w-3.5 for top-level, h-3 w-3 for children
  9. ScrollArea: Wrap nav content in <ScrollArea>, not overflow-y-auto
  10. Mobile: hidden md:flex on the aside; provide MobileDock or stacked list alternative