@adia-ai/adia-ui-factory 0.2.2 → 0.2.4

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 (95) hide show
  1. package/.claude-plugin/plugin.json +12 -3
  2. package/.mcp.json +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +24 -1
  5. package/bin/adia-scaffold +93 -13
  6. package/package.json +1 -1
  7. package/references/shell-embed.md +10 -0
  8. package/references/spa-architecture.md +7 -0
  9. package/skills/adia-ui-kit/CHANGELOG.md +41 -0
  10. package/skills/adia-ui-kit/SKILL.md +442 -0
  11. package/skills/adia-ui-kit/assets/figma-make/guidelines/Guidelines.md +87 -0
  12. package/skills/adia-ui-kit/assets/figma-make/guidelines/components.md +142 -0
  13. package/skills/adia-ui-kit/assets/figma-make/guidelines/styles.md +118 -0
  14. package/skills/adia-ui-kit/assets/figma-make/guidelines/tokens.md +120 -0
  15. package/skills/adia-ui-kit/assets/team-scaffold/archive-README.md +30 -0
  16. package/skills/adia-ui-kit/assets/team-scaffold/drafts-README.md +57 -0
  17. package/skills/adia-ui-kit/assets/team-scaffold/inbox-README.md +76 -0
  18. package/skills/adia-ui-kit/assets/team-scaffold/outbox-README.md +46 -0
  19. package/skills/adia-ui-kit/assets/team-scaffold/team-root.README.md +51 -0
  20. package/skills/adia-ui-kit/assets/team-scaffold/tickets-README.md +208 -0
  21. package/skills/adia-ui-kit/assets/team-scaffold/tickets-REGISTRY.md +81 -0
  22. package/skills/adia-ui-kit/assets/templates/AGENTS.template.md +128 -0
  23. package/skills/adia-ui-kit/assets/templates/CLAUDE.template.md +1 -0
  24. package/skills/adia-ui-kit/assets/templates/FEEDBACK-OMISSIONS-DEFENSE.template.md +150 -0
  25. package/skills/adia-ui-kit/assets/templates/FEEDBACK.template.md +213 -0
  26. package/skills/adia-ui-kit/assets/templates/RESPONSE.template.md +114 -0
  27. package/skills/adia-ui-kit/assets/templates/figma-make-guidelines.md +90 -0
  28. package/skills/adia-ui-kit/evals/adversarial-content-trust.json +83 -0
  29. package/skills/adia-ui-kit/evals/evals.json +55 -0
  30. package/skills/adia-ui-kit/evals/routing-corpus.json +121 -0
  31. package/skills/adia-ui-kit/evals/teach-routing-cases.json +92 -0
  32. package/skills/adia-ui-kit/references/capabilities.md +245 -0
  33. package/skills/adia-ui-kit/references/catalog-lookup.md +186 -0
  34. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v1.md +8 -0
  35. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v2.md +3585 -0
  36. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v3.md +1899 -0
  37. package/skills/adia-ui-kit/references/common-gotchas-consumer.md +522 -0
  38. package/skills/adia-ui-kit/references/component-literacy.md +291 -0
  39. package/skills/adia-ui-kit/references/composition.md +245 -0
  40. package/skills/adia-ui-kit/references/correction-loop.md +462 -0
  41. package/skills/adia-ui-kit/references/css-policy.md +343 -0
  42. package/skills/adia-ui-kit/references/decision-model.md +158 -0
  43. package/skills/adia-ui-kit/references/diagnostic-output.md +57 -0
  44. package/skills/adia-ui-kit/references/docs-catalog.md +239 -0
  45. package/skills/adia-ui-kit/references/exclusions.md +225 -0
  46. package/skills/adia-ui-kit/references/feedback-authoring.md +466 -0
  47. package/skills/adia-ui-kit/references/figma-make.md +244 -0
  48. package/skills/adia-ui-kit/references/flows-auth.md +307 -0
  49. package/skills/adia-ui-kit/references/flows-content.md +206 -0
  50. package/skills/adia-ui-kit/references/flows-data.md +348 -0
  51. package/skills/adia-ui-kit/references/flows-interaction.md +181 -0
  52. package/skills/adia-ui-kit/references/flows-shells.md +336 -0
  53. package/skills/adia-ui-kit/references/gap-detection.md +266 -0
  54. package/skills/adia-ui-kit/references/harness.md +284 -0
  55. package/skills/adia-ui-kit/references/loading-protocol.md +406 -0
  56. package/skills/adia-ui-kit/references/local-link.md +198 -0
  57. package/skills/adia-ui-kit/references/mcp.md +401 -0
  58. package/skills/adia-ui-kit/references/migration.md +116 -0
  59. package/skills/adia-ui-kit/references/mission.md +194 -0
  60. package/skills/adia-ui-kit/references/mode-routing.md +298 -0
  61. package/skills/adia-ui-kit/references/patterns-agent.md +223 -0
  62. package/skills/adia-ui-kit/references/patterns-data.md +1065 -0
  63. package/skills/adia-ui-kit/references/patterns-forms.md +223 -0
  64. package/skills/adia-ui-kit/references/patterns-layout.md +326 -0
  65. package/skills/adia-ui-kit/references/patterns-recipes.md +1416 -0
  66. package/skills/adia-ui-kit/references/patterns-shells.md +1097 -0
  67. package/skills/adia-ui-kit/references/patterns-traits.md +259 -0
  68. package/skills/adia-ui-kit/references/patterns-wiring.md +339 -0
  69. package/skills/adia-ui-kit/references/pev-rationale.md +137 -0
  70. package/skills/adia-ui-kit/references/reasoning-gate.md +146 -0
  71. package/skills/adia-ui-kit/references/reasoning-methodologies.md +112 -0
  72. package/skills/adia-ui-kit/references/recon.md +192 -0
  73. package/skills/adia-ui-kit/references/rendering-model.md +431 -0
  74. package/skills/adia-ui-kit/references/response-authoring.md +398 -0
  75. package/skills/adia-ui-kit/references/session-log-schema.md +203 -0
  76. package/skills/adia-ui-kit/references/session-log-template.md +118 -0
  77. package/skills/adia-ui-kit/references/setup.md +652 -0
  78. package/skills/adia-ui-kit/references/skill-hygiene.md +135 -0
  79. package/skills/adia-ui-kit/references/spec-to-ui-reasoning.md +394 -0
  80. package/skills/adia-ui-kit/references/state.md +229 -0
  81. package/skills/adia-ui-kit/references/teach-protocol.md +421 -0
  82. package/skills/adia-ui-kit/references/theme.md +142 -0
  83. package/skills/adia-ui-kit/references/version-check.md +69 -0
  84. package/skills/adia-ui-kit/scripts/audit-decision-trace.mjs +198 -0
  85. package/skills/adia-ui-kit/scripts/audit-kit-roster.mjs +366 -0
  86. package/skills/adia-ui-kit/scripts/changelog-archive.mjs +126 -0
  87. package/skills/adia-ui-kit/scripts/check-adia-versions.py +404 -0
  88. package/skills/adia-ui-kit/scripts/run-skill-evals.mjs +358 -0
  89. package/skills/adia-ui-kit/scripts/validate-session-log.mjs +242 -0
  90. package/skills/adia-ui-kit/skill.json +102 -0
  91. package/skills/adia-ui-migration/CHANGELOG.md +47 -0
  92. package/skills/adia-ui-migration/SKILL.md +381 -0
  93. package/skills/adia-ui-migration/skill.json +29 -0
  94. package/skills/adia-ui-spa/SKILL.md +1 -1
  95. package/skills/adia-ui-verify/SKILL.md +2 -2
@@ -0,0 +1,1416 @@
1
+ # §Patterns — Concrete answers to prompt-shaped pattern asks
2
+
3
+ > **Loading note.** Reference bundle extracted from `SKILL.md` per the
4
+ > §LoadingProtocol inversion contract (v2.20.4+). Load via
5
+ > `skill_view(name='adia-ui-kit', file_path='references/patterns-recipes.md')`
6
+ > when an active task asks for a concrete recipe matching one of the
7
+ > headings below (admin shell anatomy, REST→component data binding,
8
+ > chart data wiring, segmented-control children rule, entity-list →
9
+ > drawer pattern, status → badge mapping, design-board → token mapping,
10
+ > v0.6.18 loading state, imperative toasts via feed-ui). Skip
11
+ > otherwise.
12
+ >
13
+ > **Transitional bundle.** These 10 recipes were authored inline in
14
+ > SKILL.md §Patterns and extracted as a single bundle in v2.20.4 to
15
+ > get them out of the seed. Future polish cut (v2.21+) may redistribute
16
+ > them into the topic-bundled patterns-{shells,data,forms,layout,wiring}.md
17
+ > files where they would live more naturally:
18
+ > - Admin shell anatomy + composition tiers → `patterns-shells.md`
19
+ > - REST data binding + imperative toasts → `patterns-wiring.md`
20
+ > - Chart wiring + entity list/drawer + status badge + loading state → `patterns-data.md`
21
+ > - Segmented control rule → `patterns-forms.md`
22
+ > - Design board → token mapping → standalone or `patterns-layout.md`
23
+
24
+ Concrete answers to patterns that appear in prompts like
25
+ "Admin UI that loads X from endpoint Y following design board Z."
26
+
27
+ > **For the visual reference** of any pattern below, browse
28
+ > `https://ui-kit.exe.xyz/site/pages/patterns/`. For markup not covered
29
+ > here, check `.claude/docs/guides/71-recipes.md` (in-repo) or fetch the relevant
30
+ > page-shell template from `https://ui-kit.exe.xyz/site/modules/`.
31
+
32
+ ## Admin shell anatomy
33
+
34
+ > ⚠️ **Shell CSS is REQUIRED and not auto-imported.** Before any
35
+ > `<admin-shell>` markup renders, your entry file MUST contain:
36
+ >
37
+ > ```ts
38
+ > import '@adia-ai/web-modules/shell/admin-shell.css';
39
+ > ```
40
+ >
41
+ > `admin-shell.js` does NOT self-import its layout CSS — every layout
42
+ > rule (the fixed-viewport flex grid, sidebar widths, topbar/statusbar
43
+ > heights, scroll geometry, bespoke-tag selectors) lives in
44
+ > `admin-shell.css`. **This is policy, not a bug** — see §CSSPolicy
45
+ > above for the full reasoning (light-DOM cascade control, tree-shake
46
+ > honesty, theme layering). Omitting the import produces an **unstyled
47
+ > stacked divs layout with no error or warning** — the sidebar renders
48
+ > above the content instead of beside it. Same caveat for `chat-shell`,
49
+ > `editor-shell`, `simple-shell` — import the matching CSS for the shell
50
+ > tag you use (`shell/admin-shell.css`, `chat/chat-shell.css`,
51
+ > `editor/editor-shell.css`, `simple/simple-shell.css`).
52
+
53
+ > ℹ️ **There is only ONE CSS file per shell — `admin-shell.css`.** (FB-06)
54
+ > The `./shell/*.css` exports wildcard makes `@adia-ai/web-modules/shell/
55
+ > admin-topbar.css` / `admin-sidebar.css` / `admin-content.css` /
56
+ > `admin-page.css` / `admin-scroll.css` *look* like they should resolve,
57
+ > but **none of those files exist on disk**. Vite 8 (rolldown) fails to
58
+ > resolve them at build time with a terse error. Every sub-component
59
+ > style (topbar layout, sidebar grid, content scroll geometry, page
60
+ > sticky-header band, bespoke-tag selectors) is bundled into
61
+ > `admin-shell.css`. Import it once and you're done. Same convention for
62
+ > the other shells: one CSS file per shell tag, all sub-component styles
63
+ > bundled. The `admin-command.css` import is the ONE exception — it
64
+ > ships separately because the command palette is opt-in and most apps
65
+ > don't use it.
66
+
67
+ ```html
68
+ <admin-shell>
69
+ <!-- v0.6.13+: bare <admin-shell> auto-promotes to mode="rounded borderless"
70
+ on connectedCallback. Any explicit mode="..." attribute is respected
71
+ verbatim (including mode="" for the flat legacy chrome). The empty
72
+ string is the only opt-out; omitting the attribute entirely uses the
73
+ modern soft-chrome default. -->
74
+ <!-- Sidebar: navigation lives here -->
75
+ <admin-sidebar slot="leading" resizable collapsible>
76
+ <admin-topbar slot="header">
77
+ <span slot="heading">Claims Portal</span>
78
+ </admin-topbar>
79
+ <nav-ui>
80
+ <a href="/claims">Claims</a>
81
+ <a href="/policies">Policies</a>
82
+ <a href="/adjusters">Adjusters</a>
83
+ </nav-ui>
84
+ <div data-resize></div>
85
+ </admin-sidebar>
86
+
87
+ <!-- Main content area -->
88
+ <admin-content>
89
+ <admin-topbar>
90
+ <button-ui data-sidebar-toggle="leading" icon="sidebar" variant="ghost" size="sm"></button-ui>
91
+ <breadcrumb-ui></breadcrumb-ui>
92
+ <span data-spacer></span>
93
+ <div data-actions>
94
+ <popover-ui placement="bottom-end">
95
+ <button-ui icon="palette" variant="ghost" size="sm" slot="trigger"></button-ui>
96
+ <theme-panel slot="content" persist parametric presets scheme-toggle></theme-panel>
97
+ </popover-ui>
98
+ </div>
99
+ </admin-topbar>
100
+ <admin-scroll>
101
+ <!-- SPA route content lives here — the framework's route element
102
+ (e.g. <vue-router-view />, <Outlet />, <slot />, <lit-route-outlet>)
103
+ goes inside <admin-scroll>. In SSR contexts (Next.js / Nuxt /
104
+ SvelteKit / Astro / Rails+Turbo / Django+HTMX / Phoenix), the
105
+ framework's route children fill <admin-scroll> directly — there
106
+ is no separate route-outlet element. See §Rendering Model →
107
+ Routing ownership for the per-framework substitution table.
108
+ There is exactly ONE route owner per app — running two at once
109
+ causes click-double-handling and URL/content desync. -->
110
+ <!-- your router outlet here -->
111
+ </admin-scroll>
112
+ <admin-statusbar>
113
+ <span>App</span>
114
+ <span data-spacer></span>
115
+ <span>v1.0.0</span>
116
+ </admin-statusbar>
117
+ </admin-content>
118
+
119
+ <!-- Cmd+K command palette -->
120
+ <admin-command>
121
+ <command-ui placeholder="Search claims…"></command-ui>
122
+ </admin-command>
123
+ </admin-shell>
124
+ ```
125
+
126
+ Route setup requires a **one-time script block** — this is the sole
127
+ legitimate exception to "no `<script>`". The script only wires routing
128
+ metadata; it emits no layout or components:
129
+
130
+ > ⚠️ **SPA-only.** Skip this script entirely in SSR. Next.js / Nuxt /
131
+ > SvelteKit / Astro / Rails+Turbo / Django+HTMX / Phoenix all own
132
+ > routing themselves (filesystem routes + framework `<Link>` /
133
+ > `link_to` / `<.link>` primitives). See §Rendering Model → Routing
134
+ > ownership for the per-framework path. Adding this script in SSR
135
+ > creates two competing route owners.
136
+
137
+ > ✅ **Router provider — auto-registered from the barrel at v0.6.40+.**
138
+ > `import '@adia-ai/web-components'` side-effect-imports
139
+ > `core/provider.js` (which registers `<router-ui>`). The router
140
+ > "just works" after the standard cold-start import.
141
+ >
142
+ > ⚠️ **Subpath-imported consumers must register the provider
143
+ > explicitly.** Apps with bundle-size SLOs that skip the main
144
+ > barrel — `import '@adia-ai/web-components/components/button'` etc.
145
+ > — must add `import '@adia-ai/web-components/core/provider';`
146
+ > alongside their primitive imports. Omitting it makes
147
+ > `<router-ui>` render as `HTMLUnknownElement`,
148
+ > `router.routes = [...]` silently no-op (no error, no warning),
149
+ > and the route content area stay empty. Same failure mode applies
150
+ > in any consumer setup that side-effect-imports per-primitive
151
+ > instead of the full barrel.
152
+ >
153
+ > ```ts
154
+ > // Bundle-size-conscious cold-start:
155
+ > import '@adia-ai/web-components/components/button';
156
+ > import '@adia-ai/web-components/components/table';
157
+ > // ...
158
+ > import '@adia-ai/web-components/core/provider'; // <-- router
159
+ > ```
160
+ >
161
+ > Source: `core/provider.js` `customElements.define('router-ui', UIRouter)`;
162
+ > barrel-side-effect-import added 2026-05-25 (FB-72 substrate fix).
163
+
164
+ ```html
165
+ <script type="module">
166
+ // The ONLY script block allowed — route wiring, not layout
167
+ const router = document.querySelector('router-ui');
168
+ router.routes = [
169
+ { path: '/claims', content: '/pages/claims-list.html', title: 'Claims' },
170
+ { path: '/claims/:id', content: '/pages/claim-detail.html', title: 'Claim Detail' },
171
+ { path: '/policies', content: '/pages/policies.html', title: 'Policies' },
172
+ ];
173
+ </script>
174
+ ```
175
+
176
+ Each page file is a standalone HTML fragment (no `<html>` wrapper).
177
+ `<router-ui>` fetches and injects the fragment on navigation.
178
+
179
+ ## App-shell composition tiers — Advanced / Simple / Minimal (branched checklist)
180
+
181
+ The admin-shell anatomy above is the **canonical structure**, but real
182
+ apps differ in how many of the shell's optional surfaces they actually
183
+ populate. Pick a tier explicitly **at the §0 Reasoning Gate** — don't
184
+ defer it until mid-composition. The default for any new product surface
185
+ is **Advanced**; drop down only when there's a specific reason.
186
+
187
+ Use the matching tier as the starting checklist; then trim
188
+ component-by-component as your audit justifies it. Live references:
189
+ [admin-shell.examples.html](https://ui-kit.exe.xyz/site/components/admin-shell)
190
+ ships a side-by-side gallery of all three tiers.
191
+
192
+ #### Decision rule
193
+
194
+ ```
195
+ Does the product span more than one platform / sub-product? → Advanced
196
+ Does the app need an authenticated user identity? → Advanced
197
+ Does the surface need contextual help / changelog visibility? → Advanced
198
+ ↓ NONE of the above:
199
+ Does the app need authentication + a meaningful nav tree? → Simple
200
+ ↓ otherwise:
201
+ Single page or two-route demo, no auth, flat nav → Minimal
202
+ ```
203
+
204
+ When in doubt, **start Advanced and trim**. Adding `<context-switcher>`
205
+ or a trailing sidebar to a Minimal scaffold mid-build is a multi-slot
206
+ refactor; removing them from an Advanced scaffold is delete-only.
207
+
208
+ #### Tier checklists
209
+
210
+ | Surface | Advanced (default) | Simple | Minimal |
211
+ |---|---|---|---|
212
+ | **Leading sidebar** (`<admin-sidebar slot="leading">`) | ✓ resizable + collapsible | ✓ collapsible | ✓ (no resize) |
213
+ | ↳ Context switcher (top, `<select-ui avatar="…" value="…" variant="ghost">` for workspace/product jump) | ✓ | — (static brand `<span slot="heading">`) | — (static brand) |
214
+ | ↳ `<nav-ui>` with `<nav-item-ui>` + `<nav-group-ui>` + dividers | ✓ grouped + nested + badges + dividers | ✓ groups, no badges | ✓ flat nav-item-ui only |
215
+ | ↳ User menu (sidebar footer `<admin-statusbar slot="footer">` + `<select-ui avatar="…" value="…" variant="ghost">`) | ✓ avatar + account + sign-out | ✓ sign-in entry only | — |
216
+ | **Topbar** (`<admin-topbar slot="header">` inside `<admin-content>`) <sup>*</sup> | ✓ | ✓ | ✓ |
217
+ | ↳ Leading: sidebar toggle button + `<breadcrumb-ui>` (hydrated) | ✓ | ✓ | ✓ |
218
+ | ↳ Trailing `slot="action"`: theme toggle + sidebar toggle for trailing sidebar | ✓ | ✓ (theme only) | — |
219
+ | **Scroll region** (`<admin-scroll>`) | ✓ | ✓ | ✓ |
220
+ | ↳ `<router-ui>` (SPA) or framework children (SSR, see §Rendering Model → Routing ownership) | ✓ | ✓ | ✓ |
221
+ | ↳ `<admin-page>` wrapping route content | ✓ | ✓ | ✓ |
222
+ | ↳ `<admin-page-header>` with `<header>` + `<tabs-ui>` for page-scoped tabs | ✓ | ✓ (no tabs unless the page needs them) | — (h1 only) |
223
+ | ↳ `<admin-page-body>` for page content | ✓ | ✓ | ✓ |
224
+ | **Content statusbar** (`<admin-statusbar slot="footer">` inside `<admin-content>`) | ✓ pkg + skill versions (overrideable) | — | — |
225
+ | **Trailing sidebar** (`<admin-sidebar slot="trailing">`) | ✓ changelog / contextual help | — | — |
226
+ | **Command palette** (`<admin-command>`) | ✓ Cmd+K `<command-ui>` | optional | — |
227
+
228
+ #### Advanced — full markup (start here for any new product surface)
229
+
230
+ > **SoT alignment.** This markup mirrors `site/index.html` (the live
231
+ > docs-site shell at `ui-kit.exe.xyz`) and `admin-shell.examples.html`'s
232
+ > sidebar-variant gallery (the canonical demo). Where the two diverge,
233
+ > the gallery is the new-code canonical form; `site/index.html` ships
234
+ > a few legacy patterns (`[data-actions]` instead of `slot="action"`,
235
+ > `data-sidebar-toggle-trailing` boolean instead of
236
+ > `data-sidebar-toggle="trailing"` value) that admin-topbar.yaml flags
237
+ > as "for one release, prefer slots." This skill teaches the
238
+ > new-canonical forms; if you're working in the chat-ui repo itself,
239
+ > the docs site's bespoke wiring may differ.
240
+ >
241
+ > <sup>*</sup> **`<admin-topbar slot="header">` vs no-slot.** Both forms work and render identically. `<admin-content>` is light-DOM per ADR-0033 (no shadow root), so `slot="header"` is inert and CSS positions `<admin-topbar>` by tag + DOM order regardless. This skill and `site/index.html` use `slot="header"` for parity with substrate demos; `loading-protocol.md`'s 13-part list uses the no-slot form. Both are correct; do not "fix" one to the other.
242
+
243
+ ```html
244
+ <admin-shell mode="rounded borderless">
245
+
246
+ <!-- ═══ Leading sidebar ═══
247
+ name="…" attribute is OPTIONAL — defaults to the slot value
248
+ ("leading" / "trailing") for localStorage namespacing. Only set
249
+ it when running multiple sidebars with the same slot. -->
250
+ <admin-sidebar slot="leading" resizable collapsible>
251
+
252
+ <!-- Top: context switcher (jump between products / platforms /
253
+ workspaces). Canonical pattern: <select-ui avatar="…" value="…"
254
+ variant="ghost"> — used by the live docs site at
255
+ ui-kit.exe.xyz/site/examples/admin-dashboard (chat-ui:site/index.html
256
+ line 47). The `avatar` attribute renders the leading image; the
257
+ `value` attribute selects the current option from .options=[…].
258
+ No <span slot="trigger"> hand-roll needed — the primitive
259
+ self-stamps the trigger row (avatar + label + caret-up-down).
260
+
261
+ Legacy form (admin-shell.examples.html v0.5.x): <menu-ui> with
262
+ a button-trigger holding brand logo + label + caret. Still
263
+ works but is NOT the canonical pattern as of v0.6.30 — the
264
+ site shell migrated to select-ui because it handles
265
+ workspace/user dropdown cases more cleanly (auto avatar
266
+ stamping, value+options data binding, narrower API surface).
267
+ menu-ui remains correct for richer non-dropdown trigger menus
268
+ (multi-level command menus, etc.) but NOT for the
269
+ context-switcher slot. -->
270
+ <admin-topbar slot="header">
271
+ <select-ui id="workspace-select"
272
+ avatar="/icons/brand.svg"
273
+ value="studio"
274
+ variant="ghost"
275
+ style="flex:1">
276
+ <!-- Options hydrated via .options = [{ value, label, icon? }, …]
277
+ in the page's main.ts. The primitive stamps the listbox +
278
+ trigger row automatically. -->
279
+ </select-ui>
280
+ </admin-topbar>
281
+
282
+ <!-- Middle: nav with groups + dividers + badges.
283
+ Canonical wrapper: <section-ui> (the AdiaUI primitive). Used by
284
+ the live docs site at chat-ui:site/index.html line 50. The
285
+ section-ui wrap is what admin-sidebar's light-DOM CSS targets
286
+ for inner scroll + padding rules; native <section> works in
287
+ simple cases but loses the scroll-area treatment that
288
+ section-ui provides.
289
+
290
+ Legacy form: native <section>. The admin-shell.examples.html
291
+ "Advanced" gallery uses native <section> from before
292
+ section-ui shipped. Treat it as a deprecated alternate; prefer
293
+ <section-ui>. -->
294
+ <section-ui>
295
+ <nav-ui>
296
+ <nav-item-ui icon="house" text="Dashboard" value="dashboard" selected></nav-item-ui>
297
+ <nav-item-ui icon="tray" text="Inbox" value="inbox" badge="12"></nav-item-ui>
298
+
299
+ <hr data-nav-divider>
300
+
301
+ <nav-group-ui icon="chart-line" text="Analytics" open>
302
+ <nav-item-ui text="Overview" value="an-overview"></nav-item-ui>
303
+ <nav-item-ui text="Funnels" value="an-funnels"></nav-item-ui>
304
+ <nav-item-ui text="Retention" value="an-retention"></nav-item-ui>
305
+ </nav-group-ui>
306
+
307
+ <nav-group-ui icon="folders" text="Content">
308
+ <nav-item-ui text="Pages" value="content-pages"></nav-item-ui>
309
+ <nav-item-ui text="Media" value="content-media" badge="3"></nav-item-ui>
310
+ <nav-item-ui text="Drafts" value="content-drafts"></nav-item-ui>
311
+ </nav-group-ui>
312
+
313
+ <hr data-nav-divider>
314
+
315
+ <nav-group-ui icon="gear" text="Settings">
316
+ <nav-item-ui text="General" value="settings-general"></nav-item-ui>
317
+ <nav-item-ui text="Members" value="settings-members"></nav-item-ui>
318
+ <nav-item-ui text="Billing" value="settings-billing"></nav-item-ui>
319
+ </nav-group-ui>
320
+ </nav-ui>
321
+ </section-ui>
322
+
323
+ <!-- Bottom: user menu (avatar + account menu).
324
+ Inside an <admin-sidebar slot="leading"> the <admin-statusbar
325
+ slot="footer"> is the USER MENU band. Same primitive used at
326
+ <admin-content slot="footer"> = VERSION-STRIP band (see below).
327
+
328
+ Canonical pattern: <select-ui avatar="…" value="…" variant="ghost">
329
+ — used by the live docs site (chat-ui:site/index.html line 62).
330
+ Same primitive as the context-switcher above; the `avatar`
331
+ attribute auto-stamps the leading image (use a URL like
332
+ `https://i.pravatar.cc/32?u=username` or your own /icons/ path).
333
+
334
+ Legacy form (admin-shell.examples.html v0.5.x): <menu-ui> with
335
+ a button-trigger holding <avatar-ui> + name. Still works but is
336
+ NOT the canonical pattern as of v0.6.30; the docs site
337
+ migrated to select-ui for the same reasons as the
338
+ context-switcher above (auto avatar stamping, narrower API
339
+ surface, value+options data binding). -->
340
+ <admin-statusbar slot="footer">
341
+ <select-ui id="user-select"
342
+ avatar="https://i.pravatar.cc/32?u=maya"
343
+ value="maya"
344
+ variant="ghost"
345
+ style="flex:1">
346
+ <!-- Options: { value: 'maya', label: 'Maya Chen', icon: 'user' }, etc.
347
+ For account actions (Profile / Settings / Sign out), add
348
+ them as option items; select-ui emits change events on
349
+ selection that the page can route to the right action. -->
350
+ </select-ui>
351
+ </admin-statusbar>
352
+
353
+ <!-- Legacy menu-ui form (kept for reference; do not use for new
354
+ shells — site/index.html is canonical):
355
+
356
+ <admin-statusbar slot="footer">
357
+ <menu-ui placement="top-start">
358
+ <button type="button" slot="trigger" class="user-switcher">
359
+ <avatar-ui text="Maya Chen" size="sm"></avatar-ui>
360
+ <span class="user-label">Maya Chen</span>
361
+ <icon-ui name="caret-up-down" size="14"></icon-ui>
362
+ </button>
363
+ <menu-item-ui icon="user" text="Profile" value="profile"></menu-item-ui>
364
+ <menu-item-ui icon="gear" text="Account settings" value="account"></menu-item-ui>
365
+ <menu-item-ui icon="moon" text="Appearance" value="appearance"></menu-item-ui>
366
+ <menu-divider-ui></menu-divider-ui>
367
+ <menu-item-ui icon="sign-out" text="Sign out" value="signout"></menu-item-ui>
368
+ </menu-ui>
369
+ </admin-statusbar>
370
+ -->
371
+
372
+ <!-- Read-only identity primitive — admin-entity-item.
373
+ The canonical shell-tier identity-row primitive (shipped
374
+ @adia-ai/web-modules@0.7.13, FEEDBACK-38). Use it on EITHER
375
+ end of <admin-sidebar>:
376
+ - <admin-topbar slot="header"> for brand chrome (single-
377
+ product, no switcher) — see patterns-shells.md §Brand
378
+ chrome Pattern A
379
+ - <admin-statusbar slot="footer"> for user identity (non-
380
+ interactive, no menu)
381
+
382
+ Geometry mirrors <select-ui>'s trigger row (icon ≡ leading,
383
+ label ≡ display, badge ≡ caret) so it aligns 1px-perfect with
384
+ <nav-item-ui> icons in the section between header and footer.
385
+ The wrapper collapses as one slottable unit inside a
386
+ collapsible sidebar: icon survives, label + badge hide. Without
387
+ admin-entity-item, bare [slot="icon"] + [slot="heading"]
388
+ siblings have no shared collapse boundary and land at the
389
+ topbar's --page-sidebar-px (4px) — 8px short of nav alignment.
390
+
391
+ Brand chrome (header):
392
+ <admin-topbar slot="header">
393
+ <admin-entity-item slot="heading">
394
+ <icon-ui slot="icon" name="lightning"></icon-ui>
395
+ <span slot="label">Claims Portal</span>
396
+ <!-- Optional: <badge-ui slot="badge" text="Staging" size="sm" variant="warning"></badge-ui> -->
397
+ </admin-entity-item>
398
+ </admin-topbar>
399
+
400
+ User identity (footer):
401
+ <admin-statusbar slot="footer">
402
+ <admin-entity-item slot="heading">
403
+ <icon-ui slot="icon" name="user-circle"></icon-ui>
404
+ <span slot="label">Kim Granlund</span>
405
+ <badge-ui slot="badge" text="Operator" size="sm" variant="muted"></badge-ui>
406
+ </admin-entity-item>
407
+ </admin-statusbar>
408
+
409
+ For an INTERACTIVE workspace switcher OR user menu, keep the
410
+ <select-ui>/<menu-ui> trigger patterns above. admin-entity-item
411
+ is read-only display; select-ui/menu-ui are interactive. -->
412
+
413
+
414
+ <div data-resize></div>
415
+ </admin-sidebar>
416
+
417
+ <!-- ═══ Main content area ═══ -->
418
+ <admin-content>
419
+
420
+ <admin-topbar slot="header">
421
+ <!-- Leading: sidebar toggle + hydrated breadcrumb.
422
+ data-sidebar-toggle="leading|trailing" is the canonical
423
+ value-bearing attribute (per admin-shell.js wiring); the
424
+ sidebar matches by its `name` attribute (default = slot value). -->
425
+ <button-ui data-sidebar-toggle="leading" icon="sidebar" variant="ghost" size="sm"></button-ui>
426
+ <breadcrumb-ui>
427
+ <a href="/">Studio</a>
428
+ <a href="/analytics">Analytics</a>
429
+ <span aria-current="page">Overview</span>
430
+ </breadcrumb-ui>
431
+
432
+ <!-- Trailing: persistent app-wide controls.
433
+ slot="action" is the new-code canonical anchor (v0.6.13+).
434
+ The legacy <div data-actions> hook still works (admin-topbar.yaml
435
+ "coexists for one release") and is what the docs-site shell
436
+ ships today; new code should prefer slot="action".
437
+
438
+ For richer theme control than scheme toggle (light/dark/system),
439
+ swap <toggle-scheme-ui> for <popover-ui> + <theme-panel> — the
440
+ docs site uses the panel form. -->
441
+ <toggle-scheme-ui slot="action" scheme="auto"></toggle-scheme-ui>
442
+ <button-ui slot="action" data-sidebar-toggle="trailing" icon="sidebar-simple-reverse" variant="ghost" size="sm"></button-ui>
443
+ </admin-topbar>
444
+
445
+ <admin-scroll>
446
+ <!-- SPA: the router outlet stays EMPTY in the shell — admin-page +
447
+ admin-page-header + admin-page-body live in the ROUTE FRAGMENT
448
+ (a separate .html file loaded by the router). The shell defines
449
+ layout; routes define page content. See the "Route fragment"
450
+ snippet below. SSR: omit the outlet entirely — see §Rendering
451
+ Model → Routing ownership. -->
452
+ <!-- your router outlet here -->
453
+ </admin-scroll>
454
+
455
+ <!-- Content statusbar: status / version-strip. The same <admin-statusbar>
456
+ primitive that hosted the user menu in the sidebar is the
457
+ version-strip band when used at admin-content slot="footer".
458
+ Use <span data-spacer> to push trailing content to the end.
459
+ The version label is meant to be overridden per-product (sync
460
+ indicator, last-saved timestamp, row count, etc.). -->
461
+ <admin-statusbar slot="footer">
462
+ <span>Adia Studio</span>
463
+ <span data-spacer></span>
464
+ <span id="version-label">v0.6.19</span>
465
+ </admin-statusbar>
466
+
467
+ </admin-content>
468
+
469
+ <!-- ═══ Trailing sidebar — changelog / contextual help ═══
470
+ Starts hidden — click the trailing-sidebar toggle in the topbar
471
+ to reveal. resizable + collapsible matches the leading sidebar
472
+ so users get consistent affordances. -->
473
+ <admin-sidebar slot="trailing" resizable collapsible hidden>
474
+ <admin-topbar slot="header">
475
+ <span slot="heading">What's new</span>
476
+ <!-- Close button — slot="action" cluster (canonical) -->
477
+ <button-ui slot="action" data-sidebar-toggle="trailing" icon="x" variant="ghost" size="sm" aria-label="Close changelog"></button-ui>
478
+ </admin-topbar>
479
+ <section-ui>
480
+ <text-ui variant="kicker">v0.6.18 · 2026-05-21</text-ui>
481
+ <text-ui variant="heading">Loading state is first-class</text-ui>
482
+ <text-ui>
483
+ <code>&lt;stat-ui loading&gt;</code> + <code>&lt;table-ui loading&gt;</code> now
484
+ render skeleton placeholders while data fetches. Pair with
485
+ <code>data-stream-*</code> for declarative loading.
486
+ </text-ui>
487
+ </section-ui>
488
+ <div data-resize></div>
489
+ </admin-sidebar>
490
+
491
+ <!-- ═══ Cmd+K command palette ═══ -->
492
+ <admin-command>
493
+ <command-ui placeholder="Search anything…"></command-ui>
494
+ </admin-command>
495
+
496
+ </admin-shell>
497
+ ```
498
+
499
+ **Route fragment** (`pages/overview.html`) — the content the router
500
+ fetches and injects into `<router-ui>` on navigation. Each route is a
501
+ standalone HTML fragment with no `<html>` / `<body>` wrapper:
502
+
503
+ ```html
504
+ <admin-page>
505
+ <admin-page-header>
506
+ <header>
507
+ <div>
508
+ <h1>Overview</h1>
509
+ <div data-actions>
510
+ <button-ui text="Export" icon="download"></button-ui>
511
+ <button-ui text="New" variant="primary" icon="plus"></button-ui>
512
+ </div>
513
+ </div>
514
+ </header>
515
+ <tabs-ui value="summary">
516
+ <tab-ui value="summary">Summary</tab-ui>
517
+ <tab-ui value="trends">Trends</tab-ui>
518
+ <tab-ui value="cohorts">Cohorts</tab-ui>
519
+ </tabs-ui>
520
+ </admin-page-header>
521
+ <admin-page-body>
522
+ <section>
523
+ <!-- Page content — KPI cards, tables, charts wired via data-stream-* -->
524
+ </section>
525
+ </admin-page-body>
526
+ </admin-page>
527
+ ```
528
+
529
+ > ⚠️ **`<admin-page-body>` needs an inner column-owner** —
530
+ > `<section>` (or `<header-ui>` / `<section-ui>` / `<iframe>` /
531
+ > `<editor-shell>` / `<pane-ui>`). The wrapper element is a
532
+ > flex/grid container that doesn't own the `--card-inset` padding;
533
+ > a bare `<col-ui>` / `<grid-ui>` / `<card-ui>` directly inside
534
+ > renders edge-to-edge with zero gutter. The inner `<section>` carries
535
+ > the inset. Same shape for `<admin-page-header>` and its inner
536
+ > `<header>` — both wrap a content-owner. See
537
+ > `patterns-shells.md` §Wrapper-vs-column callout + the matching
538
+ > §5 entry in `common-gotchas-consumer.md`. Mechanically
539
+ > enforced via `npm run audit:admin-page-structure --strict`.
540
+
541
+ Route wiring (the one-time script block from "Admin shell anatomy"
542
+ above) connects each route path to its fragment URL.
543
+
544
+ **SSR variant** — in Next.js / Nuxt / SvelteKit / Astro the route
545
+ fragment is whatever the framework injects as `{children}` /
546
+ `<slot />` / `<Astro.children />`; the empty `<router-ui>` slot in
547
+ `<admin-scroll>` is replaced by that injection point. See
548
+ §Rendering Model → Routing ownership for the full per-framework table.
549
+
550
+ #### Simple — Advanced minus context-switcher, trailing sidebar, content statusbar
551
+
552
+ Use when there's **one product**, optional auth, and no need for
553
+ persistent contextual chrome. Delta from Advanced (5 deletions):
554
+
555
+ ```diff
556
+ <admin-sidebar slot="leading" resizable collapsible name="primary">
557
+ <admin-topbar slot="header">
558
+ - <select-ui avatar="…" value="…" variant="ghost"></select-ui> <!-- context-switcher -->
559
+ + <span slot="heading">My App</span> <!-- static brand -->
560
+ </admin-topbar>
561
+ <section-ui><nav-ui>…</nav-ui></section-ui>
562
+ <admin-statusbar slot="footer">
563
+ <!-- Either: <select-ui avatar="…"> for full account, OR a single Sign-in entry: -->
564
+ - <select-ui avatar="…" value="…" variant="ghost"></select-ui> <!-- full user menu -->
565
+ + <button-ui text="Sign in" variant="ghost" icon="sign-in"></button-ui>
566
+ </admin-statusbar>
567
+ </admin-sidebar>
568
+
569
+ <admin-content>
570
+ <admin-topbar slot="header">…</admin-topbar>
571
+ <admin-scroll>
572
+ <router-ui><admin-page>…</admin-page></router-ui>
573
+ </admin-scroll>
574
+ - <admin-statusbar slot="footer">…version-strip…</admin-statusbar>
575
+ </admin-content>
576
+
577
+ - <admin-sidebar slot="trailing" collapsible name="changelog">…</admin-sidebar>
578
+ <admin-command>…</admin-command>
579
+ </admin-shell>
580
+ ```
581
+
582
+ Keep the topbar trailing `slot="action"` cluster (theme toggle is still
583
+ useful even without a trailing sidebar). Drop the trailing-sidebar
584
+ toggle button since the trailing sidebar is gone.
585
+
586
+ #### Minimal — Advanced minus auth, statusbar (both), trailing sidebar, command palette, tabs
587
+
588
+ Use for single-page demos, two-route prototypes, or embedded surfaces
589
+ where the parent already owns chrome. Delta from Advanced:
590
+
591
+ ```diff
592
+ <admin-shell>
593
+ <admin-sidebar slot="leading" collapsible name="primary"> <!-- no resizable -->
594
+ <admin-topbar slot="header">
595
+ - <select-ui avatar="…" value="…" variant="ghost"></select-ui> <!-- context-switcher -->
596
+ + <span slot="heading">My App</span>
597
+ </admin-topbar>
598
+ <section-ui>
599
+ <nav-ui>
600
+ - <nav-item-ui …badge="12"></nav-item-ui>
601
+ - <nav-group-ui …>…</nav-group-ui> <!-- no groups -->
602
+ + <nav-item-ui icon="house" text="Home" value="home" selected></nav-item-ui>
603
+ + <nav-item-ui icon="folder" text="Items" value="items"></nav-item-ui>
604
+ + <nav-item-ui icon="gear" text="Settings" value="settings"></nav-item-ui>
605
+ </nav-ui>
606
+ </section-ui>
607
+ - <admin-statusbar slot="footer">…</admin-statusbar> <!-- no user menu -->
608
+ </admin-sidebar>
609
+
610
+ <admin-content>
611
+ <admin-topbar slot="header">
612
+ <button-ui data-sidebar-toggle="leading" icon="sidebar" …></button-ui>
613
+ <breadcrumb-ui>…</breadcrumb-ui>
614
+ - <toggle-scheme-ui slot="action" …></toggle-scheme-ui>
615
+ - <button-ui slot="action" data-sidebar-toggle="trailing" …></button-ui>
616
+ </admin-topbar>
617
+ <admin-scroll>
618
+ <router-ui>
619
+ <admin-page>
620
+ <admin-page-header>
621
+ <header><h1>Home</h1></header>
622
+ - <tabs-ui>…</tabs-ui> <!-- no tabs -->
623
+ </admin-page-header>
624
+ <admin-page-body>…</admin-page-body>
625
+ </admin-page>
626
+ </router-ui>
627
+ </admin-scroll>
628
+ - <admin-statusbar slot="footer">…version-strip…</admin-statusbar>
629
+ </admin-content>
630
+
631
+ - <admin-sidebar slot="trailing" …>…</admin-sidebar>
632
+ - <admin-command>…</admin-command>
633
+ </admin-shell>
634
+ ```
635
+
636
+ #### Authoring discipline
637
+
638
+ - **Default to Advanced.** It's delete-only to step down. Stepping up is
639
+ a multi-slot refactor.
640
+ - **Audit against SoT BEFORE authoring.** The canonical references are
641
+ `site/index.html` (the live docs-site shell), `apps/saas/app/
642
+ admin-dashboard/admin-dashboard.contents.html` (a real product
643
+ surface), and `packages/web-modules/shell/admin-shell/admin-shell.examples.html`'s
644
+ sidebar-variant gallery. When yaml + SoT disagree (e.g. `slot="action"`
645
+ vs `[data-actions]`), yaml is the new-code canonical — but check
646
+ whether the SoT lags before deciding which to ship in a chunk that
647
+ must work against the deployed system. **A 30-second `cat site/index.html`
648
+ prevents the divergence class.**
649
+ - **Each removal must be justified at the §0 Reasoning Gate.** "There's
650
+ no second product" → context-switcher out. "No authenticated identity"
651
+ → user-menu out. "No persistent chrome need" → trailing sidebar out.
652
+ - **`name="…"` on sidebars is OPTIONAL.** It defaults to the slot value
653
+ (`"leading"` / `"trailing"`) for `localStorage` namespacing. Only set
654
+ it when running multiple sidebars with the same slot (e.g. two
655
+ trailing sidebars on different routes).
656
+ - **`data-sidebar-toggle="<name>"` is value-bearing.** Buttons target
657
+ the sidebar by its `name` attribute (default = slot value). The
658
+ docs-site shell ships a bespoke boolean form
659
+ `data-sidebar-toggle-trailing` — that's the docs site's own wiring,
660
+ NOT the canonical contract documented in `admin-shell.js`.
661
+ - **`<admin-statusbar>` is dual-use.** Inside `<admin-sidebar slot="leading">
662
+ slot="footer">` it's the **user menu band**. Inside `<admin-content>
663
+ slot="footer">` it's the **content version-strip / status band**.
664
+ Same primitive, different host = different role.
665
+ - **`<admin-page-header>` always wraps a `<header>`** (native HTML). Inside it,
666
+ put `<h1>` + the action cluster in a title-row `<div>`
667
+ (`<div><h1/><div data-actions>…</div></div>`) — `[data-actions]` is what the
668
+ substrate right-aligns; a bare `<span slot="action">` is NOT styled for
669
+ `admin-page-header` and stacks under the `<h1>`. Tabs go AFTER the `<header>`
670
+ but still inside `<admin-page-header>`.
671
+ - **`<router-ui>` stays EMPTY in the shell.** `<admin-page>` +
672
+ `<admin-page-header>` + `<admin-page-body>` live in the **route
673
+ fragment** — a separate `.html` file loaded by the router on
674
+ navigation. The shell defines layout; routes define page content.
675
+ Putting `<admin-page>` inline inside `<router-ui>` makes the page
676
+ static across all routes (the router can't replace it).
677
+ - **The version-strip in the content statusbar is meant to be
678
+ overridden.** Most products replace it with their own status content
679
+ (sync indicator, row count, last-saved timestamp). Keep the
680
+ `<span data-spacer></span>` between leading + trailing spans to
681
+ preserve the push-to-end layout.
682
+
683
+ ## Data binding: REST endpoint → component
684
+
685
+ Use `data-stream-*` attributes for declarative HTTP binding.
686
+ **No `fetch()` calls in script blocks.** No manual DOM updates.
687
+
688
+ ```html
689
+ <!-- GET /api/claims every 10s → table rows -->
690
+ <table-ui
691
+ data-stream-src="/api/claims"
692
+ data-stream-interval="10000"
693
+ data-stream-path="items">
694
+ </table-ui>
695
+
696
+ <!-- GET /api/stats (one-shot) → KPI card -->
697
+ <stat-ui
698
+ label="Open Claims"
699
+ data-stream-src="/api/stats"
700
+ data-stream-target="value"
701
+ data-stream-path="open_count">
702
+ </stat-ui>
703
+
704
+ <!-- SSE stream → live updates -->
705
+ <table-ui
706
+ data-stream-src="/api/claims/live"
707
+ data-stream-mode="sse"
708
+ data-stream-event="claim-updated">
709
+ </table-ui>
710
+ ```
711
+
712
+ | Attribute | Purpose | Example |
713
+ |---|---|---|
714
+ | `data-stream-src` | Endpoint URL | `/api/claims` |
715
+ | `data-stream-interval` | Poll interval (ms) | `10000` |
716
+ | `data-stream-mode` | `fetch` (default) or `sse` | `sse` |
717
+ | `data-stream-path` | JSON path into response | `items`, `data.claims` |
718
+ | `data-stream-target` | Which property to set | `value`, `rows`, `textContent` |
719
+ | `data-stream-method` | HTTP method | `POST` |
720
+ | `data-stream-headers` | JSON headers string | `{"Authorization":"Bearer …"}` |
721
+ | `data-stream-id` | Share one transport across elements | `claims-feed` |
722
+
723
+ **Shared transport:** two elements with the same `data-stream-id` share
724
+ one HTTP connection:
725
+
726
+ ```html
727
+ <stat-ui data-stream-id="claims-feed" data-stream-src="/api/claims" data-stream-path="total" data-stream-target="value" label="Total"></stat-ui>
728
+ <table-ui data-stream-id="claims-feed" data-stream-path="items"></table-ui>
729
+ ```
730
+
731
+ ## Chart data wiring
732
+
733
+ `chart-ui` takes its data as a **JS property** — an array of plain objects —
734
+ plus `x=` / `y=` attributes naming which object key maps to each axis. It is
735
+ **not** the Chart.js `{ labels, datasets }` envelope; passing that (or any
736
+ non-array) renders a blank chart (a one-shot `console.warn` flags it).
737
+
738
+ ```js
739
+ const chart = document.querySelector('chart-ui');
740
+ chart.data = [
741
+ { month: 'Jan', revenue: 3200, expenses: 2100 },
742
+ { month: 'Feb', revenue: 4100, expenses: 2400 },
743
+ ];
744
+ ```
745
+
746
+ ```html
747
+ <!-- x= / y= name the object keys. Multi-series: comma-separate y. -->
748
+ <chart-ui type="bar" x="month" y="revenue"></chart-ui>
749
+ <chart-ui type="line" x="month" y="revenue,expenses"></chart-ui>
750
+
751
+ <!-- Sparkline / pie / donut: y only, no x -->
752
+ <chart-ui type="sparkline" y="revenue"></chart-ui>
753
+ ```
754
+
755
+ `x` and `y` are documented props in `chart.yaml`; `.data` is a JS property
756
+ (also settable as a `data="[…]"` JSON-string attribute). The Chart.js
757
+ `{ labels, datasets }` shape is a different library's API — never chart-ui's.
758
+
759
+ ## Segmented control — children must be `<segment-ui>`
760
+
761
+ `segmented-ui`'s children must be `<segment-ui>` custom elements. A bare
762
+ `<segment>` tag renders its text but receives **no** sliding indicator, no
763
+ `role="radio"` / `aria-checked`, and no reliable `change` state — silently
764
+ (a one-shot `console.warn` flags a non-`segment-ui` child).
765
+
766
+ ```html
767
+ <!-- CORRECT -->
768
+ <segmented-ui value="30d">
769
+ <segment-ui value="30d" text="30d"></segment-ui>
770
+ <segment-ui value="7d" text="7d"></segment-ui>
771
+ <segment-ui value="1d" text="1d"></segment-ui>
772
+ </segmented-ui>
773
+
774
+ <!-- WRONG — bare <segment> breaks the indicator + a11y -->
775
+ <segmented-ui value="30d">
776
+ <segment value="30d">30d</segment>
777
+ </segmented-ui>
778
+ ```
779
+
780
+ Importing `@adia-ai/web-components/components/segmented` pulls in
781
+ `<segment-ui>` transitively — no separate `components/segment` import needed.
782
+
783
+ ## Entity list → detail drawer pattern
784
+
785
+ The canonical pattern for "list of claims → click row → detail panel":
786
+
787
+ ```html
788
+ <!-- List view -->
789
+ <table-ui id="claims-table" data-stream-src="/api/claims" data-stream-path="items">
790
+ </table-ui>
791
+
792
+ <!-- Detail drawer: header + body + dual-cluster footer -->
793
+ <drawer-ui id="claim-drawer" side="right" size="lg">
794
+
795
+ <header slot="header">
796
+ <col-ui gap="1">
797
+ <h3 id="drawer-title">Claim Detail</h3>
798
+ <badge-ui id="drawer-status-badge" size="sm" hidden></badge-ui>
799
+ </col-ui>
800
+ <span slot="action">
801
+ <button-ui id="drawer-close" icon="x" variant="ghost" size="sm" aria-label="Close"></button-ui>
802
+ </span>
803
+ </header>
804
+
805
+ <section slot="body" id="claim-detail-body">
806
+ <!-- populated by row-select event.
807
+ NOTE: drawer-ui's body slot is the content surface.
808
+ Wrap stacked content in <col-ui gap="N"> if you need vertical rhythm;
809
+ drawer body has no default flex layout (only padding inset). -->
810
+ </section>
811
+
812
+ <footer slot="footer">
813
+ <!-- Dual-cluster footer: destructive on the left, commit on the right -->
814
+ <span slot="action-leading">
815
+ <button-ui id="drawer-delete" text="Delete" icon="trash" variant="danger" hidden></button-ui>
816
+ </span>
817
+ <span slot="action">
818
+ <button-ui id="drawer-cancel" text="Cancel" variant="ghost"></button-ui>
819
+ <button-ui id="drawer-save" text="Save" variant="primary" disabled></button-ui>
820
+ </span>
821
+ </footer>
822
+
823
+ </drawer-ui>
824
+
825
+ <script type="module">
826
+ // Row-select wiring — the only permitted script
827
+ document.getElementById('claims-table').addEventListener('select', e => {
828
+ const drawer = document.getElementById('claim-drawer');
829
+ const body = document.getElementById('claim-detail-body');
830
+ // Populate detail from event.detail.row
831
+ body.querySelector('[data-field="id"]').textContent = e.detail.row.id;
832
+ drawer.open = true;
833
+ });
834
+ </script>
835
+ ```
836
+
837
+ > **Button density inside the drawer.** Header close button: `size="sm"`
838
+ > (icon-only, tucked in the corner — matches topbar chrome convention).
839
+ > Footer commit/cancel/delete buttons: **default size (no `size=` attribute)**
840
+ > — these are terminal commit/cancel/delete affordances on the user's
841
+ > decision moment; sub-sizing them with `size="sm"` makes them feel
842
+ > optional on the ~80px footer band. See §Button density by surface below.
843
+
844
+ > **Dual-cluster footer layout.** `slot="action-leading"` (destructive,
845
+ > left edge) + `slot="action"` (commit/cancel, trailing edge) is the
846
+ > canonical Delete/Cancel/Save layout. Do NOT wrap the buttons in a `<div>`
847
+ > — drawer-ui's footer CSS positions each `[slot=…]` cluster directly;
848
+ > wrapping bypasses the slot grid (same anti-pattern as card-ui header).
849
+
850
+ > **Drawer slot vocabulary.** `<drawer-ui>` internally creates the
851
+ > `<dialog>` host; **content MUST be slotted** via `slot="header"` /
852
+ > `slot="body"` / `slot="footer"` (the three positioned slots). Direct
853
+ > children with no `slot=` attribute do not project — the drawer renders
854
+ > empty. The earlier `<card-ui>` wrapper shape (recipe pre-2026-05-25)
855
+ > still works but offloads chrome ownership to the card; for terminal
856
+ > commit/cancel flows, native drawer slots give cleaner footer layout.
857
+
858
+ ### Button density by surface
859
+
860
+ The footer-vs-chrome size convention matters across the kit. Default
861
+ size = "the action is the surface's primary purpose"; `size="sm"` =
862
+ "the action is one of many chrome affordances in a dense band":
863
+
864
+ | Surface | `size=` | Why |
865
+ |---|---|---|
866
+ | admin-topbar / chat-header / editor-toolbar | `"sm"` | Chrome density; many buttons in a narrow band |
867
+ | card-ui header action cluster | `"sm"` | Matches kebab-menu / icon-button neighbor density |
868
+ | table-toolbar-ui actions | `"sm"` | Same chrome-band density |
869
+ | drawer-ui **header** close button | `"sm"` | Icon-only corner-tucked affordance |
870
+ | **drawer-ui footer** commit/cancel/delete | **default** | Terminal-action; needs visual weight on the decision moment |
871
+ | modal-ui footer commit/cancel | default | Same logic as drawer footer |
872
+ | admin-page-header action cluster | default | Page-level CTAs feel primary, not chrome |
873
+ | inline-form commit/cancel | default | Same |
874
+
875
+ When in doubt: `size="sm"` for chrome-band buttons (topbar, card-header,
876
+ toolbar); default size for content-band terminal-action buttons (drawer
877
+ footer, modal footer, page-header CTAs, form commits).
878
+
879
+ **drawer-ui vs modal-ui decision:**
880
+ - `drawer-ui` — detail panels, contextual editing, side-by-side review
881
+ - `modal-ui` — confirmation dialogs, destructive actions, focused forms
882
+
883
+ ## Entity status → badge variant mapping
884
+
885
+ | Domain status | `badge-ui` variant |
886
+ |---|---|
887
+ | Open / Active / New | `info` |
888
+ | In Review / Processing | `warning` |
889
+ | Approved / Resolved / Complete | `success` |
890
+ | Rejected / Failed / Overdue | `danger` |
891
+ | Closed / Archived / Inactive | `neutral` |
892
+
893
+ ## Card-ui header — slotted children, NOT `<div>` wrapper
894
+
895
+ `<card-ui>` exposes a `<header>` slot that uses CSS Grid activated by
896
+ `:has(> [slot])`. h1-h6 elements auto-place into the heading column
897
+ without an explicit slot; elements with `slot="action"` go into the
898
+ trailing auto-width action column. The `1fr auto` grid only activates
899
+ when at least one **direct child** of `<header>` carries a `slot=`
900
+ attribute.
901
+
902
+ > ⚠️ **Anti-pattern: wrapping heading + action in a `<div>` bypasses
903
+ > the grid entirely.** The grid activator (`:has(> [slot])`) doesn't
904
+ > match because the `<header>`'s direct child is the `<div>`, which
905
+ > has no `slot=`. Both elements fall into the unslotted full-width
906
+ > fallback row — the action badge appears below the title instead of
907
+ > right-aligned.
908
+
909
+ **Wrong** (action appears below title — grid never activates):
910
+
911
+ ```html
912
+ <card-ui>
913
+ <header>
914
+ <div> <!-- ✗ bare div bypasses grid -->
915
+ <h3>Title</h3>
916
+ <tag-ui>Badge</tag-ui>
917
+ </div>
918
+ </header>
919
+ </card-ui>
920
+ ```
921
+
922
+ **Correct** (h3 auto-placed in 1fr heading column, tag-ui in trailing auto column):
923
+
924
+ ```html
925
+ <card-ui>
926
+ <header>
927
+ <h3>Title</h3> <!-- auto-placed: 1fr heading column -->
928
+ <tag-ui slot="action" size="sm">Badge</tag-ui> <!-- slot=action: trailing auto column -->
929
+ </header>
930
+ </card-ui>
931
+ ```
932
+
933
+ **Available header slots**:
934
+
935
+ | Slot | Position | Notes |
936
+ |------|----------|-------|
937
+ | `icon` (leading) | left of heading | optional |
938
+ | `heading` (1fr) | main column | `<h1>`–`<h6>` auto-place without explicit `slot=` |
939
+ | `description` (row 2) | spans full width | for sub-text below heading |
940
+ | `action` (trailing auto) | right edge | badges, buttons, kebab menus |
941
+
942
+ **CSS mechanism** (from `card.css`, for reference):
943
+ - `header:has(> [slot]) { display: grid; gap: … }` — grid only activates with slotted child
944
+ - `header :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 1; grid-row: 1 }`
945
+ - `header:not(:has(> [slot="icon"])):has(> [slot="action"]) { grid-template-columns: 1fr auto }`
946
+
947
+ **Detection** (manual): visually, the badge appears below or beside-the-wrong-edge of the title. No console error — it's a silent layout regression.
948
+
949
+ **Source**: 2026-05-23 docs-overhaul session generated this pattern repeatedly; the failure required visual debugging to identify. Documented to prevent recurrence in agent-generated card markup.
950
+
951
+ ### Card-ui header — inline title + controls without `slot=action`
952
+
953
+ The CSS grid only activates when at least one direct child carries `slot=`. Two unslotted siblings (e.g., `<text-ui>` + `<button-ui>`) fall into the unslotted fallback row, which is `display: block` — they stack vertically instead of sharing one row.
954
+
955
+ **Wrong** (controls stack BELOW the title):
956
+
957
+ ```html
958
+ <card-ui>
959
+ <header>
960
+ <text-ui weight="600">Recent activity</text-ui> <!-- no slot — fallback row -->
961
+ <button-ui size="sm" text="View all"></button-ui> <!-- no slot — next fallback row -->
962
+ </header>
963
+ </card-ui>
964
+ ```
965
+
966
+ **Right A** (slot-grid path — canonical for hand-authored HTML):
967
+
968
+ ```html
969
+ <card-ui>
970
+ <header>
971
+ <h3>Recent activity</h3> <!-- auto-placed: 1fr -->
972
+ <button-ui slot="action" size="sm" text="View all"></button-ui> <!-- trailing -->
973
+ </header>
974
+ </card-ui>
975
+ ```
976
+
977
+ **Right B** (`<row-ui>` fallback — for contexts that strip `slot=`):
978
+
979
+ ```html
980
+ <card-ui>
981
+ <header>
982
+ <row-ui justify="space-between" align="center">
983
+ <h3>Recent activity</h3>
984
+ <button-ui size="sm" text="View all"></button-ui>
985
+ </row-ui>
986
+ </header>
987
+ </card-ui>
988
+ ```
989
+
990
+ **When to use each**:
991
+ - Right A is preferred — minimal markup, uses the primitive's intended grammar
992
+ - Right B is required when authoring corpus HTML for the A2UI gen-UI pipeline. The transpiler strips `slot=` attributes from many native-element children (`<span slot="action">`, `<text-ui slot="description">`). The `<row-ui>` form survives transpilation intact.
993
+
994
+ ## Card-ui vs `<card-ui raw>` — surface-vs-chrome decision
995
+
996
+ `<card-ui>` defaults to a **chromed surface** (background + border + shadow). `<card-ui raw>` strips that chrome — content structure (`<header>`/`<section>`/`<footer>` grammar) still applies, but the parent owns the visible surface.
997
+
998
+ | Use | When |
999
+ |---|---|
1000
+ | `<card-ui>` (default) | Standalone content surface on a page background — dashboards, settings cards, summary tiles, list containers, form containers. The card IS the visible boundary. |
1001
+ | `<card-ui raw>` | Layout wrapper where the parent already owns the surface — auth screens centred on the viewport, embedded inside another chromed container, transparent column collectors. The card carries the slot grammar but not the chrome. |
1002
+
1003
+ **Two recurring anti-patterns**:
1004
+
1005
+ 1. **Defaulting to `raw` when a chromed surface is intended.** Card content (icon + heading + body + footer) floats against the page background with no visible boundary. Looks "minimal" but is the symptom of "missed default" — the agent reached for `raw` to skip thinking about surface.
1006
+
1007
+ 2. **Non-raw card without an explicit `<header>`.** The card has chrome but no title — the first visible content element (a `<field-ui label="…">`, a `<list-item-ui>`'s primary text, the first paragraph) becomes the de-facto heading visually. Reads as "two-title card" when label text and content text both compete for top-of-card primacy. Always include `<header>` with an explicit title on non-raw cards.
1008
+
1009
+ ```html
1010
+ <!-- WRONG — chrome present, no header → field-ui label reads as "Section heading" -->
1011
+ <card-ui>
1012
+ <section>
1013
+ <field-ui label="Email addresses">…</field-ui>
1014
+ </section>
1015
+ </card-ui>
1016
+
1017
+ <!-- RIGHT — explicit header owns the title; field-ui label is field-scoped -->
1018
+ <card-ui>
1019
+ <header><h3>Invite teammates</h3></header>
1020
+ <section>
1021
+ <field-ui label="Email addresses">…</field-ui>
1022
+ </section>
1023
+ </card-ui>
1024
+ ```
1025
+
1026
+ **Source**: 2026-05-26 gen-review cycle 17 surfaced this across 8 demos (invite-members, contact-form, billing-overview, role-management, status-dashboard, audit-log, integrations-grid, webhook-config). Non-raw card without `<header>` was indistinguishable from a `<card-ui raw>` field group at first glance.
1027
+
1028
+ ## Design board → AdiaUI token mapping
1029
+
1030
+ When the user provides a Figma board or screenshot:
1031
+
1032
+ 1. **Extract intent, not values.** Read the board for visual hierarchy,
1033
+ component patterns, and UX intent — not pixel measurements.
1034
+ 2. **Map design components to AdiaUI primitives.** A Figma "Status Chip"
1035
+ → `<badge-ui>`. A Figma "Data Row" → `<table-ui>` row. A Figma
1036
+ "Side Panel" → `<drawer-ui>`.
1037
+ 3. **Map colors to semantic tokens** — never copy hex values:
1038
+ - Primary brand color → `var(--a-primary-bg)` / `var(--a-primary-fg)`
1039
+ - Danger/error red → `var(--a-danger-strong)`
1040
+ - Neutral gray → `var(--a-chrome-*)`
1041
+ - Surface background → `var(--a-bg)` / `var(--a-bg-muted)` / `var(--a-bg-strong)`
1042
+ 4. **Flag mismatches.** If the board shows a component AdiaUI doesn't have
1043
+ (e.g. a custom Gantt chart, a specialized calendar heatmap), flag it
1044
+ explicitly in the GenUI Plan `deviations:` field — do not silently
1045
+ approximate it with the wrong primitive.
1046
+ 5. **Do not override tokens.** No inline `style="color: #3b82f6"`.
1047
+ If the design calls for a color not in the token system, that is a
1048
+ design system gap to surface to the user, not a one-off override.
1049
+
1050
+ ## Loading state (v0.6.18 — `stat-ui` + `table-ui`)
1051
+
1052
+ Pre-v0.6.18, consumers hand-rolled skeleton CSS to bridge the gap between
1053
+ mount and first data render. v0.6.18 adds `loading` as a first-class
1054
+ prop on the two highest-leverage data-display primitives:
1055
+
1056
+ ```html
1057
+ <!-- KPI cards: loading skeleton until value/change arrive -->
1058
+ <stat-ui label="Total Revenue" loading></stat-ui>
1059
+
1060
+ <!-- When data arrives, flip loading off and set the value+change -->
1061
+ <script>
1062
+ const card = document.querySelector('stat-ui');
1063
+ fetchKPI().then(({value, change, trend}) => {
1064
+ card.removeAttribute('loading');
1065
+ card.setAttribute('value', value);
1066
+ card.setAttribute('change', change);
1067
+ card.setAttribute('trend', trend);
1068
+ });
1069
+ </script>
1070
+ ```
1071
+
1072
+ ```html
1073
+ <!-- Tables: skeleton rows preserve header + columns -->
1074
+ <table-ui id="recent" paginate="10" loading></table-ui>
1075
+
1076
+ <script>
1077
+ const t = document.getElementById('recent');
1078
+ t.columns = [{key:'name',label:'Name'}, /* … */];
1079
+ fetchRows().then(rows => {
1080
+ t.data = rows;
1081
+ t.removeAttribute('loading');
1082
+ });
1083
+ </script>
1084
+ ```
1085
+
1086
+ **What v0.6.18 ships:**
1087
+
1088
+ - `<stat-ui loading>` — renders `<skeleton-ui>` shimmer in the value + change
1089
+ slots. Label + icon are preserved (static metadata). Host gets `aria-busy="true"`.
1090
+ - `<table-ui loading>` — renders N ghost skeleton rows (N = `paginate` capped at 8,
1091
+ else 5). Header + columns stay intact so layout is preserved. Host gets
1092
+ `aria-busy="true"`. Pre-v0.6.18 the impl was a `<progress-ui>` spinner overlay
1093
+ even though the yaml description always said "skeleton rows" — that drift is
1094
+ now fixed.
1095
+
1096
+ **`chart-ui` does NOT have a `loading` prop** — by design. `chart-ui` is a
1097
+ renderer over already-shaped data; loading-state belongs in the container
1098
+ wrapping it. Canonical pattern:
1099
+
1100
+ ```html
1101
+ <card-ui>
1102
+ <section>
1103
+ <!-- Wrap chart-ui with a sibling skeleton-ui while data fetches -->
1104
+ <skeleton-ui height="180px" width="100%" id="chart-skeleton"></skeleton-ui>
1105
+ <chart-ui hidden id="my-chart"></chart-ui>
1106
+ </section>
1107
+ </card-ui>
1108
+ ```
1109
+
1110
+ This split (own-state for data-prop primitives, compose-state for renderers)
1111
+ is intentional. See `components/chart/chart.examples.html` for the canonical
1112
+ demo, and `outbox/FEEDBACK-12--...--followup.md` for the design discussion.
1113
+
1114
+ ## Button `text=` + `icon=` symbol doubling (FEEDBACK-15 trap)
1115
+
1116
+ `<button-ui>` renders `icon=` as a prepended `<icon-ui>` child AND
1117
+ renders `text=` as the visible label. When `text=` begins with the
1118
+ same symbol the icon already provides — `+` / `-` / `×` / `✓` / `→` /
1119
+ `←` — the glyph renders **twice**. Substrate console-warns via
1120
+ `ICON_TEXT_PREFIXES` in `node_modules/@adia-ai/web-components/components/button/class.js:23-30`,
1121
+ but the warn is per-element WeakSet-guarded and easy to miss in a
1122
+ noisy dev console.
1123
+
1124
+ ```html
1125
+ <!-- Right -->
1126
+ <button-ui text="New Claim" icon="plus" variant="primary"></button-ui>
1127
+
1128
+ <!-- Wrong — doubled "+"; substrate console-warns (FEEDBACK-15) -->
1129
+ <button-ui text="+ New Claim" icon="plus" variant="primary"></button-ui>
1130
+ ```
1131
+
1132
+ **Discipline: `icon=` provides the glyph; `text=` is the semantic
1133
+ label only.** Strip the symbol from `text=` whenever the icon
1134
+ provides it. Covered collision pairs (substrate `ICON_TEXT_PREFIXES`):
1135
+
1136
+ | `icon=` | symbols stripped from `text=` |
1137
+ |---|---|
1138
+ | `plus` | `+`, `+` |
1139
+ | `minus` | `-`, `−`, `–` |
1140
+ | `x` | `×`, `x `, `X ` |
1141
+ | `check` | `✓`, `✔` |
1142
+ | `arrow-right` | `→`, `>` |
1143
+ | `arrow-left` | `←`, `<` |
1144
+
1145
+ The list is high-frequency, not exhaustive. If you find a doubled
1146
+ glyph that isn't covered (e.g. `dots-three` + `…`), the discipline
1147
+ still applies — substrate just won't warn yet.
1148
+
1149
+ ## Imperative toasts via `feed-ui` (FB-11 P2)
1150
+
1151
+ `<feed-ui position="bottom-right">` is the toast container. To post a toast
1152
+ imperatively, either:
1153
+
1154
+ **Option A — global CustomEvent (works everywhere, no import needed):**
1155
+
1156
+ ```js
1157
+ window.dispatchEvent(new CustomEvent('feed', {
1158
+ detail: { kind: 'success', title: 'Saved', message: 'Changes synced.' }
1159
+ }));
1160
+ ```
1161
+
1162
+ This is the canonical, lowest-friction API. `feed.js` (loaded via the
1163
+ barrel import `@adia-ai/web-components`) installs a global listener on
1164
+ `window` that routes `'feed'` CustomEvents to `UIFeed.post()`. No
1165
+ import needed — just dispatch the event from any module / framework /
1166
+ inline script.
1167
+
1168
+ **Option B — explicit class import:**
1169
+
1170
+ ```ts
1171
+ import { UIFeed } from '@adia-ai/web-components/components/feed/class';
1172
+ UIFeed.post({ kind: 'success', title: 'Saved', message: 'Changes synced.' });
1173
+ ```
1174
+
1175
+ The canonical import path is the `/class` shorthand (resolves via the
1176
+ `./components/*/class` exports pattern). Do NOT try
1177
+ `@adia-ai/web-components/components/feed/feed.js` — Node's exports
1178
+ wildcards can't substitute across multiple path segments, so it resolves
1179
+ to a phantom nested path and fails. (Verified end-to-end in FB-11.)
1180
+
1181
+ **Post-options shape (same for both APIs):**
1182
+
1183
+ ```ts
1184
+ {
1185
+ kind: 'success' | 'info' | 'warning' | 'danger',
1186
+ title: string,
1187
+ message: string,
1188
+ timeout: number, // ms, 0 = sticky
1189
+ }
1190
+ ```
1191
+
1192
+ ## Editor inspector pane (right-side property form inside `<editor-sidebar slot="trailing">`)
1193
+
1194
+ The canonical right-pane idiom inside `<editor-shell>` is a property
1195
+ inspector. It's not a separate component — it's a composition of
1196
+ primitives the editor cluster expects in the trailing sidebar.
1197
+
1198
+ ```html
1199
+ <editor-sidebar slot="trailing" collapsible persist="inspector">
1200
+ <pane-ui side="trailing" resizable min-width="200">
1201
+ <header>
1202
+ <span>Inspector</span>
1203
+ <span slot="action">
1204
+ <button-ui id="wrap-btn" icon="brackets-square" variant="ghost" size="sm" title="Wrap selection"></button-ui>
1205
+ <button-ui id="duplicate-btn" icon="copy" variant="ghost" size="sm" title="Duplicate"></button-ui>
1206
+ </span>
1207
+ </header>
1208
+ <section>
1209
+ <fields-ui>
1210
+ <field-ui label="Name">
1211
+ <input-ui name="name" value="..."></input-ui>
1212
+ </field-ui>
1213
+ <field-ui label="Variant">
1214
+ <select-ui name="variant" data-options='[{"value":"primary","label":"Primary"}]'></select-ui>
1215
+ </field-ui>
1216
+ <field-ui label="Size">
1217
+ <segmented-ui>
1218
+ <segment-ui value="sm" text="Sm"></segment-ui>
1219
+ <segment-ui value="md" text="Md"></segment-ui>
1220
+ <segment-ui value="lg" text="Lg"></segment-ui>
1221
+ </segmented-ui>
1222
+ </field-ui>
1223
+ </fields-ui>
1224
+ </section>
1225
+ </pane-ui>
1226
+ </editor-sidebar>
1227
+ ```
1228
+
1229
+ **Why this shape:**
1230
+ - `<pane-ui side="trailing" resizable>` carries the inner-edge resize seam (per `pane.yaml` a2ui.rules — `[side]` flips grabber + chrome to the inner edge).
1231
+ - `<header>` slot is what `<pane-ui>` projects as the pane title; `[slot="action"]` button cluster pushes to the trailing edge.
1232
+ - `<section>` carries the form rows. Use `<fields-ui>` + `<field-ui label="…">` for the typed-row scaffolding — `<field-ui>` is the canonical label+control wrapper, not a raw `<label>`.
1233
+ - Persistence via `persist="inspector"` puts pane width under `adia-editor-sidebar-inspector` localStorage key (cluster-distinct from admin and chat sidebars).
1234
+
1235
+ **Pitfalls:**
1236
+ - Don't substitute raw `<div>` for `<fields-ui>` — the typography + spacing tokens hang on the bespoke wrapper. Reference: `apps/genui/a2ui-editor/` for the production canonical.
1237
+ - Don't put header `[slot="action"]` buttons *outside* the `<header>` element. The pane projects them via the header projection.
1238
+ - A non-resizable inspector (fixed-width detail rail) drops `resizable` from `<pane-ui>` but keeps `persist=` for future toggleability.
1239
+
1240
+ ## Editor sub-views inside `<editor-canvas>` (Preview / Schema / DOM toggle)
1241
+
1242
+ When an editor has multiple representations of the same document
1243
+ (preview pane vs schema text vs DOM text), the canonical shape is a
1244
+ `<tabs-ui>` inside `<editor-canvas-toolbar>` driving multiple
1245
+ `<div data-view="…">` panels inside the canvas body.
1246
+
1247
+ ```html
1248
+ <editor-canvas>
1249
+ <editor-canvas-toolbar>
1250
+ <tabs-ui id="view-tabs" value="preview" variant="underline">
1251
+ <tab-ui value="preview" text="Preview" icon="eye"></tab-ui>
1252
+ <tab-ui value="schema" text="Schema" icon="brackets-curly"></tab-ui>
1253
+ <tab-ui value="dom" text="DOM" icon="code"></tab-ui>
1254
+ </tabs-ui>
1255
+ </editor-canvas-toolbar>
1256
+
1257
+ <div data-view-body>
1258
+ <div data-view="preview">
1259
+ <!-- live rendered tree -->
1260
+ </div>
1261
+ <div data-view="schema" hidden>
1262
+ <code-ui id="schema-code" language="json" editable bare></code-ui>
1263
+ </div>
1264
+ <div data-view="dom" hidden>
1265
+ <!-- nested canvas-toolbar for HTML/CSS/JS sub-tabs -->
1266
+ <editor-canvas-toolbar>
1267
+ <tabs-ui id="dom-tabs" value="html" variant="underline">
1268
+ <tab-ui value="html" text="HTML"></tab-ui>
1269
+ <tab-ui value="css" text="CSS"></tab-ui>
1270
+ <tab-ui value="js" text="JS"></tab-ui>
1271
+ </tabs-ui>
1272
+ </editor-canvas-toolbar>
1273
+ <div data-dom-body>
1274
+ <div data-dom-view="html"><code-ui language="html" bare></code-ui></div>
1275
+ <div data-dom-view="css" hidden><code-ui language="css" bare></code-ui></div>
1276
+ <div data-dom-view="js" hidden><code-ui language="javascript" bare></code-ui></div>
1277
+ </div>
1278
+ </div>
1279
+ </div>
1280
+ </editor-canvas>
1281
+ ```
1282
+
1283
+ **Why this shape:**
1284
+ - `<editor-canvas-toolbar>` is canvas-scope chrome (sticky at the top of the canvas body, per the yaml SoT). Distinct from the app-scope `<editor-toolbar>` at the top of `<editor-shell>`.
1285
+ - `<tabs-ui>` is the in-page section switcher (per the nav-decision rule — "switching views within the same logical page" not "navigating away").
1286
+ - The wiring is dead-simple: a `[value]` listener on `<tabs-ui>` flips the `[hidden]` attribute on the matching `<div data-view="…">`. The shell doesn't need to know about the sub-views.
1287
+ - Nested toolbars work: a sub-view can contain its own `<editor-canvas-toolbar>` for sub-sub-tabs. The DOM nesting is enough — the CSS doesn't constrain depth.
1288
+
1289
+ **Pitfalls:**
1290
+ - Don't use `<editor-toolbar>` for view-mode tabs. `<editor-toolbar>` is app-scope (top of the shell, document-wide actions); `<editor-canvas-toolbar>` is canvas-scope (top of the canvas, view-mode affordances). The yaml SoT spells this out in `editor-canvas-toolbar.yaml`'s `a2ui.rules`.
1291
+ - Don't toggle `[hidden]` on `<tab-ui>` — toggle it on the `<div data-view="…">` panels. The tabs-ui keeps its own selected-state via `[value]`.
1292
+ - Programmatic writes to a focused `<code-ui editable bare>` are no-ops by design (render-guard prevents clobbering in-flight typing). If you `.text = X` while user is typing in Schema view, the write silently won't apply. Intentional — don't "fix" it.
1293
+
1294
+ Production canonical: `apps/genui/a2ui-editor/` uses this exact shape with three top-level views + nested DOM sub-tabs.
1295
+
1296
+ ---
1297
+
1298
+ ## Meta / Introspection surfaces
1299
+
1300
+ **Pattern class:** Internal tooling, review galleries, audit dashboards, dogfood harnesses — any page whose purpose is *inspecting the system itself* rather than *using it*.
1301
+
1302
+ **Structural rules that consumer patterns don't cover:**
1303
+
1304
+ ### 1. No nested card chrome
1305
+
1306
+ The most common mistake: wrapping a prompt block in a card, then wrapping the output inside another card. Every level of card chrome implies "this is a standalone surfaced thing" — but in an inspection surface, the hierarchy is already established by document headings.
1307
+
1308
+ ❌ Wrong:
1309
+ ```html
1310
+ <card-ui> <!-- prompt "card" -->
1311
+ <header>...</header>
1312
+ <section>
1313
+ <card-ui> <!-- output card inside -->
1314
+ <header>...</header>
1315
+ <section><canvas-ui></canvas-ui></section>
1316
+ </card-ui>
1317
+ </section>
1318
+ </card-ui>
1319
+ ```
1320
+
1321
+ ✅ Right — flat document rhythm:
1322
+ ```html
1323
+ <section class="prompt-row"> <!-- role: document section, not card -->
1324
+ <h3>Login Form</h3>
1325
+ <code class="prompt-text">"login form with email and password"</code>
1326
+ <!-- flat tab bar, no wrapper chrome -->
1327
+ <div role="tablist">...</div>
1328
+ <div class="output-area"> <!-- single inset, not a card -->
1329
+ <canvas-ui></canvas-ui>
1330
+ </div>
1331
+ </section>
1332
+ ```
1333
+
1334
+ ### 2. Headings as section breaks, not card headers
1335
+
1336
+ Use `h2` / `h3` for group and prompt labels. A card header is for interactive chrome that needs the card's slot vocabulary (icon, action, description). A static label needs none of that — it's just a heading.
1337
+
1338
+ ```html
1339
+ <!-- group -->
1340
+ <h2>Auth <badge-ui text="6" variant="muted" size="sm"></badge-ui></h2>
1341
+
1342
+ <!-- individual item -->
1343
+ <h3>Login Form</h3>
1344
+ ```
1345
+
1346
+ ### 3. Code for literal values
1347
+
1348
+ Prompt strings, output IDs, version tags, selectors — any value the user will read verbatim or copy:
1349
+
1350
+ ```html
1351
+ <!-- prompt text -->
1352
+ <code class="prompt-text">"login form with email and password"</code>
1353
+
1354
+ <!-- copyable ID chip -->
1355
+ <code class="output-id">auth-login-form-Z-v2</code>
1356
+ ```
1357
+
1358
+ Font: `var(--a-font-family-code, ui-monospace, monospace)`. Not `--a-font-family` (proportional).
1359
+
1360
+ ### 4. Tab bars flush to content, no padding wrapper
1361
+
1362
+ Engine or view-mode tabs sit directly above their output, separated only by a 1px border. They don't need a padded container.
1363
+
1364
+ ```css
1365
+ .gallery-tabs {
1366
+ display: flex;
1367
+ border-bottom: 1px solid var(--a-border-subtle);
1368
+ /* No padding, no background card, no border-radius */
1369
+ }
1370
+ .gallery-tab[aria-selected="true"] {
1371
+ border-bottom: 2px solid var(--a-accent-strong);
1372
+ color: var(--a-accent-strong);
1373
+ }
1374
+ ```
1375
+
1376
+ ### 5. Inset-not-card for output areas
1377
+
1378
+ The rendered output area gets visual containment via `background: var(--a-bg-subtle)` + `border: 1px solid var(--a-border-subtle)` + `border-radius`. That's enough — no `<card-ui>` wrapper needed.
1379
+
1380
+ ```css
1381
+ .output-area {
1382
+ background: var(--a-bg-subtle);
1383
+ border: 1px solid var(--a-border-subtle);
1384
+ border-radius: var(--a-radius-md);
1385
+ padding: var(--a-space-4);
1386
+ min-height: 200px;
1387
+ max-height: 600px;
1388
+ overflow-y: auto;
1389
+ }
1390
+ ```
1391
+
1392
+ ### 6. Two-column inspection layout
1393
+
1394
+ Left: sticky anchor-link rail to jump between groups (not a `<toc-ui>` — that auto-scans headings; here we want explicit curated links).
1395
+ Right: scrollable content with generous padding.
1396
+
1397
+ ```html
1398
+ <div style="display:grid;grid-template-columns:220px 1fr;min-height:100dvh;">
1399
+ <nav style="position:sticky;top:0;height:100dvh;overflow-y:auto;border-right:1px solid var(--a-border-subtle);padding:var(--a-space-4);">
1400
+ <!-- explicit group links, not toc-ui -->
1401
+ <a href="#group-auth" class="rail-link">Auth <small>(6)</small></a>
1402
+ ...
1403
+ </nav>
1404
+ <main style="padding:var(--a-space-6);">
1405
+ <!-- groups -->
1406
+ </main>
1407
+ </div>
1408
+ ```
1409
+
1410
+ **When to use this pattern:**
1411
+ - Gen UI output gallery
1412
+ - Eval result browsers
1413
+ - Audit log / drift report pages
1414
+ - Any internal tooling surface where the content IS the system's output
1415
+
1416
+ **What separates this from `patterns-shells.md`:** Shell patterns are for product pages users navigate to complete tasks. Meta surfaces are for operators inspecting or debugging the system — document-like, dense, information-first, no decorative chrome.