{
  "name": "responsive-shell-sidebar",
  "kind": "page",
  "primary": "body",
  "page": "/packages/web-components/patterns/responsive-shell-sidebar/responsive-shell-sidebar.examples.html",
  "slots": [
    {
      "name": "leading-inline",
      "tagName": "aside",
      "html": "<pane-ui>\n        <header>\n          <h3>Tokens</h3>\n        </header>\n        <section>\n          <list-ui>\n            <list-item-ui>Primary</list-item-ui>\n            <list-item-ui>Secondary</list-item-ui>\n            <list-item-ui>Neutral</list-item-ui>\n            <list-item-ui>Surface</list-item-ui>\n          </list-ui>\n        </section>\n      </pane-ui>"
    },
    {
      "name": "main",
      "tagName": "main",
      "html": "<!-- ═══ Narrow toolbar (drawer triggers) ═══ -->\n      <row-ui class=\"toolbar\" data-chunk-slot=\"narrow-toolbar\">\n        <button-ui id=\"open-leading\" text=\"Tokens\" icon=\"list\" variant=\"ghost\" size=\"sm\"></button-ui>\n        <button-ui id=\"open-trailing\" text=\"Settings\" icon=\"sliders\" variant=\"ghost\" size=\"sm\"></button-ui>\n      </row-ui>\n\n      <card-ui>\n        <header>\n          <h2>Responsive Shell Sidebar pattern</h2>\n        </header>\n        <section>\n          <text-ui>\n            Resize this viewport: at ≤900px the leading + trailing sidebars collapse\n            to drawers triggered by the toolbar buttons. Above 900px the sidebars are\n            inline. The same content is mounted in both surfaces; consumer signals\n            drive both copies via the parent template's re-read.\n          </text-ui>\n        </section>\n      </card-ui>\n\n      <div class=\"doc-body\">\n        <h2>The pattern</h2>\n        <p>Three pieces:</p>\n        <ol>\n          <li><strong>Inline mounts</strong> for the sidebars wrapped in <code>.wide-only</code> (display: contents at &gt;900px; display: none at ≤900px).</li>\n          <li><strong>Drawer twins</strong> with the same children, mounted outside the shell's named slots, controlled by toolbar buttons inside <code>.narrow-only</code>.</li>\n          <li><strong>MediaQueryList</strong> listener that auto-closes the drawers on viewport widening (the gap FEEDBACK-06 §3 explicitly flagged).</li>\n        </ol>\n\n        <h2>Dual-mount caveat</h2>\n        <p>Components with <strong>local DOM state</strong> (uncontrolled form inputs, scroll position, focused element) will lose state when the viewport crosses the breakpoint — the inline copy unmounts and the drawer copy mounts (or vice versa). <strong>Signal-bound components are safe</strong> because their state lives in the signal, not the DOM. Most AdiaUI primitives are signal-backed; the affected cases are:</p>\n        <ul>\n          <li>Native <code>&lt;input&gt;</code>/<code>&lt;textarea&gt;</code> with no signal binding</li>\n          <li>Components using <code>this.querySelector(...)</code> with imperative state</li>\n          <li>Scroll-position-sensitive containers (e.g. chat threads at a specific scroll)</li>\n        </ul>\n        <p>For these, either: (a) lift the state to a signal in the parent, or (b) document the unmount-on-resize edge.</p>\n\n        <h2>Auto-close on viewport widening</h2>\n        <code-ui language=\"javascript\">const mql = window.matchMedia('(max-width: 900px)');\nmql.addEventListener('change', (e) =&gt; {\n  if (!e.matches) {\n    // Viewport widened past breakpoint — close any open drawers\n    document.getElementById('leading-drawer').open = false;\n    document.getElementById('trailing-drawer').open = false;\n  }\n});</code-ui>\n\n        <h2>State sharing</h2>\n        <p>Single signal source consumed by both copies. The parent template reads the signal once and passes the extracted value to both the inline mount AND the drawer mount; on signal change the parent re-renders both:</p>\n        <code-ui language=\"javascript\">import { signal } from '@adia-ai/web-components/core';\n\nconst activeToken = signal('primary');\n\nstatic template = () =&gt; {\n  const current = activeToken.value;  // dep read here\n  return html`\n    &lt;aside class=\"wide-only\"&gt;\n      &lt;list-ui&gt; ... .active=${current === 'primary'} ... &lt;/list-ui&gt;\n    &lt;/aside&gt;\n    &lt;drawer-ui id=\"leading-drawer\"&gt;\n      &lt;list-ui&gt; ... .active=${current === 'primary'} ... &lt;/list-ui&gt;\n    &lt;/drawer-ui&gt;\n  `;\n}</code-ui>\n\n        <h2>Future v0.6.0: built-in <code>responsive-breakpoint</code></h2>\n        <p>FEEDBACK-06 §3 Option A proposed a built-in <code>&lt;editor-sidebar responsive-breakpoint=\"900\"&gt;</code> attribute that auto-stamps the drawer twin. Deferred to v0.6.0 — requires editor-shell + pane-ui semantics audit (focus handoff at the inline↔drawer transition, dual-mount event-listener deduplication, etc.). Pairs with a <code>&lt;show-when breakpoint=\"...\"&gt;</code> trait. Until then, the pattern above is the canonical recipe.</p>\n      </div>"
    },
    {
      "name": "narrow-toolbar",
      "tagName": "row-ui",
      "html": "<button-ui id=\"open-leading\" text=\"Tokens\" icon=\"list\" variant=\"ghost\" size=\"sm\"></button-ui>\n        <button-ui id=\"open-trailing\" text=\"Settings\" icon=\"sliders\" variant=\"ghost\" size=\"sm\"></button-ui>"
    },
    {
      "name": "trailing-inline",
      "tagName": "aside",
      "html": "<pane-ui>\n        <header>\n          <h3>Settings</h3>\n        </header>\n        <section>\n          <list-ui>\n            <list-item-ui>Lightness</list-item-ui>\n            <list-item-ui>Chroma</list-item-ui>\n            <list-item-ui>Hue</list-item-ui>\n          </list-ui>\n        </section>\n      </pane-ui>"
    },
    {
      "name": "leading-drawer",
      "tagName": "drawer-ui",
      "html": "<header>\n      <h3>Tokens</h3>\n    </header>\n    <section>\n      <list-ui>\n        <list-item-ui>Primary</list-item-ui>\n        <list-item-ui>Secondary</list-item-ui>\n        <list-item-ui>Neutral</list-item-ui>\n        <list-item-ui>Surface</list-item-ui>\n      </list-ui>\n    </section>"
    },
    {
      "name": "trailing-drawer",
      "tagName": "drawer-ui",
      "html": "<header>\n      <h3>Settings</h3>\n    </header>\n    <section>\n      <list-ui>\n        <list-item-ui>Lightness</list-item-ui>\n        <list-item-ui>Chroma</list-item-ui>\n        <list-item-ui>Hue</list-item-ui>\n      </list-ui>\n    </section>"
    }
  ],
  "nested": [],
  "attrs": {
    "with": "",
    "leading": "",
    "trailing": "",
    "sidebars": "",
    "that": "",
    "collapse": "",
    "to": "",
    "drawers": "",
    "below": "",
    "a": "",
    "px": "",
    "viewport": "",
    "breakpoint": "",
    "dual-mount": "",
    "same": "",
    "content": "",
    "renders": "",
    "inline": "",
    "on": "",
    "wide": "",
    "viewports": "",
    "inside": "",
    "drawer-ui": ""
  },
  "html": "<body>\n\n  <!-- ═══ INLINE shell (wide-only) ═══ -->\n  <div class=\"shell\">\n    <!-- Leading sidebar — inline mount (wide-only) -->\n    <aside class=\"wide-only\" data-chunk-slot=\"leading-inline\">\n      <pane-ui>\n        <header>\n          <h3>Tokens</h3>\n        </header>\n        <section>\n          <list-ui>\n            <list-item-ui>Primary</list-item-ui>\n            <list-item-ui>Secondary</list-item-ui>\n            <list-item-ui>Neutral</list-item-ui>\n            <list-item-ui>Surface</list-item-ui>\n          </list-ui>\n        </section>\n      </pane-ui>\n    </aside>\n\n    <!-- Main content (always inline) -->\n    <main data-chunk-slot=\"main\">\n      <!-- ═══ Narrow toolbar (drawer triggers) ═══ -->\n      <row-ui class=\"toolbar\" data-chunk-slot=\"narrow-toolbar\">\n        <button-ui id=\"open-leading\" text=\"Tokens\" icon=\"list\" variant=\"ghost\" size=\"sm\"></button-ui>\n        <button-ui id=\"open-trailing\" text=\"Settings\" icon=\"sliders\" variant=\"ghost\" size=\"sm\"></button-ui>\n      </row-ui>\n\n      <card-ui>\n        <header>\n          <h2>Responsive Shell Sidebar pattern</h2>\n        </header>\n        <section>\n          <text-ui>\n            Resize this viewport: at ≤900px the leading + trailing sidebars collapse\n            to drawers triggered by the toolbar buttons. Above 900px the sidebars are\n            inline. The same content is mounted in both surfaces; consumer signals\n            drive both copies via the parent template's re-read.\n          </text-ui>\n        </section>\n      </card-ui>\n\n      <div class=\"doc-body\">\n        <h2>The pattern</h2>\n        <p>Three pieces:</p>\n        <ol>\n          <li><strong>Inline mounts</strong> for the sidebars wrapped in <code>.wide-only</code> (display: contents at &gt;900px; display: none at ≤900px).</li>\n          <li><strong>Drawer twins</strong> with the same children, mounted outside the shell's named slots, controlled by toolbar buttons inside <code>.narrow-only</code>.</li>\n          <li><strong>MediaQueryList</strong> listener that auto-closes the drawers on viewport widening (the gap FEEDBACK-06 §3 explicitly flagged).</li>\n        </ol>\n\n        <h2>Dual-mount caveat</h2>\n        <p>Components with <strong>local DOM state</strong> (uncontrolled form inputs, scroll position, focused element) will lose state when the viewport crosses the breakpoint — the inline copy unmounts and the drawer copy mounts (or vice versa). <strong>Signal-bound components are safe</strong> because their state lives in the signal, not the DOM. Most AdiaUI primitives are signal-backed; the affected cases are:</p>\n        <ul>\n          <li>Native <code>&lt;input&gt;</code>/<code>&lt;textarea&gt;</code> with no signal binding</li>\n          <li>Components using <code>this.querySelector(...)</code> with imperative state</li>\n          <li>Scroll-position-sensitive containers (e.g. chat threads at a specific scroll)</li>\n        </ul>\n        <p>For these, either: (a) lift the state to a signal in the parent, or (b) document the unmount-on-resize edge.</p>\n\n        <h2>Auto-close on viewport widening</h2>\n        <code-ui language=\"javascript\">const mql = window.matchMedia('(max-width: 900px)');\nmql.addEventListener('change', (e) =&gt; {\n  if (!e.matches) {\n    // Viewport widened past breakpoint — close any open drawers\n    document.getElementById('leading-drawer').open = false;\n    document.getElementById('trailing-drawer').open = false;\n  }\n});</code-ui>\n\n        <h2>State sharing</h2>\n        <p>Single signal source consumed by both copies. The parent template reads the signal once and passes the extracted value to both the inline mount AND the drawer mount; on signal change the parent re-renders both:</p>\n        <code-ui language=\"javascript\">import { signal } from '@adia-ai/web-components/core';\n\nconst activeToken = signal('primary');\n\nstatic template = () =&gt; {\n  const current = activeToken.value;  // dep read here\n  return html`\n    &lt;aside class=\"wide-only\"&gt;\n      &lt;list-ui&gt; ... .active=${current === 'primary'} ... &lt;/list-ui&gt;\n    &lt;/aside&gt;\n    &lt;drawer-ui id=\"leading-drawer\"&gt;\n      &lt;list-ui&gt; ... .active=${current === 'primary'} ... &lt;/list-ui&gt;\n    &lt;/drawer-ui&gt;\n  `;\n}</code-ui>\n\n        <h2>Future v0.6.0: built-in <code>responsive-breakpoint</code></h2>\n        <p>FEEDBACK-06 §3 Option A proposed a built-in <code>&lt;editor-sidebar responsive-breakpoint=\"900\"&gt;</code> attribute that auto-stamps the drawer twin. Deferred to v0.6.0 — requires editor-shell + pane-ui semantics audit (focus handoff at the inline↔drawer transition, dual-mount event-listener deduplication, etc.). Pairs with a <code>&lt;show-when breakpoint=\"...\"&gt;</code> trait. Until then, the pattern above is the canonical recipe.</p>\n      </div>\n    </main>\n\n    <!-- Trailing sidebar — inline mount (wide-only) -->\n    <aside class=\"wide-only\" data-chunk-slot=\"trailing-inline\">\n      <pane-ui>\n        <header>\n          <h3>Settings</h3>\n        </header>\n        <section>\n          <list-ui>\n            <list-item-ui>Lightness</list-item-ui>\n            <list-item-ui>Chroma</list-item-ui>\n            <list-item-ui>Hue</list-item-ui>\n          </list-ui>\n        </section>\n      </pane-ui>\n    </aside>\n  </div>\n\n  <!-- ═══ DRAWER twins (narrow-only) ═══ -->\n  <drawer-ui id=\"leading-drawer\" side=\"left\" size=\"sm\" data-chunk-slot=\"leading-drawer\">\n    <header>\n      <h3>Tokens</h3>\n    </header>\n    <section>\n      <list-ui>\n        <list-item-ui>Primary</list-item-ui>\n        <list-item-ui>Secondary</list-item-ui>\n        <list-item-ui>Neutral</list-item-ui>\n        <list-item-ui>Surface</list-item-ui>\n      </list-ui>\n    </section>\n  </drawer-ui>\n\n  <drawer-ui id=\"trailing-drawer\" side=\"right\" size=\"sm\" data-chunk-slot=\"trailing-drawer\">\n    <header>\n      <h3>Settings</h3>\n    </header>\n    <section>\n      <list-ui>\n        <list-item-ui>Lightness</list-item-ui>\n        <list-item-ui>Chroma</list-item-ui>\n        <list-item-ui>Hue</list-item-ui>\n      </list-ui>\n    </section>\n  </drawer-ui>\n\n  <script type=\"module\">\n    // Wire toolbar buttons to drawer triggers\n    document.getElementById('open-leading')?.addEventListener('press', () => {\n      document.getElementById('leading-drawer').open = true;\n    });\n    document.getElementById('open-trailing')?.addEventListener('press', () => {\n      document.getElementById('trailing-drawer').open = true;\n    });\n\n    // Auto-close drawers when viewport crosses breakpoint (widening)\n    const mql = window.matchMedia('(max-width: 900px)');\n    mql.addEventListener('change', (e) => {\n      if (!e.matches) {\n        document.getElementById('leading-drawer').open = false;\n        document.getElementById('trailing-drawer').open = false;\n      }\n    });\n  </script>\n</body>",
  "source": "packages/web-components/patterns/responsive-shell-sidebar/responsive-shell-sidebar.examples.html",
  "metadata": {
    "domain": "layout",
    "description": "\"Editor-shell"
  },
  "captured_at": "2026-06-10T20:25:14.783Z",
  "template": [
    {
      "id": "root",
      "component": "Text",
      "textContent": "Tokens Primary Secondary Neutral Surface Responsive Shell Sidebar pattern Resize this viewport: at ≤900px the leading + trailing sidebars collapse to drawers triggered by the toolbar buttons. Above 900px the sidebars are inline. The same content is mounted in both surfaces; consumer signals drive both copies via the parent template's re-read. The pattern Three pieces: Inline mounts for the sidebars wrapped in .wide-only (display: contents at &gt;900px; display: none at ≤900px). Drawer twins with the same children, mounted outside the shell's named slots, controlled by toolbar buttons inside .narrow-only . MediaQueryList listener that auto-closes the drawers on viewport widening (the gap FEEDBACK-06 §3 explicitly flagged). Dual-mount caveat Components with local DOM state (uncontrolled form inputs, scroll position, focused element) will lose state when the viewport crosses the breakpoint — the inline copy unmounts and the drawer copy mounts (or vice versa). Signal-bound components are safe because their state lives in the signal, not the DOM. Most AdiaUI primitives are signal-backed; the affected cases are: Native &lt;input&gt; / &lt;textarea&gt; with no signal binding Components using this.querySelector(...) with imperative state Scroll-position-sensitive containers (e.g. chat threads at a specific scroll) For these, either: (a) lift the state to a signal in the parent, or (b) document the unmount-on-resize edge. Auto-close on viewport widening const mql = window.matchMedia('(max-width: 900px)'); mql.addEventListener('change', (e) =&gt; { if (!e.matches) { // Viewport widened past breakpoint — close any open drawers document.getElementById('leading-drawer').open = false; document.getElementById('trailing-drawer').open = false; } }); State sharing Single signal source consumed by both copies. The parent template reads the signal once and passes the extracted value to both the inline mount AND the drawer mount; on signal change the parent re-renders both: import { signal } from '@adia-ai/web-components/core'; const activeToken = signal('primary'); static template = () =&gt; { const current = activeToken.value; // dep read here return html` &lt;aside class=\"wide-only\"&gt; &lt;list-ui&gt; ... .active=${current === 'primary'} ... &lt;/list-ui&gt; &lt;/aside&gt; &lt;drawer-ui id=\"leading-drawer\"&gt; &lt;list-ui&gt; ... .active=${current === 'primary'} ... &lt;/list-ui&gt; &lt;/drawer-ui&gt; `; } Future v0.6.0: built-in responsive-breakpoint FEEDBACK-06 §3 Option A proposed a built-in &lt;editor-sidebar responsive-breakpoint=\"900\"&gt; attribute that auto-stamps the drawer twin. Deferred to v0.6.0 — requires editor-shell + pane-ui semantics audit (focus handoff at the inline↔drawer transition, dual-mount event-listener deduplication, etc.). Pairs with a &lt;show-when breakpoint=\"...\"&gt; trait. Until then, the pattern above is the canonical recipe. Settings Lightness Chroma Hue Tokens Primary Secondary Neutral Surface Settings Lightness Chroma Hue // Wire toolbar buttons to drawer triggers document.getElementById('open-leading')?.addEventListener('press', () => { document.getElementById('leading-drawer').open = true; }); document.getElementById('open-trailing')?.addEventListener('press', () => { document.getElementById('trailing-drawer').open = true; }); // Auto-close drawers when viewport crosses breakpoint (widening) const mql = window.matchMedia('(max-width: 900px)'); mql.addEventListener('change', (e) => { if (!e.matches) { document.getElementById('leading-drawer').open = false; document.getElementById('trailing-drawer').open = false; } });",
      "variant": "body"
    }
  ]
}
