@adia-ai/adia-ui-forge 0.8.50 → 0.8.52

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 (31) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +30 -0
  4. package/README.md +23 -1
  5. package/__init__.py +63 -0
  6. package/agents/a2ui-maintenance-agent.md +6 -4
  7. package/agents/a2ui-planner-agent.md +5 -5
  8. package/agents/demo-audit-agent.md +4 -4
  9. package/agents/framework-checker.md +5 -5
  10. package/agents/framework-planner-agent.md +7 -6
  11. package/agents/gen-ui-review-agent.md +4 -4
  12. package/agents/package-release-agent.md +4 -2
  13. package/agents/primitive-authoring-agent.md +1 -0
  14. package/package.json +10 -2
  15. package/plugin.yaml +4 -0
  16. package/prompts/demo-audit.md +10 -0
  17. package/prompts/gen-ui-review.md +9 -0
  18. package/prompts/package-release.md +12 -0
  19. package/prompts/site-deployment.md +9 -0
  20. package/skills/cross-harness-authoring-standards/SKILL.md +120 -0
  21. package/skills/cross-harness-authoring-standards/agents/openai.yaml +3 -0
  22. package/skills/cross-harness-compatibility-standards/SKILL.md +219 -0
  23. package/skills/cross-harness-compatibility-standards/agents/openai.yaml +3 -0
  24. package/skills/package-release/references/cut-procedure.md +7 -0
  25. package/skills/package-release/scripts/gate-roster.mjs +5 -0
  26. package/skills/package-release/scripts/release-pack.mjs +244 -42
  27. package/skills/primitive-authoring/references/INDEX.md +1 -1
  28. package/skills/primitive-authoring/references/api-contract.md +35 -0
  29. package/skills/primitive-authoring/references/css-patterns.md +79 -0
  30. package/skills/primitive-authoring/references/lifecycle-patterns.md +17 -0
  31. package/skills/primitive-authoring/references/module-promotion.md +2 -0
@@ -143,6 +143,41 @@ pixel-diff/visual-eval gate, not unit tests or review alone. Source:
143
143
  [ADR-0081](../../../../../../docs/ops/adr/adr-0081-chart-2-0-foundations-attribute-grammar.md)
144
144
  Amendment (2026-08-21).
145
145
 
146
+ ## Canonical breakpoint scale (adr-0089)
147
+
148
+ [verified 2026-08-25, gh#1984] `core/responsive.js`'s `BREAKPOINTS` export is
149
+ the single canonical reference for any device-class threshold, whether the
150
+ query mechanism is `@media` (the five sanctioned viewport primitives —
151
+ `grid-ui`, `col-ui`, `row-ui`, `block-ui`, `text-ui` — plus top-layer/popover
152
+ positioning with no ancestor box to query) or `@container` (everything else,
153
+ per `adr-0088` — `spec-breakpoint-convention` REQ-004 forbids importing
154
+ `core/responsive.js` itself outside those cases). This table MUST match
155
+ `core/responsive.js`'s `BREAKPOINTS` export byte-for-byte — a gap here was
156
+ exactly the drift `spec-breakpoint-convention` REQ-007 named and closed:
157
+
158
+ | Name | Min-width |
159
+ | --- | --- |
160
+ | `xs` | `0` |
161
+ | `sm` | `480px` |
162
+ | `md` | `768px` |
163
+ | `lg` | `1024px` |
164
+ | `xl` | `1280px` |
165
+
166
+ A `@container`/`@media` rule can't read a CSS custom property at parse time
167
+ (forbidden-pattern #2 in [token-contract.md](token-contract.md)), so these
168
+ values are cited as raw literals in component CSS, never tokenized — but
169
+ every such literal MUST carry a preceding comment naming the rung it
170
+ implements (device-class threshold) or the ergonomic reason for the number
171
+ (a component-intrinsic threshold, exempt from this table entirely — `nav-ui`'s
172
+ 96px icon-rail floor, `table-toolbar-ui`'s compaction stages, `chart-ui`'s
173
+ 200px legend-hide are correctly exempt and are NOT migration candidates).
174
+ `packages/web-components/core/breakpoint-observer.js` is the shared
175
+ `ResizeObserver`-to-attribute helper for a component that needs
176
+ JS-observable state (relocating a node, switching a positioning strategy)
177
+ rather than a pure CSS layout switch — see its own module doc for the
178
+ boolean-vs-named-value reflection modes. Full requirements:
179
+ [spec-breakpoint-convention](../../../../../../docs/ops/spec/spec-breakpoint-convention.md).
180
+
146
181
  ## Numeric props — `null` over sentinels
147
182
 
148
183
  Indeterminate, unknown, or "not yet set" numeric state uses `null`, not `-1` or `Infinity`:
@@ -99,6 +99,8 @@ Does your [attribute=value] need to change any of:
99
99
 
100
100
  This list is the single source of truth. If your mode isn't here, add it. If adding would feel weird, that's a signal the "mode" is actually a **sibling component** — prefer `code-inline-ui` over `code-ui[inline]` unless the attribute genuinely toggles one surface between two states of the same thing.
101
101
 
102
+ **Orthogonal boolean combinations need every combination defined, not left as an undefined hybrid.** Two independently-toggleable boolean attributes on the same component (e.g. stat-ui's `band` and `bleed`) form an N² space; shipping three of the four combinations and leaving the fourth undefined means a consumer who reaches it gets whatever the cascade happens to produce, not a designed layout. ADR-0083 gave stat-ui's `band bleed` combination a real contract — the band bleeds inline-start/inline-end/block-end via the `--card-inset` negative-extent technique, with `[slot="change"]` overlaid as a z-ordered, `pointer-events: none` chip anchored block-start/inline-end. When you add a second orthogonal boolean to an existing single-boolean variant, audit all four quadrants before shipping — an unstyled hybrid is a defect, not a follow-up.
103
+
102
104
  ## Font-family floor — text-bearing primitives must anchor to a token
103
105
 
104
106
  A primitive that renders text must NOT rely on `font: inherit` / `font-family: inherit` alone. Those carry **no default** — the primitive inherits whatever the host page sets, so a consumer page with a broken or serif `font-family` (a dead token, a missing `--a-font-family`, a serif host document) makes the primitive's labels render in UA serif while token-anchored siblings (`text-ui`) stay correct. A confusing same-page split — the exact bug behind an embedded-app `<segmented-ui>` serif regression (25 primitives shared the flaw).
@@ -218,6 +220,83 @@ Example carve-out:
218
220
 
219
221
  A cursory check might flag `--col-*` as "wrong" because the file is named `layout.css`. It's not wrong — the **scope tag** determines the stem, not the filename.
220
222
 
223
+ **Cross-component token-fallback aliasing.** A new component's tokens can alias a sibling component's existing token ladder as their `var()` fallback instead of minting an independent scale — when the two components share a visual role closely enough that re-theming one should re-theme the other in the same stroke. ADR-0083's stat-ui `[slot="change"]` chip mints six stat-scoped tokens (`--stat-change-*`) that fall back to chart.css's existing `--chart-chip-*` rungs:
224
+
225
+ ```css
226
+ :where(:scope) {
227
+ --stat-change-bg: var(--chart-chip-bg);
228
+ --stat-change-fg: var(--chart-chip-fg);
229
+ }
230
+ ```
231
+
232
+ A consumer who re-themes chart chips re-themes the stat delta chip too, with no separate override required. Reach for this only when the aliasing component is a genuine visual sibling of the aliased one (same role, same page context) — otherwise it's spooky action at a distance when the aliased component's tokens change for an unrelated reason.
233
+
234
+ ## Concentric-corner radius — deriving an item's radius from its container
235
+
236
+ When a rounded container pads a rounded-corner item flush against its own edge — a popover listbox around `[role="option"]` rows, a menu popover around `menu-item-ui` — a flat item-radius token only reads as concentric with the container's own corner at the one padding value it happened to be tuned against. Change the density scale or the radius scale independently (a consumer re-theme, a `--a-density` step) and the two arcs drift apart.
237
+
238
+ **The formula.** Solve for the ITEM's radius; keep the container's padding fixed:
239
+
240
+ ```
241
+ Ir = max(0, Cr − Cp)
242
+ ```
243
+
244
+ - `Cr` — the container's own `border-radius` (an existing fixed radius-scale token, untouched).
245
+ - `Cp` — the container's own padding/inset (an existing fixed spacing-scale token, untouched — this pattern never derives padding).
246
+ - `Ir` — the item's `border-radius`, the only new derived value.
247
+
248
+ **Why solve for `Ir`, not `Cp`.** The tempting reverse direction — hold the item's radius fixed and derive the container's padding from it — needs a second term, `min(Ir, Ih/2)` (`Ih` = item height), to keep the derived padding from going negative once the item is short enough that its own radius would pill-clamp. That's the browser's own `border-radius` clamp (CSS Backgrounds §5.5 — a radius past half an element's shorter side reduces to a pill) reimplemented by hand inside the padding formula. Solving for `Ir` instead gets that clamp for free: `max(0px, calc(Cr - Cp))` alone is correct at every item height, because the browser applies its pill-clamp to the declared `Ir` automatically — no `min()` term needed anywhere in the CSS.
249
+
250
+ **Companion rule — the `min-height` floor is load-bearing, not optional.** The browser's pill-clamp cuts both ways: if the item is shorter than `2 × Ir`, the *effective* rendered radius clamps down even though the *declared* `Ir` is correct — the item silently stops reading as concentric, with no error and no visual-eval regression to catch it (a live-browser-only defect class, the same shape as this file's `display:contents` entry below). Pair every derived radius with a matching height floor:
251
+
252
+ ```css
253
+ min-height: calc(2 * <item-radius-var>);
254
+ ```
255
+
256
+ Ruled (Kim, 2026-08-24): enforce this floor rather than let it silently degrade — items get taller at large radius/density scales as the accepted tradeoff for exact concentricity, not a bug to route around.
257
+
258
+ **Worked example** (`select-ui`'s listbox — the reported surface, gh#1956):
259
+
260
+ ```css
261
+ /* Container — Cr and Cp declared as LOCAL custom properties on the
262
+ popover's own top-layer rule. A top-layer popover usually can't inherit
263
+ the host element's @scope'd component tokens (a different, often
264
+ detached DOM subtree once popover-open) — declaring them locally here
265
+ sidesteps that; a local declaration DOES inherit down to a genuine DOM
266
+ child. */
267
+ select-ui [slot="listbox"] {
268
+ --select-listbox-padding: var(--a-space-1); /* Cp — fixed */
269
+ --select-listbox-radius: var(--a-radius); /* Cr — fixed */
270
+ padding: var(--select-listbox-padding);
271
+ border-radius: var(--select-listbox-radius);
272
+ }
273
+
274
+ /* Item — Ir derived; Cr/Cp inherited from the listbox above because
275
+ [role="option"] is a genuine DOM child of it (an appended option row). */
276
+ select-ui [slot="listbox"] [role="option"] {
277
+ --select-option-radius:
278
+ max(0px, calc(var(--select-listbox-radius) - var(--select-listbox-padding)));
279
+ border-radius: var(--select-option-radius);
280
+ min-height: calc(2 * var(--select-option-radius));
281
+ }
282
+ ```
283
+
284
+ (`packages/web-components/components/select/select.css:330-343` and `:378-386`.)
285
+
286
+ Express `Ir` as a live `calc()`/`max()` referencing the radius/space custom properties — never a baked pixel value — so it holds across every `--a-radius-k` / `--a-density` scale change.
287
+
288
+ **Shipped in** (gh#1956, PR #1959 — the reference implementation for this pattern):
289
+
290
+ - `select.css:330-343,378-386` — `[slot="listbox"]` vs. `[role="option"]`.
291
+ - `combobox.css:209-221,254-264` — `[data-listbox]`.
292
+ - `tags-input.css:166-184,200-209` — `[data-suggestions]`.
293
+ - `menu.css:51-65,90-92` — `[data-menu-popover]` vs. `menu-item-ui`.
294
+ - `context-menu.css:26-36,56-58` — `[data-context-menu-surface]`, which shares `menu-item-ui` rows with `menu.css`.
295
+ - `nav-group.css:328-340,358-365` — `[slot="popover"]` vs. `[role="option"]`. Previously gave option rows the container's own flat radius — a design inconsistency next to every sibling popover's distinct smaller item radius. Deriving `Ir` resolves it with no special-casing: under this direction (unlike the reverse "solve for `Cp`" direction, which floors padding to a hard 0px here and was reverted) the row's radius just comes out smaller than the container's automatically.
296
+ - `calendar-picker.css:87` / `date-range-picker.css:267` — `calendar-grid-ui`'s day cells (gh#1966), a variant shape: the item primitive is a *shared substrate* consumed by two different popover containers with different `Cr`/`Cp`, not a single component owning both container and item, so `Ir` is derived once per consumer (not once in the shared `calendar-grid.css`) via a `--calendar-grid-day-radius` override cascaded down. `date-range-picker`'s popover padding is asymmetric (`--date-range-picker-px` ≠ `-py`) — a single circular radius can't be exactly concentric on both axes, so it derives from `min(px, py)`, the tighter constraint; harmless on the corner (bottom-left, with a preset rail present) that isn't actually flush. Also the first shipped case to pair the derived radius with the `min-height: calc(2 * Ir)` companion floor (`calendar-grid.css:199`), per the ruling above.
297
+
298
+ Audited, not applicable: `command-ui`, `drilldown-ui` — item list sits inside a padded region behind a header, not flush against the container's own rounded corner, or the host carries no radius at all.
299
+
221
300
  ## Slot styling without `::slotted()`
222
301
 
223
302
  AdiaUI is light-DOM. Slotted children are just children. Style them with attribute selectors:
@@ -150,6 +150,23 @@ disconnected() {
150
150
 
151
151
  **Lesson:** a class must declare `disconnected()` exactly once. If you need to add teardown, edit the existing method. Duplicate method names silently shadow — no error, no warning.
152
152
 
153
+ ## Reflection — non-finite Number-typed props don't stringify
154
+
155
+ `reflect()` in `core/element.js` writes a `Number`-typed reflected property's value back onto the DOM attribute by stringifying it. That's correct for finite numbers, but a non-numeric sentinel assigned to a `Number`-typed reflected prop (e.g. `range-total="?"` — table-ui's open/unproven-total marker for cursor/hasMore server paging) used to get silently coerced and re-stringified as the literal attribute `range-total="NaN"`, before any render ran (gh#1895, PR #1907, ADR-0082 amendment).
156
+
157
+ **Fix, framework-wide:** a non-finite `Number`-typed value is left unreflected instead of stringified — gated narrowly on `Number` type + `Number.isFinite()` failing, so no other reflected prop's behavior changes.
158
+
159
+ ```javascript
160
+ // core/element.js reflect() — the narrow gate
161
+ if (type === Number && !Number.isFinite(value)) {
162
+ // leave unreflected — do NOT write "NaN" to the attribute
163
+ } else {
164
+ this.setAttribute(attr, String(value));
165
+ }
166
+ ```
167
+
168
+ **Recognition:** any future non-numeric sentinel authored onto a `Number`-typed reflected prop (a `'?'`, an `'auto'`, an open-ended marker) hits the same wart unless it too fails `Number.isFinite()` before the stringify step. If a prop's contract legitimately mixes numbers with a sentinel string, declare it non-reflecting or widen its declared type — don't rely on the reflect() gate alone as documentation of the contract.
169
+
153
170
  ## `UIFormElement` — `super` discipline
154
171
 
155
172
  Form-participating components inherit `ElementInternals` wiring from `UIFormElement`. That wiring only works if `super.connected()` and `super.disconnected()` are called:
@@ -176,6 +176,8 @@ Either ship as a standalone lockstep cut (per the sibling **package-release** sk
176
176
  | **C. Inside a shell cluster** | Almost never — fails if the element is consumed by _other_ shells. Use only when the element is structurally shell-bound. |
177
177
  | **D. Promote to primitive** | Only if it composes < 3 other primitives. Most controls fail this test. |
178
178
 
179
+ **ADR prose drift — don't trust an ADR's "Today's work" narrative for live paths.** ADR-0012 is the source for the primitives-vs-modules-vs-themes decision above, but its own "Today's work"/outcome-notes prose has drifted out from under its ratified Decision: that prose describes `packages/web-components/patterns/` as the shell/chat/editor/runtime source-clustering directory, but present-day `patterns/` holds 45+ usage-example dirs instead — source moved on to `packages/web-modules/{chat,editor,runtime,shell}/` per the later ADR-0015. The same drift claims `@adia-ai/web-themes` as a sibling workspace package; it never shipped that way — themes live under `packages/web-modules/theme/` as a subpath instead. An ADR's ratified **Decision** stays append-only-safe; its **"Today's work" / outcome-notes prose is not** — later reorganizations can strand it, and a reader following the literal directory claims today would git-mv into the wrong (docs-example) directory. Before executing a cluster-placement call cited from an ADR, verify the directory it names still matches the live tree (see "Path-rebase awareness" below) rather than trusting the ADR's narrative section verbatim.
180
+
179
181
  ## Two rules from observed bugs
180
182
 
181
183
  ### OD-002 — Absorb the standalone-button-next-door