# Sidebar

A collapsible side bar: on desktop, `collapsed` narrows it to an icon strip; on mobile (narrower than `mobile-breakpoint`, default 768px) it automatically becomes an overlay drawer with a backdrop. Clicking outside, the close button or Esc collapses it.

## Basic usage

Pass menu JSON via the `items` attribute (`[{label, value, icon?}]`); the default slot holds custom content.

<DemoBlock title="Default sidebar">
  <div style="height: 260px; width: 100%; display: flex">
    <oas-sidebar id="sidebar-basic" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dashboard","icon":"📊"},{"label":"Orders","value":"orders","icon":"📦"},{"label":"Settings","value":"settings","icon":"⚙️"}]'>
      <oas-tag size="small">Custom content area</oas-tag>
    </oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      Right content area
    </div>
  </div>
</DemoBlock>

## Collapsing

Clicking the bottom「«」button toggles `collapsed` (a controlled attribute; fires `oas-collapse`). In the collapsed state only icons are shown, and menu items without an `icon` are hidden automatically. `hide-toggle` hides the button (host opt-out, e.g. for static sidebars).

**Nested submenus use a flyout panel in the collapsed state**: a tree parent renders as an icon item; clicking (or hovering with a short delay) opens a submenu panel anchored to the item's inline-end (mirrored to the left in RTL) with collision avoidance. Clicking a leaf fires `oas-select` and closes the panel; nested subtrees expand inline inside the panel; Esc closes and refocuses the parent; clicking outside closes; only one flyout is open at a time. Note (contract change): clicking a collapsed-state parent only opens the flyout and **no longer fires `oas-select` for the parent itself** (a parent is not a navigable page) — if you previously relied on the parent's `oas-select` in the collapsed state, listen for `oas-select` from the flyout's leaf items instead.

<DemoBlock title="hide-toggle (no collapse button, static sidebar)">
  <div style="height: 240px; width: 100%; display: flex">
    <oas-sidebar hide-toggle items='[{"label":"Home","value":"home","icon":"star"},{"label":"Docs","value":"docs","icon":"edit"},{"label":"Settings","value":"settings","icon":"gear"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      Static sidebar: no「«」collapse button at the bottom (compare with the "Collapsing" demo).
    </div>
  </div>
</DemoBlock>

<DemoBlock title="collapsed icon state">
  <oas-space direction="vertical" style="width: 100%">
    <oas-button size="small" onclick="document.querySelector('#sidebar-collapsible').toggleAttribute('collapsed')">Toggle collapsed</oas-button>
    <div style="height: 240px; width: 100%; display: flex">
      <oas-sidebar id="sidebar-collapsible" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dashboard","icon":"📊"},{"label":"Settings","value":"settings","icon":"⚙️"}]'></oas-sidebar>
      <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
        Right content area
      </div>
    </div>
  </oas-space>
</DemoBlock>

## Mobile drawer

On narrow screens the sidebar automatically becomes an overlay drawer: the floating ☰ button at the top-left opens it, and the backdrop / close button / Esc collapses it. Tune the breakpoint with `mobile-breakpoint` (this demo uses 700px so it can be observed on a wide screen).

<DemoBlock title="Mobile drawer (breakpoint 700)">
  <div style="height: 260px; width: 100%; display: flex">
    <oas-sidebar mobile-breakpoint="700" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dashboard","icon":"📊"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      Shrink the window / preview container below 700px, then click the ☰ at the top-left to open the drawer
    </div>
  </div>
</DemoBlock>

## Controlled mobile drawer

`drawer-open` is a controlled attribute: setting/removing it externally opens/closes the mobile drawer (no need to click the floating ☰). This demo raises the breakpoint to force the mobile form so it can be observed on a wide screen.

<DemoBlock title="Controlled drawer-open">
  <oas-space>
    <oas-button type="primary" onclick="document.getElementById('sidebar-drawer').setAttribute('drawer-open','')">Open drawer (set drawer-open)</oas-button>
    <oas-button onclick="document.getElementById('sidebar-drawer').removeAttribute('drawer-open')">Close drawer (remove drawer-open)</oas-button>
  </oas-space>
  <div style="height: 260px; width: 100%; display: flex">
    <oas-sidebar id="sidebar-drawer" mobile-breakpoint="2000" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dashboard","icon":"📊"},{"label":"Orders","value":"orders","icon":"📦"},{"label":"Settings","value":"settings","icon":"⚙️"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      External buttons set <code>drawer-open</code> to control the drawer; after it is closed via the mask / ✕ / Esc, the attribute is removed.
    </div>
  </div>
</DemoBlock>

## Controlled highlight

`active` is a controlled attribute: setting/removing it externally migrates the currently highlighted item (no click needed).

<DemoBlock title="Controlled active">
  <oas-space>
    <oas-button type="primary" size="small" onclick="document.getElementById('sidebar-active').setAttribute('active','dashboard')">Highlight Dashboard</oas-button>
    <oas-button size="small" onclick="document.getElementById('sidebar-active').setAttribute('active','settings')">Highlight Settings</oas-button>
    <oas-button size="small" onclick="document.getElementById('sidebar-active').removeAttribute('active')">Clear highlight</oas-button>
  </oas-space>
  <div style="height: 240px; width: 100%; display: flex">
    <oas-sidebar id="sidebar-active" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dashboard","icon":"📊"},{"label":"Orders","value":"orders","icon":"📦"},{"label":"Settings","value":"settings","icon":"⚙️"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      External buttons set <code>active</code> to control the current highlighted item; clearing it leaves no item highlighted.
    </div>
  </div>
</DemoBlock>

## Groups

Menu `items` support an optional `group` field: consecutive items in the same group get a group title rendered before the group's first item (display-only, non-clickable). Group titles are hidden in the collapsed icon strip and shown in the mobile drawer.

<DemoBlock title="Groups (items.group)">
  <div style="height: 300px; width: 100%; display: flex">
    <oas-sidebar items='[{"label":"Dashboard","value":"dash","icon":"📊","group":"Overview"},{"label":"Live Trends","value":"trend","icon":"📈","group":"Overview"},{"label":"Orders","value":"orders","icon":"📦","group":"Business"},{"label":"Products","value":"goods","icon":"🛍️","group":"Business"},{"label":"Users","value":"users","icon":"👥","group":"Business"},{"label":"Profile","value":"me","icon":"👤"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      “Overview” and “Business” are group titles; “Profile” has no `group` field and renders flat.
    </div>
  </div>
</DemoBlock>

## Nested submenus

Items support `children` nesting: a parent item toggles expansion on click (without firing `oas-select`); a parent containing the active child auto-expands. Subtrees are hidden in the collapsed icon strip. Expand/collapse runs a smooth height animation (degrades automatically under `prefers-reduced-motion`); `accordion` makes siblings mutually exclusive — expanding one parent collapses the others at the same level (great for long navs that keep only one section open).

> **Nesting depth**: mechanically unlimited (recursive rendering; level 3 verified — per-level expansion, active-cascade auto-expansion and child-selected all work). But each level adds ~28px of indentation, so keep it to **≤3 levels** in a 220px sidebar — use `oas-tree` for deeper trees (built for arbitrary depth: connecting lines / virtual scroll).

<DemoBlock title="Three-level nesting (Business → Users → User groups)">
  <div style="height: 400px; width: 100%; display: flex">
    <oas-sidebar active="admins" items='[{"label":"Dashboard","value":"dash","icon":"star"},{"label":"Business","value":"biz","icon":"edit","children":[{"label":"Orders","value":"orders","icon":"edit"},{"label":"Users","value":"users","icon":"user","children":[{"label":"Regular users","value":"normal","icon":"user"},{"label":"Admins","value":"admins","icon":"lock"}]},{"label":"Products","value":"goods","icon":"heart"}]},{"label":"Settings","value":"sys","icon":"gear"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      The "Business → Users → User groups" three-level chain: with `active` pointing at the third level ("Admins"), both ancestors auto-expand with child-selected indication; indentation and guide lines stack per level.
    </div>
  </div>
</DemoBlock>

<DemoBlock title="Accordion (siblings mutually exclusive)">
  <div style="height: 380px; width: 100%; display: flex">
    <oas-sidebar accordion items='[{"label":"Dashboard","value":"dash","icon":"star"},{"label":"Business","value":"biz","icon":"edit","children":[{"label":"Orders","value":"orders","icon":"edit"},{"label":"Products","value":"goods","icon":"heart"},{"label":"Users","value":"users","icon":"user"}]},{"label":"Marketing","value":"mkt","icon":"mail","children":[{"label":"Campaigns","value":"acts","icon":"calendar"},{"label":"Coupons","value":"coupon","icon":"star"}]},{"label":"Settings","value":"sys","icon":"gear","children":[{"label":"Permissions","value":"perm","icon":"lock"},{"label":"Audit log","value":"audit","icon":"clock"}]}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      Click "Business", "Marketing", "Settings" in turn — only one section stays open at a time.<br />
      Each click also demos the <strong>smooth expand/collapse animation</strong> (grid height transition + chevron rotation; degrades automatically under `prefers-reduced-motion`).
    </div>
  </div>
</DemoBlock>

<DemoBlock title="Nested submenus (items.children)">
  <div style="height: 340px; width: 100%; display: flex">
    <oas-sidebar active="users" items='[{"label":"Dashboard","value":"dash","icon":"📊"},{"label":"Business","value":"biz","icon":"📦","children":[{"label":"Orders","value":"orders","icon":"edit"},{"label":"Products","value":"goods","icon":"heart"},{"label":"Users","value":"users","icon":"user"}]},{"label":"System","value":"sys","icon":"⚙️","children":[{"label":"Permissions","value":"perm","icon":"lock"},{"label":"Audit Log","value":"audit"}]}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      “Business” auto-expands because it contains the active child (Users); clicking a parent only toggles expansion, clicking a child fires selection.
    </div>
  </div>
</DemoBlock>

## Badges and item actions

Items support `badge` count badges (colors via `--oas-sidebar-badge-bg/-color` variables) and `actions` hover action buttons (clicking fires `oas-action` without selecting).

<DemoBlock title="Badges and item actions">
  <div style="height: 260px; width: 100%; display: flex">
    <oas-sidebar id="sidebar-badge-action" onoas-action="sidebarActionLog(event)" items='[{"label":"Inbox","value":"inbox","icon":"📥","badge":"12"},{"label":"Notifications","value":"notice","icon":"🔔","badge":"3"},{"label":"Projects","value":"proj","icon":"📁","actions":[{"icon":"✏️","value":"edit","label":"Edit"},{"icon":"🗑️","value":"delete","label":"Delete"}]}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      <p>Hover “Projects” to reveal action buttons; clicking fires an <code>oas-action</code> event.</p>
      <oas-tag id="sidebar-action-log" type="info">No action yet</oas-tag>
    </div>
  </div>
</DemoBlock>

## Custom icons and coloring

Item `icon` defaults to the built-in `@oas-ui/icons` registry; app-level custom icons go through the official `registerIcon(name, svg)` path (exported by `@oas-ui/ui`) — **register once, and `<oas-icon>`, the sidebar and any future consumers all see it** (do not mutate the built-in icon data; on-demand mode using built-in names needs a one-time `import '@oas-ui/icons/register'`); a same-name registration overrides the built-in icon.

`iconColor` sets a per-item target color: when present it is fixed, taking precedence over the default disabled/active-state colors; when absent it falls back to `currentColor` and follows the state (active uses the primary color). Registered colored SVGs (paths with their own `stroke`/`fill`) keep their own color naturally — the outer `stroke` never overrides them.

<DemoBlock title="Custom icons & coloring (registerIcon + iconColor)">
  <div style="height: 300px; width: 100%; display: flex">
    <oas-sidebar id="sidebar-custom-icon" items='[{"label":"Workbench","value":"workbench","icon":"oas-rocket","iconColor":"var(--oas-color-warning)"},{"label":"Repositories","value":"repo","icon":"oas-folder","iconColor":"var(--oas-color-primary)"},{"label":"Team feed","value":"team","icon":"oas-heart"},{"label":"Built-in star","value":"star","icon":"star","iconColor":"var(--oas-color-danger)"},{"label":"Built-in heart","value":"heart","icon":"heart"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      “Workbench” is fixed to the warning color and “Repositories” to the theme primary (<code>iconColor</code> present → does not follow the active state); “Team feed” has no <code>iconColor</code> so it follows the state, and its custom SVG keeps its own red stroke. Built-in icons take <code>iconColor</code> too: <strong>“Built-in star” uses the registry name <code>star</code> + a forced red</strong> (the built-in monochrome path’s <code>currentColor</code> is also replaced); “Built-in heart” without one follows the state. The tooltip icon in the collapsed strip shares the same color.
    </div>
  </div>
</DemoBlock>

## Dividers and skeleton

A `{type:"divider"}` entry renders a divider line; the `loading` attribute shows a pulsing skeleton (the value sets the row count, default 4).

<DemoBlock title="Divider + loading">
  <oas-space style="width: 100%">
    <oas-button size="small" onclick="document.getElementById('sidebar-loading').toggleAttribute('loading')">Toggle loading</oas-button>
    <div style="height: 280px; width: 100%; display: flex">
      <oas-sidebar id="sidebar-loading" items='[{"label":"Dashboard","value":"dash","icon":"📊"},{"type":"divider"},{"label":"Orders","value":"orders","icon":"📦"},{"label":"Users","value":"users","icon":"👥"}]'></oas-sidebar>
      <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">Click the button to toggle the skeleton.</div>
    </div>
  </oas-space>
</DemoBlock>

## Shortcut and keyboard navigation

The `shortcut` attribute enables `Ctrl/Cmd+B` collapse toggling (off by default to avoid hijacking a global key); the menu supports `↑/↓` focus movement, `Home/End` jumps, and `Enter/Space` activation.

<DemoBlock title="Shortcut + keyboard navigation">
  <div style="height: 260px; width: 100%; display: flex">
    <oas-sidebar shortcut items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Data","value":"data","icon":"📊"},{"label":"Settings","value":"settings","icon":"⚙️"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      Press <oas-kbd>Ctrl</oas-kbd>+<oas-kbd>B</oas-kbd> to collapse/expand; focus the menu and navigate with arrow keys.
    </div>
  </div>
</DemoBlock>

## Hover expansion (expand-on-hover)

The `expand-on-hover` attribute temporarily expands the collapsed icon strip on hover (pure visual state — does not change the controlled `collapsed` attribute).

<DemoBlock title="expand-on-hover">
  <oas-space direction="vertical" style="width: 100%">
    <oas-button size="small" onclick="document.getElementById('sidebar-hover').toggleAttribute('collapsed')">Collapse to icon strip first</oas-button>
    <div style="height: 260px; width: 100%; display: flex">
      <oas-sidebar id="sidebar-hover" expand-on-hover collapsed items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dash","icon":"📊"},{"label":"Settings","value":"settings","icon":"⚙️"}]'></oas-sidebar>
      <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
        Hover the icon strip to temporarily expand and reveal labels; moving away collapses it again.
      </div>
    </div>
  </oas-space>
</DemoBlock>

## Variants

The `variant` attribute: `sidebar` (default, flush) / `floating` (margin + radius + shadow) / `inset` (margin + radius + background contrast).

<DemoBlock title="variant: floating / inset">
  <oas-space style="width: 100%">
    <div style="height: 240px; flex: 1; display: flex; background: var(--oas-color-bg-hover); padding: var(--oas-space-2)">
      <oas-sidebar variant="floating" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Settings","value":"s","icon":"⚙️"}]'></oas-sidebar>
      <div style="flex: 1; padding: var(--oas-space-4)">floating</div>
    </div>
    <div style="height: 240px; flex: 1; display: flex; background: var(--oas-color-bg); padding: var(--oas-space-2)">
      <oas-sidebar variant="inset" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Settings","value":"s","icon":"⚙️"}]'></oas-sidebar>
      <div style="flex: 1; padding: var(--oas-space-4)">inset</div>
    </div>
  </oas-space>
</DemoBlock>

## Right side and dual sidebars

`side="right"`: the mobile drawer slides in from the right with the trigger on the right; multiple sidebars can coexist (one on each side, with independent state).

<DemoBlock title="side=right + dual sidebars">
  <div style="height: 280px; width: 100%; display: flex">
    <oas-sidebar mobile-breakpoint="2000" items='[{"label":"Main nav","value":"main","icon":"🏠"},{"label":"Data","value":"data","icon":"📊"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      “Main nav” on the left and “Auxiliary panel” (side=right) on the right — each opens/closes independently (this demo forces the mobile state for observation).
    </div>
    <oas-sidebar side="right" mobile-breakpoint="2000" items='[{"label":"Auxiliary panel","value":"aux","icon":"🔧"},{"label":"Logs","value":"log","icon":"📋"}]'></oas-sidebar>
  </div>
</DemoBlock>

## Resizable width (resizable)

The `resizable` attribute shows a drag rail on the host edge for real-time resizing (writes back to the `width` attribute); `resize-min`/`resize-max` clamp the range (default 160–480); the rail supports arrow keys (±8px, `Home/End` jumps to min/max); `oas-resize` fires on drag end or key adjustments. Desktop non-collapsed only (hidden when collapsed or on mobile).

<DemoBlock title="resizable edge drag-resize">
  <div style="height: 280px; width: 100%; display: flex">
    <oas-sidebar id="sidebar-resizable" resizable resize-min="180" resize-max="400" onoas-resize="sidebarResizeLog(event)" width="220px" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dash","icon":"📊"},{"label":"Settings","value":"settings","icon":"⚙️"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      Drag the rail on the sidebar's right edge to resize in real time; focus the rail and use arrow keys too.
      <oas-tag id="sidebar-resize-log" type="info">Current width 220px</oas-tag>
    </div>
  </div>
</DemoBlock>

## Resizable width (oas-splitter)

For resizable width, compose with `oas-splitter` instead of a built-in rail: place the sidebar in the left pane and drag the splitter handle to resize.

<DemoBlock title="oas-splitter + sidebar">
  <div style="height: 280px; width: 100%">
    <oas-splitter percent="22" min="12" max="45">
      <oas-sidebar slot="left" width="100%" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Data","value":"data","icon":"📊"},{"label":"Settings","value":"s","icon":"⚙️"}]'></oas-sidebar>
      <div slot="right" style="padding: var(--oas-space-4)">Drag the splitter handle to resize the sidebar (it fills the left pane with `width="100%"`, tracking the pane width in real time).</div>
    </oas-splitter>
  </div>
</DemoBlock>

## Custom width

The `width` attribute overrides the expanded width (defaults to the `--oas-sidebar-width` token); the collapsed state still narrows to an icon strip.

<DemoBlock title="width attribute">
  <oas-space>
    <oas-button size="small" onclick="document.getElementById('sidebar-width').setAttribute('width','180px')">180px</oas-button>
    <oas-button size="small" onclick="document.getElementById('sidebar-width').setAttribute('width','280px')">280px</oas-button>
    <oas-button size="small" onclick="document.getElementById('sidebar-width').removeAttribute('width')">Default token</oas-button>
  </oas-space>
  <div style="height: 260px; width: 100%; display: flex">
    <oas-sidebar id="sidebar-width" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dashboard","icon":"📊"},{"label":"Settings","value":"settings","icon":"⚙️"}]'></oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      Click the buttons above to switch the expanded width and watch the sidebar change.
    </div>
  </div>
</DemoBlock>

## Combining with oas-layout

`oas-sidebar` can be used directly as the sider of `oas-layout` (slot="sider").

<DemoBlock title="sidebar as the sider">
  <oas-layout style="height: 300px; width: 100%">
    <oas-header slot="header">Header area</oas-header>
    <oas-sidebar slot="sider" items='[{"label":"Home","value":"home","icon":"🏠"},{"label":"Dashboard","value":"dashboard","icon":"📊"}]'>
      <oas-tag size="small">Side content</oas-tag>
    </oas-sidebar>
    <oas-content slot="content">
      <oas-space direction="vertical" style="width: 100%">
        <p>Main content area.</p>
        <oas-tag type="primary">flex 1</oas-tag>
      </oas-space>
    </oas-content>
    <oas-footer slot="footer">Footer info</oas-footer>
  </oas-layout>
</DemoBlock>

## Declarative child channel

Besides the `items` JSON, items can be declared with `<oas-sidebar-item>` / `<oas-sidebar-divider>` child elements (the `items` attribute **takes priority when explicitly set**; otherwise children are parsed and converge to the same rendering path). The default slot text becomes the label; attributes map to the `SidebarItem` fields: `value` / `icon` / `group` / `badge` (all-digit strings convert to `number`, otherwise stay `string`); nesting child elements directly inside `<oas-sidebar-item>` recursively becomes the nested `children` (dividers allowed). Child additions/removals, attribute and text changes re-render automatically (MutationObserver). **Boundary**: `actions` (hover action buttons) are an object array that does not fit scalar attribute mapping — use the `items` JSON when you need them.

<DemoBlock title="Declarative children (group / divider / nested / badge)">
  <div style="height: 340px; width: 100%; display: flex">
    <oas-sidebar id="sidebar-decl" active="users">
      <oas-sidebar-item value="dash" icon="star" group="Overview">Dashboard</oas-sidebar-item>
      <oas-sidebar-item value="trend" icon="star" group="Overview">Live Trends</oas-sidebar-item>
      <oas-sidebar-divider></oas-sidebar-divider>
      <oas-sidebar-item value="biz" icon="star">Business
        <oas-sidebar-item value="orders">Orders</oas-sidebar-item>
        <oas-sidebar-item value="users">Users</oas-sidebar-item>
      </oas-sidebar-item>
      <oas-sidebar-item value="inbox" icon="star" badge="12">Inbox</oas-sidebar-item>
    </oas-sidebar>
    <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
      “Overview” is a group title; divider, nested submenu (auto-expanded by the active child) and badge behave identically to the `items` channel.
    </div>
  </div>
</DemoBlock>

<DemoBlock title="Dynamic add/remove (MutationObserver auto-refresh)">
  <oas-space direction="vertical" style="width: 100%">
    <oas-button size="small" onclick="sidebarDeclAdd()">Add an item</oas-button>
    <div style="height: 200px; width: 100%; display: flex">
      <oas-sidebar id="sidebar-decl-dyn">
        <oas-sidebar-item value="home" icon="star">Home</oas-sidebar-item>
        <oas-sidebar-item value="settings" icon="star">Settings</oas-sidebar-item>
      </oas-sidebar>
      <div style="flex: 1; min-width: 0; padding: var(--oas-space-4); background: var(--oas-color-bg)">
        Click the button to append a menu item; the sidebar refreshes automatically.
      </div>
    </div>
  </oas-space>
</DemoBlock>

## API

### Methods

| Method | Description |
| --- | --- |
| `openDrawer()` | Opens the mobile drawer programmatically (same as clicking the built-in trigger `part="trigger"`) |
| `closeDrawer()` | Closes the mobile drawer programmatically (same as mask click / close button / Esc) |

- Usage: `const el = document.querySelector('oas-sidebar') as OASSidebar; el.openDrawer()`.
- Equivalent to the controlled `drawer-open` attribute: the methods are sugar for setting/removing it (`openDrawer` sets, `closeDrawer` removes) and can be mixed; repeated calls are idempotent.
- Mobile only (viewport narrower than `mobile-breakpoint`); on desktop the attribute is reclaimed by `update` immediately, leaving no floating layer.
- Drawer open/close emits no event: observe the `drawer-open` attribute (MutationObserver) or keep host state; for a two-state toggle, call `openDrawer()` / `closeDrawer()` based on your own state.

### oas-sidebar

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `accordion` | Accordion mode (when present: expanding one parent collapses the other parents at the same level; multiple parents can be open by default) | `boolean` | — |
| `active` | Value of the currently-highlighted menu item (controlled: set/clear immediately re-renders highlight) | `string` | — |
| `collapsed` | Controlled collapse to an icon strip (present means collapsed) | `boolean` | — |
| `drawer-open` | Mobile drawer open state (controlled: set opens, clear closes; auto-removed when breakpoint returns to desktop) | `boolean` | — |
| `expand-on-hover` | Temporarily expand the collapsed icon strip on hover (visual only; does not change controlled collapsed) | — | — |
| `hide-toggle` | Hide the bottom collapse button (a built-in toggle always visible on desktop; presence hides it — host opt-out for static sidebars) | `boolean` | — |
| `items` | Menu items JSON `[{label, value, icon?, iconColor?, group?, badge?, children?, actions?}]` (supports divider entries `{type:"divider"}`; children for nested submenus) | `SidebarEntry[] \| string` | `[]` |
| `loading` | Skeleton loading state (shows a pulsing skeleton when present; value sets row count, default 4) | `string` | `4` |
| `mobile-breakpoint` | Mobile breakpoint (px); narrower than this becomes an overlay drawer | — | — |
| `resizable` | Edge drag-resize (shows a drag rail on the host edge; desktop non-collapsed only) | `boolean` | — |
| `resize-max` | Maximum resize width (px, default 480) | `string` | `480` |
| `resize-min` | Minimum resize width (px, default 160) | `string` | `160` |
| `shortcut` | Enable Ctrl/Cmd+B collapse toggling (off by default to avoid hijacking a global key) | `boolean` | — |
| `side` | Drawer side: left (default) / right (mobile drawer slides from the right, trigger on the right) | — | — |
| `variant` | Variant: sidebar (default flush) / floating (radius + shadow) / inset (radius + background contrast) | — | — |
| `width` | Expanded width; defaults to the `--oas-sidebar-width` token | `string` | `0` |

#### Events

| Event | Description |
| --- | --- |
| `oas-action` | `detail: { value: string, action: string, label: string }`; When fired: an item hover action button is clicked (does not fire oas-select) |
| `oas-collapse` | `detail: { collapsed: boolean }`; When fired: Desktop collapse button toggled |
| `oas-resize` | `detail: { width: number }`; When fired: drag-resize ends / arrow keys adjust the width |
| `oas-select` | `detail: { value: string, label: string }`; When fired: A menu item was selected (also collapses the drawer on mobile) |

#### Slots

| Name | Description |
| --- | --- |
| default | Menu item content (default slot text; direct `<oas-sidebar-item>` children recursively become nested `children`) |
| `footer` | Custom sidebar footer content |
| `header` | Custom sidebar header content |

#### CSS Variables

| CSS Variable | Description | Default |
| --- | --- | --- |
| `--oas-sidebar-badge-bg` | Badge background | `var(--oas-color-primary)` |
| `--oas-sidebar-badge-color` | Badge text color | `#fff` |
| `--oas-sidebar-bg` | Sidebar background; defaults stay anchored to base tokens (theme / dark / brand flow through automatically), host override recolors the whole bar | `var(--oas-color-bg-hover)` |
| `--oas-sidebar-collapsed-width` | Collapsed icon-rail width | `64px` |
| `--oas-sidebar-item-hover-bg` | Item hover background (defaults to a text-primary 6% mix) | `color-mix(in srgb, var(--oas-color-text-primary) 6%, transparent)` |
| `--oas-sidebar-width` | Expanded width (the `width` attribute takes precedence) | `220px` |

### oas-sidebar-item

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `badge` | Count badge (all-digit strings convert to `number`, otherwise stay `string`; matches `SidebarItem.badge`'s `string \| number`) | — | — |
| `group` | Group name (consecutive items in the same group get a group title before the group's first item) | — | — |
| `icon` | Leading icon (`@oas-ui/icons` registry icon name; shown in the collapsed icon strip) | — | — |
| `icon-color` | Icon color: when set, fixes the icon color (replaces the built-in mono path's currentColor, taking precedence over disabled/active state colors); defaults to currentColor so the color follows states | — | — |
| `value` | Selection value (data-carrier field of the declarative child channel) | — | — |

#### Slots

| Name | Description |
| --- | --- |
| default | Sidebar item label content (default slot text); direct child `<oas-sidebar-item>` (and `<oas-sidebar-divider>`) elements recursively become the nested `children` |

### oas-sidebar-divider

#### Slots

| Name | Description |
| --- | --- |
| default | Divider data carrier (no attributes; the host parses it as `{type:"divider"}`) |

### Parts

`root` / `panel` / `head` / `close` / `nav` / `body` / `foot` / `toggle` (desktop collapse) / `trigger` (mobile trigger) / `mask` / `item`; the header and footer content are injected via `slot="header"`, the default slot and `slot="footer"` respectively.

<script setup>
import { onMounted } from 'vue'
onMounted(async () => {
  // Custom icons go through the official registerIcon path (one registration, shared by oas-icon and the sidebar)
  const ui = await import('@oas-ui/ui')
  ui.registerIcon(
    'oas-rocket',
    '<path d="M8 2.2 C9.5 3 10.6 4.6 10.6 6.9 C10.6 9 9.8 11 9 12.5 H7 C6.2 11 5.4 9 5.4 6.9 C5.4 4.6 6.5 3 8 2.2 Z"/><path d="M8 6.2 V8.6 M6.5 12.5 H9.5 M7 12.5 V14 M9 12.5 V14"/>',
  )
  ui.registerIcon('oas-folder', '<path d="M3 4.5 H6.1 L7.3 6 H13 V11.5 H3 Z"/><path d="M3 6.5 H13"/>')
  ui.registerIcon(
    'oas-heart',
    '<path d="M8 13.5 C7.6 13.1 4.8 10.6 3 8.5 C1.5 6.8 1.2 5.3 2 4.1 C2.8 2.9 4.4 2.7 5.7 3.5 C6.5 4 7.3 5 8 6 C8.7 5 9.5 4 10.3 3.5 C11.6 2.7 13.2 2.9 14 4.1 C14.8 5.3 14.5 6.8 13 8.5 C11.2 10.6 8.4 13.1 8 13.5 Z" stroke="var(--oas-color-danger)"/>',
  )
  // Re-set items after registration to trigger a re-render so the lookup hits the custom icons
  // (the first render happens before registration completes and would fall back to text)
  const customSidebar = document.getElementById('sidebar-custom-icon')
  if (customSidebar) customSidebar.setAttribute('items', customSidebar.getAttribute('items') || '')
  window.sidebarActionLog = (e) => {
    const tag = document.getElementById('sidebar-action-log')
    if (tag) tag.textContent = `Action: ${e.detail.label} (${e.detail.action})`
  }
  window.sidebarResizeLog = (e) => {
    const tag = document.getElementById('sidebar-resize-log')
    if (tag) tag.textContent = `Current width ${e.detail.width}px`
  }
  window.sidebarDeclAdd = () => {
    const sidebar = document.getElementById('sidebar-decl-dyn')
    if (!sidebar) return
    const n = sidebar.children.length + 1
    const item = document.createElement('oas-sidebar-item')
    item.setAttribute('value', `dyn-${n}`)
    item.textContent = `Dynamic item ${n}`
    sidebar.appendChild(item)
  }
})
</script>
