@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/README.md +23 -1
- package/__init__.py +63 -0
- package/agents/a2ui-maintenance-agent.md +6 -4
- package/agents/a2ui-planner-agent.md +5 -5
- package/agents/demo-audit-agent.md +4 -4
- package/agents/framework-checker.md +5 -5
- package/agents/framework-planner-agent.md +7 -6
- package/agents/gen-ui-review-agent.md +4 -4
- package/agents/package-release-agent.md +4 -2
- package/agents/primitive-authoring-agent.md +1 -0
- package/package.json +10 -2
- package/plugin.yaml +4 -0
- package/prompts/demo-audit.md +10 -0
- package/prompts/gen-ui-review.md +9 -0
- package/prompts/package-release.md +12 -0
- package/prompts/site-deployment.md +9 -0
- package/skills/cross-harness-authoring-standards/SKILL.md +120 -0
- package/skills/cross-harness-authoring-standards/agents/openai.yaml +3 -0
- package/skills/cross-harness-compatibility-standards/SKILL.md +219 -0
- package/skills/cross-harness-compatibility-standards/agents/openai.yaml +3 -0
- package/skills/package-release/references/cut-procedure.md +7 -0
- package/skills/package-release/scripts/gate-roster.mjs +5 -0
- package/skills/package-release/scripts/release-pack.mjs +244 -42
- package/skills/primitive-authoring/references/INDEX.md +1 -1
- package/skills/primitive-authoring/references/api-contract.md +35 -0
- package/skills/primitive-authoring/references/css-patterns.md +79 -0
- package/skills/primitive-authoring/references/lifecycle-patterns.md +17 -0
- 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
|