@adia-ai/web-modules 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/chat/chat-composer/chat-composer.a2ui.json +94 -0
  3. package/chat/chat-composer/chat-composer.examples.html +28 -0
  4. package/chat/chat-composer/chat-composer.html +43 -0
  5. package/chat/chat-composer/chat-composer.js +107 -0
  6. package/chat/chat-composer/chat-composer.test.js +112 -0
  7. package/chat/chat-composer/chat-composer.yaml +91 -0
  8. package/chat/chat-empty/chat-empty.a2ui.json +68 -0
  9. package/chat/chat-empty/chat-empty.examples.html +34 -0
  10. package/chat/chat-empty/chat-empty.html +42 -0
  11. package/chat/chat-empty/chat-empty.yaml +58 -0
  12. package/chat/chat-header/chat-header.a2ui.json +77 -0
  13. package/chat/chat-header/chat-header.examples.html +30 -0
  14. package/chat/chat-header/chat-header.html +42 -0
  15. package/chat/chat-header/chat-header.yaml +68 -0
  16. package/chat/chat-shell/chat-shell.css +1 -0
  17. package/chat/chat-shell/chat-shell.examples.html +126 -0
  18. package/chat/chat-shell/chat-shell.html +42 -0
  19. package/chat/chat-shell/chat-shell.js +35 -7
  20. package/chat/chat-shell/css/chat-shell.bespoke.css +196 -0
  21. package/chat/chat-sidebar/chat-sidebar.a2ui.json +136 -0
  22. package/chat/chat-sidebar/chat-sidebar.examples.html +36 -0
  23. package/chat/chat-sidebar/chat-sidebar.html +43 -0
  24. package/chat/chat-sidebar/chat-sidebar.js +227 -0
  25. package/chat/chat-sidebar/chat-sidebar.test.js +110 -0
  26. package/chat/chat-sidebar/chat-sidebar.yaml +140 -0
  27. package/chat/chat-status/chat-status.a2ui.json +63 -0
  28. package/chat/chat-status/chat-status.examples.html +29 -0
  29. package/chat/chat-status/chat-status.html +42 -0
  30. package/chat/chat-status/chat-status.yaml +52 -0
  31. package/chat/chat-thread/chat-thread.a2ui.json +91 -0
  32. package/chat/chat-thread/chat-thread.examples.html +36 -0
  33. package/chat/chat-thread/chat-thread.html +43 -0
  34. package/chat/chat-thread/chat-thread.js +106 -0
  35. package/chat/chat-thread/chat-thread.test.js +82 -0
  36. package/chat/chat-thread/chat-thread.yaml +89 -0
  37. package/chat/index.js +3 -0
  38. package/editor/editor-shell/editor-shell.examples.html +71 -0
  39. package/editor/editor-shell/editor-shell.html +42 -0
  40. package/package.json +1 -1
  41. package/shell/admin-command/admin-command.a2ui.json +102 -0
  42. package/shell/admin-command/admin-command.examples.html +83 -0
  43. package/shell/admin-command/admin-command.html +42 -0
  44. package/shell/admin-command/admin-command.js +161 -0
  45. package/shell/admin-command/admin-command.test.js +115 -0
  46. package/shell/admin-command/admin-command.yaml +102 -0
  47. package/shell/admin-content/admin-content.a2ui.json +73 -0
  48. package/shell/admin-content/admin-content.examples.html +33 -0
  49. package/shell/admin-content/admin-content.html +42 -0
  50. package/shell/admin-content/admin-content.yaml +63 -0
  51. package/shell/admin-page/admin-page.a2ui.json +74 -0
  52. package/shell/admin-page/admin-page.examples.html +37 -0
  53. package/shell/admin-page/admin-page.html +42 -0
  54. package/shell/admin-page/admin-page.yaml +61 -0
  55. package/shell/admin-page-body/admin-page-body.a2ui.json +62 -0
  56. package/shell/admin-page-body/admin-page-body.examples.html +34 -0
  57. package/shell/admin-page-body/admin-page-body.html +42 -0
  58. package/shell/admin-page-body/admin-page-body.yaml +49 -0
  59. package/shell/admin-page-header/admin-page-header.a2ui.json +62 -0
  60. package/shell/admin-page-header/admin-page-header.examples.html +34 -0
  61. package/shell/admin-page-header/admin-page-header.html +42 -0
  62. package/shell/admin-page-header/admin-page-header.yaml +47 -0
  63. package/shell/admin-scroll/admin-scroll.a2ui.json +62 -0
  64. package/shell/admin-scroll/admin-scroll.examples.html +31 -0
  65. package/shell/admin-scroll/admin-scroll.html +42 -0
  66. package/shell/admin-scroll/admin-scroll.yaml +51 -0
  67. package/shell/admin-shell/admin-shell.a2ui.json +0 -10
  68. package/shell/admin-shell/admin-shell.css +1 -0
  69. package/shell/admin-shell/admin-shell.examples.html +61 -5
  70. package/shell/admin-shell/admin-shell.js +165 -121
  71. package/shell/admin-shell/admin-shell.yaml +6 -6
  72. package/shell/admin-shell/css/admin-shell.bespoke.css +198 -0
  73. package/shell/admin-shell/css/admin-shell.tokens.css +10 -0
  74. package/shell/admin-sidebar/admin-sidebar.a2ui.json +138 -0
  75. package/shell/admin-sidebar/admin-sidebar.examples.html +76 -0
  76. package/shell/admin-sidebar/admin-sidebar.html +47 -0
  77. package/shell/admin-sidebar/admin-sidebar.js +227 -0
  78. package/shell/admin-sidebar/admin-sidebar.test.js +123 -0
  79. package/shell/admin-sidebar/admin-sidebar.yaml +140 -0
  80. package/shell/admin-statusbar/admin-statusbar.a2ui.json +81 -0
  81. package/shell/admin-statusbar/admin-statusbar.examples.html +29 -0
  82. package/shell/admin-statusbar/admin-statusbar.html +42 -0
  83. package/shell/admin-statusbar/admin-statusbar.yaml +68 -0
  84. package/shell/admin-topbar/admin-topbar.a2ui.json +83 -0
  85. package/shell/admin-topbar/admin-topbar.examples.html +31 -0
  86. package/shell/admin-topbar/admin-topbar.html +42 -0
  87. package/shell/admin-topbar/admin-topbar.yaml +75 -0
  88. package/shell/index.js +2 -0
@@ -0,0 +1,51 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: AdminScroll
4
+ tag: admin-scroll
5
+ component: AdminScroll
6
+ category: layout
7
+ version: 1
8
+ description: |
9
+ Module-tier shell scroll surface. CSS-only — no behavior, no JS.
10
+ Sits inside <admin-content> as the central scrollable region.
11
+ Single-axis scroll (vertical), scroll-contained so wheel/touch
12
+ events don't propagate to the document.
13
+
14
+ Replaces the legacy <section> child of <main> at shell-tier per
15
+ ADR-0023.
16
+
17
+ props: {}
18
+
19
+ events: {}
20
+
21
+ slots:
22
+ default:
23
+ description: >-
24
+ Scrollable content — typically <admin-page> wrapping the
25
+ page-tier sticky bands + body.
26
+
27
+ states:
28
+ - name: idle
29
+ description: Default, the only state.
30
+
31
+ traits: []
32
+
33
+ a2ui:
34
+ rules:
35
+ - >-
36
+ admin-scroll is the bespoke replacement for the legacy <section>
37
+ child of <main> inside admin-shell. Single child convention —
38
+ typically wraps an <admin-page> for sticky-band layout.
39
+
40
+ keywords:
41
+ - admin-scroll
42
+ - scroll-region
43
+ - scroll-container
44
+
45
+ synonyms:
46
+ scroll-region: [scroll-area, viewport-content]
47
+
48
+ related:
49
+ - AdminShell
50
+ - AdminContent
51
+ - AdminPage
@@ -97,16 +97,6 @@
97
97
  {
98
98
  "description": "Default, interactive shell.",
99
99
  "name": "idle"
100
- },
101
- {
102
- "description": "Leading sidebar is collapsed; content expands.",
103
- "attribute": "data-sidebar-leading-collapsed",
104
- "name": "collapsed-leading"
105
- },
106
- {
107
- "description": "Trailing sidebar (inspector) is collapsed.",
108
- "attribute": "data-sidebar-trailing-collapsed",
109
- "name": "collapsed-trailing"
110
100
  }
111
101
  ],
112
102
  "synonyms": {
@@ -12,3 +12,4 @@
12
12
  @import "./css/admin-shell.collapsed.css";
13
13
  @import "./css/admin-shell.templates.css";
14
14
  @import "./css/admin-shell.helpers.css";
15
+ @import "./css/admin-shell.bespoke.css";
@@ -9,8 +9,8 @@
9
9
  </header>
10
10
 
11
11
  <section data-section>
12
- <h2 variant="section">Basic shape</h2>
13
- <p data-note>Author supplies the DOM (sidebars + main column + optional command dialog); the shell binds the interactions.</p>
12
+ <h2 variant="section">Basic shape (legacy)</h2>
13
+ <p data-note>Author supplies the DOM (sidebars + main column + optional command dialog); the shell binds the interactions. This is the original raw-HTML authoring shape — still fully supported.</p>
14
14
  <code-ui language="html">&lt;admin-shell mode="rounded"&gt;
15
15
  &lt;aside data-sidebar="leading"&gt;
16
16
  &lt;header&gt;…&lt;/header&gt;
@@ -40,6 +40,62 @@
40
40
  &lt;/admin-shell&gt;</code-ui>
41
41
  </section>
42
42
 
43
+ <section data-section>
44
+ <h2 variant="section">Basic shape (bespoke — recommended)</h2>
45
+ <p data-note>The bespoke shape uses module-namespaced custom elements (<code>&lt;admin-sidebar&gt;</code>, <code>&lt;admin-content&gt;</code>, etc.) per <a href="../../../../.brain/adrs/0023-bespoke-shell-tier-children.md">ADR-0023</a>. Each child owns its own behavior + state attributes; queryable from outside via <code>:has(admin-sidebar[collapsed])</code>.</p>
46
+ <code-ui language="html">&lt;admin-shell mode="rounded"&gt;
47
+ &lt;admin-sidebar slot="leading" resizable collapsible&gt;
48
+ &lt;admin-topbar slot="header"&gt;
49
+ &lt;span slot="heading"&gt;Workspace&lt;/span&gt;
50
+ &lt;/admin-topbar&gt;
51
+ &lt;nav-ui&gt;…&lt;/nav-ui&gt;
52
+ &lt;admin-statusbar slot="footer"&gt;
53
+ &lt;span&gt;User&lt;/span&gt;
54
+ &lt;/admin-statusbar&gt;
55
+ &lt;div data-resize&gt;&lt;/div&gt;
56
+ &lt;/admin-sidebar&gt;
57
+
58
+ &lt;admin-content&gt;
59
+ &lt;admin-topbar slot="header"&gt;
60
+ &lt;button-ui data-sidebar-toggle="leading" icon="sidebar" variant="ghost" size="sm"&gt;&lt;/button-ui&gt;
61
+ &lt;breadcrumb-ui slot="heading"&gt;…&lt;/breadcrumb-ui&gt;
62
+ &lt;/admin-topbar&gt;
63
+ &lt;admin-scroll&gt;
64
+ &lt;admin-page&gt;
65
+ &lt;admin-page-header&gt;
66
+ &lt;header-ui&gt;&lt;span slot="heading"&gt;Page Title&lt;/span&gt;&lt;/header-ui&gt;
67
+ &lt;/admin-page-header&gt;
68
+ &lt;admin-page-body&gt;
69
+ &lt;section-ui&gt;…content…&lt;/section-ui&gt;
70
+ &lt;/admin-page-body&gt;
71
+ &lt;/admin-page&gt;
72
+ &lt;/admin-scroll&gt;
73
+ &lt;admin-statusbar slot="footer"&gt;
74
+ &lt;span&gt;Status&lt;/span&gt;
75
+ &lt;/admin-statusbar&gt;
76
+ &lt;/admin-content&gt;
77
+
78
+ &lt;admin-command&gt;
79
+ &lt;command-ui placeholder="Search..."&gt;&lt;/command-ui&gt;
80
+ &lt;/admin-command&gt;
81
+ &lt;/admin-shell&gt;</code-ui>
82
+ </section>
83
+
84
+ <section data-section>
85
+ <h2 variant="section">State as attribute</h2>
86
+ <p>Every queryable state is reflected on the relevant child element. Style cross-cuts via <code>:has()</code>:</p>
87
+ <code-ui language="css">/* Theme tweak when leading sidebar is collapsed */
88
+ admin-shell:has(admin-sidebar[slot="leading"][collapsed]) admin-content {
89
+ /* … */
90
+ }
91
+
92
+ /* Show focus indicator while command palette is open */
93
+ admin-shell:has(admin-command[open]) {
94
+ /* … */
95
+ }</code-ui>
96
+ <p data-note>JS reads the same attribute: <code>shell.querySelector('admin-sidebar[slot="leading"]').hasAttribute('collapsed')</code>. No threshold math, no <code>getBoundingClientRect</code>.</p>
97
+ </section>
98
+
43
99
  <section data-section>
44
100
  <h2 variant="section">Properties</h2>
45
101
  <table>
@@ -55,9 +111,9 @@
55
111
  <table>
56
112
  <thead><tr><th>Affordance</th><th>Author markup</th><th>What the shell does</th></tr></thead>
57
113
  <tbody>
58
- <tr><td>Sidebar toggle</td><td><code>[data-sidebar-toggle="leading"]</code> / <code>[data-sidebar-toggle="trailing"]</code></td><td>Click flips <code>[data-collapsed]</code> on the corresponding aside. Persists in <code>localStorage</code>.</td></tr>
59
- <tr><td>Sidebar resize</td><td><code>&lt;div data-resize&gt;&lt;/div&gt;</code> inside the sidebar</td><td>Pointer-drag resizes the aside; bounded by <code>leading-min/max-width</code> attrs. Snaps below threshold to collapsed.</td></tr>
60
- <tr><td>Command palette</td><td><code>&lt;dialog data-command&gt;</code> + shell <code>[cmd-k]</code></td><td><code>Cmd+K</code> / <code>Ctrl+K</code> calls <code>showModal()</code>. Forwards <code>command-select</code> events from the inner <code>&lt;command-ui&gt;</code>.</td></tr>
114
+ <tr><td>Sidebar toggle</td><td><code>[data-sidebar-toggle="leading"]</code> / <code>[data-sidebar-toggle="trailing"]</code></td><td>Click flips <code>[collapsed]</code> on the corresponding sidebar. Persists in <code>localStorage</code>.</td></tr>
115
+ <tr><td>Sidebar resize</td><td><code>&lt;div data-resize&gt;&lt;/div&gt;</code> inside the sidebar</td><td>Pointer-drag resizes the aside; bounded by <code>--page-sidebar-min-width</code> / <code>--page-sidebar-max-width</code> CSS custom properties. Snaps below threshold to collapsed.</td></tr>
116
+ <tr><td>Command palette</td><td><code>&lt;dialog data-command&gt;</code> as a child of the shell</td><td><code>Cmd+K</code> / <code>Ctrl+K</code> calls <code>showModal()</code> on the inner dialog. Forwards <code>command-select</code> events from the inner <code>&lt;command-ui&gt;</code>.</td></tr>
61
117
  <tr><td>Responsive collapse</td><td>(automatic)</td><td>ResizeObserver collapses sidebars when viewport drops below the breakpoint; restores on widen.</td></tr>
62
118
  </tbody>
63
119
  </table>
@@ -1,31 +1,35 @@
1
1
  /**
2
2
  * <admin-shell mode="rounded borderless">
3
- * <aside data-sidebar="leading">...</aside> (or <aside-ui slot="leading">)
4
- * <main>...</main>
5
- * <aside data-sidebar="trailing">...</aside> (or <aside-ui slot="trailing">)
6
- * <dialog data-command>...</dialog>
3
+ * <admin-sidebar slot="leading" resizable collapsible>…</admin-sidebar>
4
+ * <admin-content>…</admin-content>
5
+ * <admin-sidebar slot="trailing" resizable collapsible>…</admin-sidebar>
6
+ * <admin-command><command-ui></command-ui></admin-command>
7
7
  * </admin-shell>
8
8
  *
9
- * Behavior-only app shell. Stamps no HTML — the page author writes the
10
- * structure using semantic elements + data attributes. Both the legacy
11
- * `aside[data-sidebar]` and the slot-vocabulary `aside-ui[slot]`
12
- * authoring shapes are recognised; #sidebarName() reads from either.
13
- * The component auto-wires four JS behaviors that CSS can't handle:
9
+ * Module-tier app shell. Coordinates bespoke shell-tier children;
10
+ * does NOT centralize their behavior. Each child owns its own
11
+ * concern (resize/collapse on <admin-sidebar>, palette wiring on
12
+ * <admin-command>).
14
13
  *
15
- * 1. Sidebar resize (drag handle with snap thresholds)
16
- * 2. Sidebar toggle (collapse/restore width)
17
- * 3. Command palette (Cmd+K keyboard shortcut)
18
- * 4. ResizeObserver (select placement in narrow sidebars)
14
+ * Backwards compatibility: legacy authoring shapes still work —
15
+ * <aside data-sidebar="leading"> and <dialog data-command> are
16
+ * recognized + wired the same as their bespoke equivalents. Mixed
17
+ * markup (some bespoke, some legacy) renders identically. See
18
+ * ADR-0023 for the migration window.
19
19
  *
20
- * CSS handles everything else: layout, collapse, container queries,
21
- * sticky headers, scroll containment, mode composition.
20
+ * Host responsibilities (the only behavior that stays here):
21
+ * 1. [mode] reflection "rounded", "borderless", or both
22
+ * 2. [data-sidebar-toggle="leading|trailing"] click forwarding to
23
+ * the matching sidebar's .toggle() (works for bespoke + legacy)
24
+ * 3. [data-command-trigger] click forwarding to the inner palette's
25
+ * .show() (works for bespoke + legacy)
26
+ * 4. Re-emit child events as host events for backwards compat
27
+ * (sidebar-toggle, sidebar-resize, command-select all bubble
28
+ * already, but consumers who listened on the host get them)
22
29
  */
23
30
 
24
31
  import { UIElement } from '../../../web-components/core/element.js';
25
32
 
26
- const SNAP_THRESHOLD = 96;
27
- const SNAP_MIN_USABLE = 160;
28
-
29
33
  class AdminShell extends UIElement {
30
34
  static properties = {
31
35
  mode: { type: String, default: '', reflect: true },
@@ -33,107 +37,161 @@ class AdminShell extends UIElement {
33
37
 
34
38
  static template = () => null;
35
39
 
36
- #sidebarWidths = new Map();
37
- #resizeCleanups = [];
38
- #sidebarRO = null;
39
40
  #cmdKeyHandler = null;
41
+ #legacyResizeCleanups = [];
42
+ #legacySidebarRO = null;
43
+ #legacySidebarWidths = new Map();
40
44
 
41
45
  connected() {
42
- this.#setupToggles();
43
- this.#setupResizeHandles();
44
- this.#setupCommandPalette();
45
- this.#setupResizeObserver();
46
+ this.#wireToggleButtons();
47
+ this.#wireCommandTriggers();
48
+ this.#setupLegacyShapes();
46
49
  }
47
50
 
48
51
  disconnected() {
49
- // Clean up resize handles
50
- for (const cleanup of this.#resizeCleanups) cleanup();
51
- this.#resizeCleanups = [];
52
-
53
- // Clean up ResizeObserver
54
- this.#sidebarRO?.disconnect();
55
- this.#sidebarRO = null;
56
-
57
- // Clean up Cmd+K
52
+ for (const cleanup of this.#legacyResizeCleanups) cleanup();
53
+ this.#legacyResizeCleanups = [];
54
+ this.#legacySidebarRO?.disconnect();
55
+ this.#legacySidebarRO = null;
58
56
  if (this.#cmdKeyHandler) {
59
57
  document.removeEventListener('keydown', this.#cmdKeyHandler);
60
58
  this.#cmdKeyHandler = null;
61
59
  }
62
60
  }
63
61
 
64
- // ── Sidebar persistence ────────────────────────────────────
62
+ // Selector that matches BOTH bespoke <admin-sidebar slot> AND legacy
63
+ // shapes. Used by toggle-button wiring + ResizeObserver setup.
64
+ static #SIDEBAR_SEL =
65
+ ':is(admin-sidebar[slot="leading"], admin-sidebar[slot="trailing"], ' +
66
+ '[data-sidebar], aside-ui[slot="leading"], aside-ui[slot="trailing"])';
65
67
 
66
- // Selector that matches both legacy raw-HTML and slot-vocabulary forms.
67
- static #SIDEBAR_SEL = ':is([data-sidebar], aside-ui[slot="leading"], aside-ui[slot="trailing"])';
68
-
69
- // Helper: read sidebar name from either authoring shape.
70
68
  #sidebarName(el) {
71
- return el.getAttribute('data-sidebar') ?? el.getAttribute('slot');
69
+ return (
70
+ el.getAttribute('name') ??
71
+ el.getAttribute('data-sidebar') ??
72
+ el.getAttribute('slot')
73
+ );
72
74
  }
73
75
 
74
- #persistSidebar(sidebarName, width) {
75
- try { localStorage.setItem(`adia-sidebar-${sidebarName}`, width); } catch {}
76
+ #findSidebar(name) {
77
+ return this.querySelector(
78
+ ':is(' +
79
+ `admin-sidebar[slot="${name}"], admin-sidebar[name="${name}"], ` +
80
+ `[data-sidebar="${name}"], aside-ui[slot="${name}"]` +
81
+ ')'
82
+ );
76
83
  }
77
84
 
78
- #restoreSidebars() {
79
- for (const sidebar of this.querySelectorAll(AdminShell.#SIDEBAR_SEL)) {
85
+ // ── 1. Toggle buttons — works for bespoke + legacy ──
86
+
87
+ #wireToggleButtons() {
88
+ for (const btn of this.querySelectorAll('[data-sidebar-toggle]')) {
89
+ const name = btn.getAttribute('data-sidebar-toggle');
90
+ btn.addEventListener('click', () => {
91
+ const sidebar = this.#findSidebar(name);
92
+ if (!sidebar) return;
93
+
94
+ // Bespoke <admin-sidebar> has a public toggle() method
95
+ if (typeof sidebar.toggle === 'function') {
96
+ sidebar.toggle();
97
+ return;
98
+ }
99
+
100
+ // Legacy: replicate the old in-place toggle on raw HTML
101
+ this.#legacyToggle(sidebar, name);
102
+ });
103
+ }
104
+ }
105
+
106
+ // ── 2. Command triggers — works for bespoke + legacy ──
107
+
108
+ #wireCommandTriggers() {
109
+ const command = this.querySelector('admin-command');
110
+ const legacyDialog = command ? null : this.querySelector('dialog[data-command]');
111
+
112
+ if (!command && !legacyDialog) return;
113
+
114
+ // Bespoke path: trigger calls .show() on <admin-command>
115
+ if (command) {
116
+ for (const trigger of this.querySelectorAll('[data-command-trigger]')) {
117
+ trigger.addEventListener('click', (e) => {
118
+ e.stopPropagation();
119
+ command.show();
120
+ });
121
+ }
122
+ // <admin-command> owns its own keyboard shortcut; nothing more
123
+ // for the host to wire. We DO re-listen for command-select to
124
+ // wire navigation if a <nav-ui> is present.
125
+ this.#wireCommandSelectToNav(command);
126
+ return;
127
+ }
128
+
129
+ // Legacy path: replicate the old wiring around <dialog data-command>
130
+ this.#wireLegacyCommand(legacyDialog);
131
+ }
132
+
133
+ #wireCommandSelectToNav(command) {
134
+ const nav = this.querySelector('nav-ui');
135
+ if (!nav) return;
136
+ command.addEventListener('command-select', (e) => {
137
+ const item = nav.querySelector(`nav-item-ui[value="${e.detail.value}"]`);
138
+ if (item) nav.select(item);
139
+ });
140
+ }
141
+
142
+ // ── 3. Legacy shape wiring (raw <aside data-sidebar>, <dialog data-command>) ──
143
+
144
+ #setupLegacyShapes() {
145
+ const legacySidebars = this.querySelectorAll(
146
+ ':is([data-sidebar], aside-ui[slot="leading"], aside-ui[slot="trailing"])'
147
+ );
148
+ if (legacySidebars.length === 0) return;
149
+
150
+ this.#restoreLegacyWidths(legacySidebars);
151
+ this.#setupLegacyResizeHandles(legacySidebars);
152
+ this.#setupLegacyResizeObserver(legacySidebars);
153
+ }
154
+
155
+ #restoreLegacyWidths(sidebars) {
156
+ for (const sidebar of sidebars) {
80
157
  const name = this.#sidebarName(sidebar);
81
158
  try {
82
159
  const saved = localStorage.getItem(`adia-sidebar-${name}`);
83
160
  if (saved) {
84
161
  sidebar.style.width = saved;
85
- // Only store as the "previous expanded width" if it's actually expanded.
86
- // If collapsed, keep the default expanded width so toggle can restore it.
87
162
  const w = parseFloat(saved);
88
- if (isNaN(w) || w > SNAP_THRESHOLD) {
89
- this.#sidebarWidths.set(name, saved);
163
+ if (!isNaN(w) && w > 96) {
164
+ this.#legacySidebarWidths.set(name, saved);
90
165
  }
91
166
  }
92
167
  } catch {}
93
168
  }
94
169
  }
95
170
 
96
- // ── 1. Sidebar toggle ──────────────────────────────────────
97
-
98
- #setupToggles() {
99
- this.#restoreSidebars();
100
-
101
- for (const btn of this.querySelectorAll('[data-sidebar-toggle]')) {
102
- const sidebarName = btn.getAttribute('data-sidebar-toggle');
103
- btn.addEventListener('click', () => {
104
- const sidebar = this.querySelector(`:is([data-sidebar="${sidebarName}"], aside-ui[slot="${sidebarName}"])`);
105
- if (!sidebar) return;
106
-
107
- const isCollapsed = sidebar.getBoundingClientRect().width <= SNAP_THRESHOLD;
108
-
109
- if (isCollapsed) {
110
- // Expand: restore previous width
111
- const prev = this.#sidebarWidths.get(sidebarName);
112
- sidebar.style.width = prev || '';
113
- this.#persistSidebar(sidebarName, prev || '');
114
- } else {
115
- // Collapse: save current width, set to min
116
- this.#sidebarWidths.set(sidebarName, sidebar.style.width || getComputedStyle(sidebar).width);
117
- const minW = getComputedStyle(sidebar).minWidth;
118
- sidebar.style.width = minW;
119
- this.#persistSidebar(sidebarName, minW);
120
- }
121
-
122
- this.dispatchEvent(new CustomEvent('sidebar-toggle', {
123
- bubbles: true,
124
- detail: { sidebar: sidebarName, expanded: !isCollapsed },
125
- }));
126
- });
171
+ #legacyToggle(sidebar, name) {
172
+ const isCollapsed = sidebar.getBoundingClientRect().width <= 96;
173
+ if (isCollapsed) {
174
+ const prev = this.#legacySidebarWidths.get(name);
175
+ sidebar.style.width = prev || '';
176
+ try { localStorage.setItem(`adia-sidebar-${name}`, prev || ''); } catch {}
177
+ } else {
178
+ this.#legacySidebarWidths.set(name, sidebar.style.width || getComputedStyle(sidebar).width);
179
+ const minW = getComputedStyle(sidebar).minWidth;
180
+ sidebar.style.width = minW;
181
+ try { localStorage.setItem(`adia-sidebar-${name}`, minW); } catch {}
127
182
  }
183
+ this.dispatchEvent(new CustomEvent('sidebar-toggle', {
184
+ bubbles: true,
185
+ detail: { sidebar: name, name, expanded: !isCollapsed },
186
+ }));
128
187
  }
129
188
 
130
- // ── 2. Sidebar resize handles ──────────────────────────────
131
-
132
- #setupResizeHandles() {
133
- for (const handle of this.querySelectorAll(`${AdminShell.#SIDEBAR_SEL} > [data-resize]`)) {
134
- const sidebar = handle.parentElement;
135
- const sidebarName = this.#sidebarName(sidebar);
136
- const isLeading = sidebarName === 'leading';
189
+ #setupLegacyResizeHandles(sidebars) {
190
+ for (const sidebar of sidebars) {
191
+ const handle = sidebar.querySelector(':scope > [data-resize]');
192
+ if (!handle) continue;
193
+ const name = this.#sidebarName(sidebar);
194
+ const isLeading = name === 'leading';
137
195
 
138
196
  const onPointerDown = (e) => {
139
197
  e.preventDefault();
@@ -156,18 +214,17 @@ class AdminShell extends UIElement {
156
214
  handle.removeEventListener('pointermove', onMove);
157
215
  handle.removeEventListener('pointerup', onUp);
158
216
 
159
- // Snap logic
160
217
  const w = sidebar.getBoundingClientRect().width;
161
- if (w <= SNAP_THRESHOLD) {
218
+ if (w <= 96) {
162
219
  sidebar.style.width = getComputedStyle(sidebar).minWidth;
163
- } else if (w < SNAP_MIN_USABLE) {
164
- sidebar.style.width = `${SNAP_MIN_USABLE}px`;
220
+ } else if (w < 160) {
221
+ sidebar.style.width = '160px';
165
222
  }
223
+ try { localStorage.setItem(`adia-sidebar-${name}`, sidebar.style.width); } catch {}
166
224
 
167
- this.#persistSidebar(sidebarName, sidebar.style.width);
168
225
  this.dispatchEvent(new CustomEvent('sidebar-resize', {
169
226
  bubbles: true,
170
- detail: { sidebar: sidebarName, width: sidebar.getBoundingClientRect().width },
227
+ detail: { sidebar: name, name, width: sidebar.getBoundingClientRect().width },
171
228
  }));
172
229
  };
173
230
 
@@ -176,16 +233,24 @@ class AdminShell extends UIElement {
176
233
  };
177
234
 
178
235
  handle.addEventListener('pointerdown', onPointerDown);
179
- this.#resizeCleanups.push(() => handle.removeEventListener('pointerdown', onPointerDown));
236
+ this.#legacyResizeCleanups.push(() => handle.removeEventListener('pointerdown', onPointerDown));
180
237
  }
181
238
  }
182
239
 
183
- // ── 3. Command palette ─────────────────────────────────────
184
-
185
- #setupCommandPalette() {
186
- const dialog = this.querySelector('dialog[data-command]');
187
- if (!dialog) return;
240
+ #setupLegacyResizeObserver(sidebars) {
241
+ this.#legacySidebarRO = new ResizeObserver((entries) => {
242
+ for (const entry of entries) {
243
+ const sidebar = entry.target;
244
+ const narrow = entry.contentBoxSize[0].inlineSize <= 96;
245
+ for (const sel of sidebar.querySelectorAll('select-ui')) {
246
+ sel.setAttribute('placement', narrow ? 'right' : 'bottom-start');
247
+ }
248
+ }
249
+ });
250
+ for (const sb of sidebars) this.#legacySidebarRO.observe(sb);
251
+ }
188
252
 
253
+ #wireLegacyCommand(dialog) {
189
254
  const cmdEl = dialog.querySelector('command-ui');
190
255
  const nav = this.querySelector('nav-ui');
191
256
 
@@ -193,13 +258,11 @@ class AdminShell extends UIElement {
193
258
  dialog.showModal();
194
259
  if (cmdEl) { cmdEl.open = true; cmdEl.value = ''; cmdEl.focus(); }
195
260
  };
196
-
197
261
  const closeCmd = () => {
198
262
  dialog.close();
199
263
  if (cmdEl) cmdEl.open = false;
200
264
  };
201
265
 
202
- // Trigger elements
203
266
  for (const trigger of this.querySelectorAll('[data-command-trigger]')) {
204
267
  trigger.addEventListener('click', (e) => {
205
268
  e.stopPropagation();
@@ -207,12 +270,10 @@ class AdminShell extends UIElement {
207
270
  });
208
271
  }
209
272
 
210
- // Backdrop click closes
211
273
  dialog.addEventListener('click', (e) => {
212
274
  if (e.target === dialog) closeCmd();
213
275
  });
214
276
 
215
- // Command-n events
216
277
  if (cmdEl) {
217
278
  cmdEl.addEventListener('dismiss', closeCmd);
218
279
  cmdEl.addEventListener('select', (e) => {
@@ -228,7 +289,8 @@ class AdminShell extends UIElement {
228
289
  });
229
290
  }
230
291
 
231
- // Cmd+K / Ctrl+K
292
+ // Cmd+K listener — only for legacy shape (bespoke <admin-command>
293
+ // owns its own listener)
232
294
  this.#cmdKeyHandler = (e) => {
233
295
  if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
234
296
  e.preventDefault();
@@ -237,24 +299,6 @@ class AdminShell extends UIElement {
237
299
  };
238
300
  document.addEventListener('keydown', this.#cmdKeyHandler);
239
301
  }
240
-
241
- // ── 4. ResizeObserver for select placement ─────────────────
242
-
243
- #setupResizeObserver() {
244
- this.#sidebarRO = new ResizeObserver((entries) => {
245
- for (const entry of entries) {
246
- const sidebar = entry.target;
247
- const narrow = entry.contentBoxSize[0].inlineSize <= SNAP_THRESHOLD;
248
- for (const sel of sidebar.querySelectorAll('select-ui')) {
249
- sel.setAttribute('placement', narrow ? 'right' : 'bottom-start');
250
- }
251
- }
252
- });
253
-
254
- for (const sb of this.querySelectorAll(AdminShell.#SIDEBAR_SEL)) {
255
- this.#sidebarRO.observe(sb);
256
- }
257
- }
258
302
  }
259
303
 
260
304
  customElements.define('admin-shell', AdminShell);
@@ -66,12 +66,12 @@ slots:
66
66
  states:
67
67
  - name: idle
68
68
  description: Default, interactive shell.
69
- - name: collapsed-leading
70
- attribute: data-sidebar-leading-collapsed
71
- description: Leading sidebar is collapsed; content expands.
72
- - name: collapsed-trailing
73
- attribute: data-sidebar-trailing-collapsed
74
- description: Trailing sidebar (inspector) is collapsed.
69
+
70
+ # Note: collapsed state has moved to <admin-sidebar>'s [collapsed]
71
+ # reflected attribute (per ADR-0023). External consumers should query
72
+ # admin-sidebar[slot="leading"][collapsed] / [slot="trailing"][collapsed]
73
+ # directly. The previous data-sidebar-{name}-collapsed attributes on
74
+ # <admin-shell> are not set; new code should style via :has().
75
75
 
76
76
  traits: []
77
77