@adia-ai/adia-ui-forge 0.8.40 → 0.8.42

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 (35) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +22 -0
  3. package/package.json +1 -1
  4. package/scripts/release-pretag-docs-gate +1 -1
  5. package/skills/a2ui-maintenance/SKILL.md +43 -6
  6. package/skills/a2ui-maintenance/references/anti-patterns.md +1 -1
  7. package/skills/a2ui-maintenance/references/chunk-authoring.md +2 -2
  8. package/skills/a2ui-maintenance/references/eval-diagnostics.md +1 -1
  9. package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +2 -2
  10. package/skills/a2ui-maintenance/references/pipeline-overview.md +2 -2
  11. package/skills/a2ui-maintenance/references/surface-lifecycle.md +73 -0
  12. package/skills/demo-audit/references/admin-shell-anatomy.md +1 -1
  13. package/skills/demo-audit/scripts/analyze.mjs +1 -1
  14. package/skills/package-release/SKILL.md +1 -1
  15. package/skills/package-release/references/cut-procedure.md +14 -4
  16. package/skills/package-release/references/gates-catalog.md +7 -1
  17. package/skills/package-release/scripts/gate-roster.mjs +28 -1
  18. package/skills/package-release/scripts/release-pack.mjs +43 -4
  19. package/skills/primitive-authoring/SKILL.md +1 -0
  20. package/skills/primitive-authoring/references/INDEX.md +2 -1
  21. package/skills/primitive-authoring/references/anti-patterns.md +27 -0
  22. package/skills/primitive-authoring/references/api-contract.md +39 -0
  23. package/skills/primitive-authoring/references/composite-demo-protocol.md +1 -1
  24. package/skills/primitive-authoring/references/css-patterns.md +7 -1
  25. package/skills/primitive-authoring/references/shell-patterns.md +1 -1
  26. package/skills/primitive-authoring/references/svg-authoring.md +282 -0
  27. package/skills/primitive-authoring/references/token-contract.md +47 -2
  28. package/skills/primitive-authoring/references/yaml-contract.md +239 -6
  29. package/skills/site-deployment/references/deploy-playbooks.md +12 -4
  30. package/skills/ssr-compatibility/SKILL.md +10 -5
  31. package/skills/ssr-compatibility/references/consumer-workarounds.md +14 -6
  32. package/skills/ssr-compatibility/references/failure-shapes.md +30 -7
  33. package/skills/ssr-compatibility/references/guard-patterns.md +28 -0
  34. package/skills/ssr-compatibility/references/status-ledger.md +13 -0
  35. package/skills/ssr-compatibility/references/test-without-linkedom.md +29 -14
@@ -120,6 +120,33 @@ allowHalf: { type: Boolean, default: false, reflect: true, attr: 'allow-half' }
120
120
  <toggle-group-ui single> <!-- opt-in to single-select -->
121
121
  ```
122
122
 
123
+ ### AP-09 · Native `<input>` / `<select>` / `<textarea>` stamped inside a primitive
124
+
125
+ ```javascript
126
+ // WRONG — command.class.js before gh#1302 (a9d282461^); select.class.js's
127
+ // searchable branch and drilldown.class.js carried the same shape
128
+ this.innerHTML = `
129
+ <header>
130
+ <icon-ui name="magnifying-glass" slot="icon"></icon-ui>
131
+ <input type="text" placeholder="${this.placeholder}" slot="input" />
132
+ </header>…`;
133
+ ```
134
+
135
+ **What went wrong:** AdiaUI interactive primitives own their affordance — the host IS the interactive surface: `contenteditable="plaintext-only"` (or a custom non-editable surface), form participation via `UIFormElement` + `ElementInternals`, explicit ARIA on the host, a JS keyboard model per the WAI-ARIA APG, and `--a-*`-token chrome (ADR-0025, 2026-05-11). A native form control under the hood leaks browser chrome the token contract cannot uniformly restyle, imports per-browser keyboard quirks, and splits the value contract. Three primitives stamped a raw `<input>` anyway with no recorded exception — a search field is exactly the case the rule exists for (ADR-0055, gh#1302).
136
+
137
+ **Fix:** compose an in-house surface already in the tree — pick per component:
138
+
139
+ - the `combobox-ui` shape — `contenteditable="plaintext-only"` + `role="combobox"` + list filtering (`combobox.class.js:22-23`); now used by `command-ui`'s palette field (`command.class.js:108`) and `select-ui`'s searchable trigger (`select.class.js:442`);
140
+ - composition of `<input-ui>` — the `search-ui` precedent (`search.class.js:19,44`); now used by `drilldown-ui`'s filter field (`drilldown.class.js:184-201`).
141
+
142
+ Behavior parity is part of done: focus order, IME/paste, `inputmode`, and APG-combobox ARIA, verified by a browser probe on the surface — not by tests alone.
143
+
144
+ **The exception list is singular and closed:** `<input-ui type="password">` wraps a native `<input type="password">` (`input.class.js:116,213`) because `-webkit-text-security: disc` masking only paints on a real input — a mechanical justification, not an aesthetic one. Nothing else qualifies by convenience; a new exception is its own ADR, not a comment in the class. **Out of scope:** disclosure widgets (`<details>/<summary>`, e.g. `agent-trace.js:170`'s row expand) — the rule covers surfaces that gather user input, participate in `<form>`, or carry a value; a `<details>` toggle does none of these. Widening the rule to all native interactive chrome would be a new decision with its own blast radius.
145
+
146
+ **Gate:** `npm run audit:stamped-native-buttons` (`scripts/dev/audit-stamped-native-buttons.mjs`; runs in PR CI via `.github/workflows/ci.yml`, NOT in the `npm run check` aggregate) scans `packages/web-components/components/**` + `packages/web-modules/**` `.js` for all four control classes ADR-0055 §Consequences named — `<button` / `createElement('button')` (gh#276, `SANCTIONED_BUTTON` map), `<input` (ADR-0055 / gh#1302, `SANCTIONED_INPUT`), and since gh#1495 `<select` (`SANCTIONED_SELECT`) and `<textarea` (`SANCTIONED_TEXTAREA`) — and fails any hit outside the matching map. The two stamps that widening surfaced are both dispositioned in the maps: `table.class.js` (`createElement('select')` — the numeric-filter operator picker, a value-gathering surface squarely inside the rule, PENDING the same gh#276 filter-popover migration blocker as this file's button/input entries) and `color-picker.class.js` (`createElement('textarea')` — DECIDED gh#1495, keep: an off-screen `execCommand('copy')` clipboard-transport shim, appended and removed within one call, is out of ADR-0025's scope by ADR-0055 §Decision 4's own test — it gathers no user input, joins no `<form>`, carries no user-facing value; the primary path is `navigator.clipboard.writeText` and the shim only runs where the Clipboard API is absent). Entries carry per-file reasons: password is the ADR-0025 exception; `type="file"` pickers (chat-input, upload) and the otp-input / table stamps are listed PENDING their own decision — sanctioned-with-reason is not the same as ratified. A sanctioned entry whose stamp disappears is reported stale so the list only shrinks, and the script's `selftest` proves each element class's positive + negative controls on scratch fixtures (also run in PR CI).
147
+
148
+ Source: ADR-0025 (the rule) · ADR-0055 (conformance amendment, ratified 2026-08-15, gh#1302 — appends to ADR-0025's record; the ADR-0025 text itself is not edited).
149
+
123
150
  ---
124
151
 
125
152
  ## Tokens
@@ -136,6 +136,45 @@ Reserve `error` for validation state (`[error]` on form inputs matches ARIA patt
136
136
  <toggle-group-ui single>...</toggle-group-ui>
137
137
  ```
138
138
 
139
+ ## Selection-item state — a declared, reflected `selected` prop, never a private `data-*` stamp
140
+
141
+ A **selection-item primitive** (one selectable option inside a selection-group parent — `segment-ui` in `segmented-ui`, `toggle-option-ui` in `toggle-group-ui`) exposes its current state as a declared, reflected `selected: Boolean` prop, documented as parent-managed, and styled via `[selected]`:
142
+
143
+ ```javascript
144
+ // child — the declared API surface
145
+ static properties = {
146
+ selected: { type: Boolean, default: false, reflect: true },
147
+ };
148
+ ```
149
+
150
+ ```yaml
151
+ # child yaml — the SoT the catalog and A2UI grammar read
152
+ selected:
153
+ description: >-
154
+ Whether this option is currently selected. Managed by the parent
155
+ <group> container — don't set directly; the group's `value` is the
156
+ single source of truth.
157
+ type: boolean
158
+ default: false
159
+ reflect: true
160
+ ```
161
+
162
+ ```css
163
+ /* child css — the state hook is the reflected attribute */
164
+ segment-ui[selected] { … }
165
+ ```
166
+
167
+ Rules:
168
+
169
+ - **The parent's `value` is the single source of truth**; the parent writes `selected` on its children (`segmented.class.js:146,149` sets/removes the attribute; `toggle-group.class.js:104-111` assigns `opt.selected`). Authors and generated markup drive the group's `value`, never `[selected]` on a child (`toggle-group.yaml`'s rules block says so in so many words).
170
+ - **No private `data-*` stamp for API-conceptual state.** `data-selected` hides the state from the yaml SoT, the catalog, and generative authoring — a consumer or the A2UI grammar cannot express "this option is selected" against a stamp that no schema declares. `toggle-group-ui` used to stamp `data-selected` + style `:scope[data-selected]`; it converged onto `segment-ui`'s mechanism (`toggle-option.yaml:38-46`, `toggle-group.css:14`, `toggle-group.test.js:64` asserts the stamp is gone).
171
+ - **Parent-stamped ARIA stays as-is — ARIA is wiring, not API.** `segment-ui` derives `aria-checked` from `selected` (`segment.class.js:42`); `toggle-option-ui` derives `aria-pressed` (`toggle-group.class.js:65`). The reflected prop is the API; the ARIA attribute follows it.
172
+ - **Reference implementation:** `segment-ui` (`segment.yaml:32-36`, `segment.css:7`); `toggle-option-ui` is the converged second instance. A new selection-item primitive copies this shape — extending one of the two by analogy is what produced the divergence in the first place (gh#1303).
173
+
174
+ Migration note: removing `data-selected` was a breaking change for external CSS that targeted it, so it shipped in two halves — the additive `selected` prop in `0.8.39`, the stamp removal on the `0.8.40` breaking wave with a migration-guide entry (`packages/web-components/CHANGELOG.md`; the ADR text names the wave `0.9.0` — it shipped as `0.8.40`).
175
+
176
+ Source: ADR-0056 (ratified 2026-08-15, gh#1303).
177
+
139
178
  ## Three-way name consistency
140
179
 
141
180
  The component has three names that must agree:
@@ -195,7 +195,7 @@ a toolbar (AP-DP-06, the integrations-page incident).
195
195
 
196
196
  ```bash
197
197
  npm run dev # foreground
198
- # QA isolation: /docs/qa/component-isolation.html?slug=<slug>
198
+ # QA isolation: /.claude/docs/qa/component-isolation.html?c=<slug>
199
199
  # Site route: /site/components/<slug>
200
200
  ```
201
201
 
@@ -82,7 +82,6 @@ Does your [attribute=value] need to change any of:
82
82
 
83
83
  **Approved mode attributes** (as of the most recent contract update):
84
84
 
85
- - `progress-ui[variant="spinner"]` — size and flex centering change
86
85
  - `code-ui[inline]` — inline vs block display
87
86
  - `divider-ui[vertical]` — flex-direction, width ↔ height swap
88
87
  - `tabs-ui[orientation="vertical"]` — flex-direction swap
@@ -172,6 +171,13 @@ This was a real bug in `button.css` caught in a final audit pass.
172
171
  - ≥ 3px: forbidden in component base styles. Use `var(--a-space-*)`.
173
172
  - Exception: component-intrinsic constants (e.g. a port-dot diameter, an icon size that must match a specific SVG coordinate). Each such literal needs a one-line comment justifying why.
174
173
 
174
+ - **Typography — leading, tracking, weight (ADR-0052, ratified 2026-08-15, gh#1298):** zero bare literals in `packages/web-components/components/*/*.css`; every value goes through the one scale per property in `styles/type/scale.css`.
175
+ - **Leading:** `--a-font-leading-*` is the single canonical scale — `none: 1` · `tight: 1.05` · `snug: 1.2` · `normal: 1.35` · `relaxed: 1.5` · `loose: 1.6` (`scale.css:67-72`). The legacy flat `--a-leading-*` family (`none/tight/snug/normal` = 1/1.2/**1.3**/**1.5**) is **retired** — removed from `scale.css` with no value-preserving aliases, because `snug` and `normal` named different values in each family and an alias would silently restyle. Two families under shared step names was the root cause of the literal epidemic (55/131 component files hardcoding `line-height`): no token choice was obviously correct, so authors reached for a number.
176
+ - **Mapping for the literals you will find in older code:** `1.3` and `1.4` both → `--a-font-leading-normal` (1.35); at the 12–14 px sizes where they occur, ±0.05 is ≤ 0.7 px — below visual significance, and no new step is minted to ratify drift. Escape valve: genuinely multi-line body copy may map up to `--a-font-leading-relaxed` (1.5), case-by-case. Legacy `--a-leading-snug` (1.3) → `--a-font-leading-normal`; legacy `--a-leading-normal` (1.5) → `--a-font-leading-relaxed` — value-nearest, not name-preserving.
177
+ - **Tracking:** `0.05em` → `--a-font-tracking-wide` (0.04em); everything else was already on-scale (`--a-font-tracking-tight/snug/normal/wide/wider`, `scale.css:75-79`). **Weight:** numeric weights tokenize to `--a-weight-*` (`scale.css:47-52`).
178
+ - **Gate:** `npm run check:typography-tokens` (`scripts/audit/check-typography-tokens.mjs --strict`, in the `npm run check` aggregate) fails any bare numeric `line-height` (except `0`, the icon line-box-collapse idiom), any `em`-literal `letter-spacing`, any numeric `font-weight`, **and (gh#1496) any numeric literal used as a `var()` fallback** on one of those three properties or on a custom property whose own name names the role (`--alert-line-height`, `--foo-weight`) — `line-height: var(--slider-hint-lh, 1.4)` trips it exactly like a bare literal, because the fallback still computes to the raw number whenever the custom property is unset. When you author a fallback, fall back to a token (`var(--x, var(--a-font-leading-normal))`), not a number — the gate now enforces that rather than merely recommending it. Comments are stripped before scanning.
179
+ - Shipped as a breaking change on the `0.8.40` wave (`--a-leading-*` were published stylesheet symbols; the ADR text names the wave `0.9.0` — it shipped as `0.8.40`), with its migration-guide entry in the same cycle. Source: ADR-0052.
180
+
175
181
  Example carve-out:
176
182
 
177
183
  ```css
@@ -331,7 +331,7 @@ This is the **highest-impact category** — corpus chunks with stale `source` or
331
331
  ##### Category D — Inventory tables in cross-cutting docs
332
332
 
333
333
  ```bash
334
- grep -ln 'old-name' README.md AGENTS.md .claude/docs/ROADMAP.md .claude/docs/specs/INDEX.md
334
+ grep -ln 'old-name' README.md AGENTS.md docs/ops/ROADMAP.md .claude/docs/specs/INDEX.md
335
335
  ```
336
336
 
337
337
  **Pre-rename verification check**: before the rename, take a snapshot:
@@ -0,0 +1,282 @@
1
+ # SVG authoring — coordinate, color, and hit-testing quirks
2
+
3
+ SVG content behaves differently from HTML in ways that don't show up until a
4
+ primitive is placed inside a themed, resizable, or bled container. This file
5
+ collects the SVG-specific rules — everything else about authoring a
6
+ primitive (yaml, tokens, lifecycle) is the rest of this skill's charter, not
7
+ repeated here. Load this file when modifying `chart-ui`, `qr-code-ui`,
8
+ `icon-ui`, or authoring any NEW primitive whose `class.js` builds `<svg>`
9
+ markup (via `document.createElementNS`/`innerHTML`) rather than plain HTML.
10
+
11
+ ## 0. Which primitives actually render SVG (scope check first)
12
+
13
+ Not every chart-family or chart-adjacent primitive renders SVG — check
14
+ before assuming this file applies:
15
+
16
+ - **Genuinely SVG-rendered**: `chart-ui` (`packages/web-components/components/chart/chart.class.js` — builds a `<svg>` string per chart type, §§#renderBar/#renderLine/etc.), `qr-code-ui` (`packages/web-components/components/qr-code/qr-code.class.js:107-118` + `qr-encoder.js:609-631`'s `matrixToSVG`), `icon-ui` (`packages/web-components/components/icon/icon.class.js:97-98` — stamps a Phosphor `<svg>` string via `getIcon()`).
17
+ - **NOT SVG** despite living in the chart family: `chart-legend-ui` (`packages/web-components/components/chart-legend/chart-legend.class.js` — composes `<badge-ui>` + `<swatch-ui>`, no `<svg>` anywhere) and `swatch-ui` (`packages/web-components/components/swatch/swatch.class.js` — plain `<span data-tile>` divs styled via CSS `background`/`border`, confirmed by `grep -rn svg` returning nothing in either file). gh#1344's own body assumed `chart-legend-ui` was SVG-adjacent; it isn't — its swatch shapes (dot/square/line/dashed) are CSS box-model tricks, not paths.
18
+
19
+ A future primitive whose `class.js` calls `createElementNS('http://www.w3.org/2000/svg', ...)` or sets `innerHTML` to a string containing `<svg>` is in scope for every rule below; one that only composes other `*-ui` elements (however chart-shaped visually) is not.
20
+
21
+ ## 1. viewBox is a coordinate system, not a size — two sizing strategies coexist
22
+
23
+ `viewBox="minX minY width height"` defines the SVG's INTERNAL coordinate
24
+ system; the element's rendered box size is separate (CSS `width`/`height` or
25
+ SVG `width`/`height` attributes). Every number emitted into the SVG markup
26
+ (`x`, `y`, `r`, `stroke-width`, `font-size`) is in viewBox units, not CSS
27
+ pixels — the browser scales the whole coordinate system to fit the rendered
28
+ box (`preserveAspectRatio`, default `xMidYMid meet`).
29
+
30
+ Two different sizing strategies are in use, deliberately:
31
+
32
+ - **`chart-ui` — responsive viewBox, CSS owns the box.** `chart.css:143-149` sets `svg { width: 100%; height: auto; max-height: 100%; overflow: visible }`; `#dims()` (`chart.class.js:405-457`) computes `width`/`height` FROM `this.clientWidth`/`clientHeight` every render, and `#renderChart()` sets `viewBox="0 0 ${width} ${height}"` (e.g. `chart.class.js:523`) to match. Because the viewBox is recomputed from the actual container size on every render, viewBox units and CSS px are numerically equal in the steady state — a `stroke-width: 2` in `chart.css:199` reads as 2 real px. A `ResizeObserver` (`chart.class.js:363-380`, debounced via `requestAnimationFrame`) keeps this in sync across container resizes; there's a brief window between a resize and the debounced re-render where the OLD viewBox is still active against the NEW box size, during which strokes/dots/fonts visually scale up or down with the mismatch — this is inherent to the responsive-viewBox strategy, not a bug to fix per-primitive.
33
+ - **`qr-code-ui` — fixed pixel viewBox, explicit width/height attributes.** `matrixToSVG` (`qr-encoder.js:609-631`) sets `viewBox="0 0 ${total} ${total}"` AND `width="${total}" height="${total}"` (equal, so no scaling happens at generation time); `qr-code.class.js:123-127` then overwrites the `width`/`height` ATTRIBUTES (not CSS) to the `[size]` prop after `innerHTML` is set. `qr-code.css:23-28` documents why it does NOT use `width: 100%`: the host is `display: block` sized-to-content (the SVG itself), so a CSS-percentage width on the SVG would create a circular sizing dependency — "trust the SVG attributes" is the comment's own words.
34
+
35
+ **When authoring a new SVG primitive**, pick one of these two strategies deliberately and document which: responsive-viewBox (chart-ui's approach — needed when the primitive must fill an arbitrary, resizable container) or fixed-attribute (qr-code-ui's approach — needed when the primitive has a scannable/pixel-exact payload where uncontrolled scaling would break fidelity, and the `[size]` prop is the only sizing lever a consumer needs).
36
+
37
+ ## 2. `stroke-width` and other bare numbers scale with the coordinate system
38
+
39
+ Because `stroke-width`, circle `r`, and `font-size` values written into the
40
+ SVG markup are viewBox-unit numbers (see §1), they are NOT the same kind of
41
+ value as a CSS `border-width` or `font-size` on an HTML element — an HTML
42
+ border stays a fixed px regardless of ancestor `transform: scale()` (the
43
+ border itself doesn't get bigger, only the box does); an SVG stroke drawn in
44
+ viewBox units scales proportionally with ANY transform that changes the
45
+ effective viewBox-to-rendered-size ratio, including a CSS `transform: scale()`
46
+ on the `<svg>` or an ancestor, and including the responsive-viewBox mismatch
47
+ window described in §1. `chart.css:198-203`'s `[data-line] { stroke-width:
48
+ var(--chart-line-width) }` (unitless — SVG interprets an unadorned number as
49
+ user units) is a concrete example: at steady state this renders at the CSS
50
+ `--chart-line-width` value in real px, but during a `transform: scale(1.5)`
51
+ hover-zoom on a chart card it renders at 1.5× that, same as every other
52
+ number in the shape's geometry — there is no way to pin stroke-width to a
53
+ fixed screen px independent of the coordinate system short of
54
+ `vector-effect: non-scaling-stroke` (not used anywhere in this codebase
55
+ today — flag it if a future primitive needs scale-independent strokes).
56
+
57
+ ## 3. `text-anchor`/`dominant-baseline` position an anchor POINT, not a box corner
58
+
59
+ SVG `<text>` has no intrinsic box model — `x`/`y` mark a single anchor
60
+ point, and `text-anchor`/`dominant-baseline` say which part of the glyph run
61
+ sits at that point. Getting this wrong is the single most common SVG label
62
+ bug (text drifts off its intended mark as content length changes). Every
63
+ label renderer in `chart.class.js` picks the anchor deliberately:
64
+
65
+ - **Y-axis labels** — `text-anchor="end"` (`chart.class.js:1053`): the anchor point sits at the RIGHT edge of the label so labels of different digit-widths ("5", "5,000") stay right-aligned against the axis rather than growing rightward from a fixed left point.
66
+ - **X-axis / value labels** — `text-anchor="middle"` (`chart.class.js:1068`, `1109`): centers over each bar/point regardless of label width.
67
+ - **Donut center total/label, gauge value, funnel stage/value/drop, radar labels, sankey node labels** — `dominant-baseline="central"` (e.g. `chart.class.js:1270-1271`, `1585,1587`, `1642-1648`, `1357`, `1846,1852`): vertically centers the glyph on its `y` coordinate — needed anywhere a label sits beside or inside a shape whose center, not its top, is the meaningful reference point (a donut's numeric center, a radial label at a computed angle).
68
+ - **Treemap tile labels — `dominant-baseline` switches per available space** (`chart.class.js:1766-1771`): `hanging` (top-aligned, the default vertical-metrics baseline) for a "tall" tile where label + value stack top-down, `central` for a "short" tile where only the label fits and it should sit mid-height rather than clipped against the top edge. Pick the baseline that matches the layout decision, not a single default for the whole primitive.
69
+
70
+ Rule of thumb: `text-anchor` picks the horizontal anchor (`start`/`middle`/`end`), `dominant-baseline` picks the vertical one (`hanging`/`central`/`middle`/the default alphabetic baseline) — set both explicitly whenever a label's position depends on computed geometry rather than a fixed corner.
71
+
72
+ ## 4. Card-boundary clipping — `overflow: visible` is the default; a bleed section changes the contract
73
+
74
+ `chart.css:143-149`'s `svg { overflow: visible }` is intentional: chart
75
+ labels routinely extend slightly past the nominal plot rectangle (Y-axis
76
+ labels sit at `pad.left - 4`, per §3), and `overflow: visible` lets that
77
+ render instead of clipping at the SVG's own box edge. That default is safe
78
+ inside a normally-inset `card-ui` section. It stops being safe the moment
79
+ the SAME chart sits inside a `<section bleed>` — `card-ui`'s `:scope` itself
80
+ clips at `overflow: hidden` with a rounded `border-radius` (`card.css`, top
81
+ of file), and `[bleed]` zeroes the section's own margin/padding
82
+ (`card.css:342` onward) — so a chart's axis-label overhang, or gridlines
83
+ extending to the plot edge, lands flush against that rounded corner and
84
+ clips silently. This was gh#1095's real incident (PR #1105): a bar/line
85
+ chart's Y-axis labels clipped under a bled card's corner.
86
+
87
+ **The fix is the canonical convention** — `card.css:400-422` (comment block
88
+ + rule):
89
+
90
+ ```css
91
+ /* Guide/value/legend safety inset (gh#1095) — [bleed]'s zero-inset is
92
+ only collision-free for the bare-marks case: a sparkline, or any other
93
+ chart-ui type with BOTH [hide-grid] and [hide-values] set. */
94
+ & > section[bleed]:has(> chart-ui:not([type="sparkline"]):not([hide-grid])),
95
+ & > section[bleed]:has(> chart-ui:not([type="sparkline"]):not([hide-values])),
96
+ & > section[bleed]:has(> chart-legend-ui) {
97
+ margin: var(--card-inset);
98
+ padding: 0;
99
+ }
100
+ ```
101
+
102
+ This restores the card's own inset automatically the moment a bled chart
103
+ still draws guides/values/a legend — a `:has()`-based fail-safe rather than
104
+ relying on every author to remember. `#renderSparkline()` is the ONLY
105
+ renderer that never emits axis ticks, gridlines, value text, or a legend
106
+ (`chart.class.js`'s sparkline branch) — it's the sole type where genuine
107
+ edge-to-edge bleed is collision-free with no attribute needed. Every other
108
+ type needs BOTH `[hide-grid]` and `[hide-values]` set to opt into true
109
+ bleed; short of that, the safety inset applies and the chart renders inset
110
+ like a normal card section — expected, not a bug.
111
+
112
+ **The generalized rule for any new SVG primitive placed inside a bleed
113
+ section**: an SVG whose content can extend past its own nominal box
114
+ (`overflow: visible`, or geometry computed with negative padding) needs
115
+ either a "bare marks" mode (no overhanging content) that's safe to bleed, or
116
+ a CSS `:has()` safety net analogous to `card.css:400-422` that restores
117
+ inset automatically when the overhanging content is present. Don't assume
118
+ `overflow: hidden` on the ancestor container will clip cleanly — SVG content
119
+ drawn PAST an ancestor's padding box (not its own) clips at whatever
120
+ ancestor in the chain actually sets `overflow: hidden`, which for
121
+ `card-ui` is the rounded-corner boundary itself, producing the specific
122
+ silently-clipped-under-a-curve look #1095 reported.
123
+
124
+ ## 5. CSS custom properties don't resolve inside raw SVG attribute strings — only inside actual CSS declarations
125
+
126
+ A CSS custom property (`var(--foo)`) only resolves where the CSS cascade
127
+ parses it: inside a stylesheet rule, or inside an inline `style="..."`
128
+ attribute. It does NOT resolve inside an arbitrary SVG presentation
129
+ attribute value written as a plain string (`fill="var(--foo)"` is invalid —
130
+ the literal text `var(--foo)` is not a recognized SVG color, and the shape
131
+ renders with the initial/inherited fill instead, silently). `currentColor`
132
+ is different: it's a CSS-wide keyword the SVG spec itself recognizes inside
133
+ presentation attributes, and it resolves against the computed `color`
134
+ property the normal way — so `fill="currentColor"` written directly into
135
+ markup DOES cascade correctly. `icon-ui` relies on exactly this: the
136
+ installed Phosphor SVGs ship `fill="currentColor"` on their root `<svg>`
137
+ (confirmed: `node_modules/@phosphor-icons/core/assets/regular/caret-right.svg`
138
+ — `<svg ... fill="currentColor">`), and `icon.css`'s `:scope { color:
139
+ var(--icon-color) }` (`icon.css:11`) drives it through the ordinary
140
+ `color` inheritance chain — no `var()` inside the SVG markup is needed
141
+ because `currentColor` isn't a custom property.
142
+
143
+ `chart-ui` hit this distinction directly and got it wrong once (gh#561,
144
+ documented in `chart.class.js:548-560`): an earlier version wrote
145
+ `--color-{key}: var(--chart-N)` as an inline STYLE on the chart HOST, then
146
+ tried to reference `--color-{key}` from series-colored shapes — but because
147
+ inline styles win the cascade over everything except `!important`, a
148
+ consumer's own `--color-MAU` set on an ancestor lost to the chart's own
149
+ inline default, making the documented "override `--color-{key}` to recolor
150
+ a series" hook unusable. **The fix, and the pattern to follow**: never set
151
+ the color custom property on the host; instead emit it as an inline `style`
152
+ attribute ON THE SHAPE ITSELF, with the fallback chain built into the same
153
+ declaration — `#seriesFill()`/`#seriesStroke()` (`chart.class.js:568-575`)
154
+ emit ` style="fill: var(--color-${seriesKey}, var(--chart-${slotIdx}))"` per
155
+ `<path>`/`<circle>`. Because this IS a real CSS declaration (inside
156
+ `style=""`), `var()` resolves normally, an ancestor-set `--color-{key}`
157
+ flows through the cascade and wins, and an unset one falls through to the
158
+ palette slot — exactly the semantics a bare attribute string can't provide.
159
+
160
+ **`qr-code-ui` shows the failure mode `chart-ui` avoided**: `qr-code.css:7-8`
161
+ declares `--qr-code-fg: currentColor` / `--qr-code-bg: transparent` and sets
162
+ them as `color`/`background` on the HOST (`qr-code.css:17-18`) — but the
163
+ actual QR modules are painted via `matrixToSVG` (`qr-encoder.js:609-631`),
164
+ which bakes `fill="${fg}"`/`fill="${bg}"` as literal hex strings
165
+ (`options.color || '#000'`, `qr-code.class.js:115-116` passes
166
+ `this.color || '#000000'`) directly into the generated markup at render
167
+ time. The `--qr-code-fg`/`--qr-code-bg` tokens are real and declared, but
168
+ nothing in the render path ever reads them — setting `color` on an ancestor
169
+ of a default `<qr-code-ui>` does nothing to its rendered fill; only the
170
+ explicit `[color]`/`[background]` HTML attributes do (and deliberately so —
171
+ the code comment at `qr-code.class.js:110-115` explains theme-aware
172
+ `currentColor` would produce light-on-dark QR codes that most phone cameras
173
+ refuse to scan). **When authoring a new SVG primitive with a
174
+ "theming token" in its CSS, verify the render path actually consumes it as
175
+ a live CSS value (inline `style=` per shape, or a bare `currentColor`
176
+ keyword) rather than baking a computed color into the generated markup as a
177
+ one-time string — a declared-but-dead token is a real trap for the next
178
+ author who tries to theme the primitive from outside.**
179
+
180
+ ## 6. Hit-testing: `fill: transparent` is clickable, `fill: none` is not
181
+
182
+ SVG's default `pointer-events: visiblePainted` treats a shape as
183
+ hit-testable only if it's "painted" — `fill: transparent` counts as painted
184
+ (alpha-zero, but still a fill), `fill: none` does not. `chart.css:381-386`
185
+ states this explicitly as the reason its hit-target circles are always
186
+ `fill: transparent !important` rather than `fill: none`:
187
+
188
+ ```css
189
+ /* Hit-target overlays must never be filled by the slice palette —
190
+ they're meant to be invisible pointer-event surfaces. */
191
+ circle[data-hit] {
192
+ fill: transparent !important;
193
+ stroke: none;
194
+ }
195
+ ```
196
+
197
+ This matters most for THIN shapes — a `<path data-line>` stroke has almost
198
+ zero hit area along its own geometry, so `chart.class.js` renders a SEPARATE,
199
+ generously-radiused invisible circle per point (`data-hit`, `hitR =
200
+ Math.max(dotR, 10)` at `chart.class.js:1157/1165`, similarly `1450` for
201
+ scatter) purely to catch pointer/click events, decoupled from the visible
202
+ dot's actual radius. The average-line overlay is the same pattern applied to
203
+ a LINE instead of a point: the visible dashed average line is `stroke-width:
204
+ 1.5` (`chart.css:259-263`), effectively unclickable, so a second invisible
205
+ line with `stroke: transparent stroke-width="12"` rides on top purely for
206
+ hit area (`chart.class.js:1119,1177`, labeled "Wider invisible hit target so
207
+ the thin dashed line is hoverable" in the source comment). By contrast,
208
+ FILLED shapes with real area — bars (`<path data-bar>`), pie/donut slices,
209
+ radial-bar arcs — carry their `tip()` data attributes directly on the
210
+ visible shape (`chart.class.js:1106`, `1209`, `1514`) with no separate hit
211
+ overlay needed, because the visible fill already satisfies
212
+ `visiblePainted`.
213
+
214
+ **Rule for a new SVG primitive**: any interactive target whose visible
215
+ stroke/fill area is too thin or too small to reliably hit with a pointer
216
+ needs an invisible, generously-sized `fill: transparent` (never `fill:
217
+ none`) overlay shape carrying the actual event data — don't rely on the
218
+ visible geometry's own hit area once its rendered stroke-width or radius
219
+ drops below a comfortable pointer target size (chart-ui's overlays use
220
+ `r ≥ 10`, `stroke-width ≥ 12` as the floor).
221
+
222
+ ## 7. `shape-rendering` — pixel-grid content vs. smooth curves
223
+
224
+ `qr-code.css:29`/`qr-encoder.js:629` set `shape-rendering: crispEdges` on
225
+ the generated QR `<svg>` — this disables anti-aliasing so each QR module
226
+ renders as a hard-edged square rather than a slightly blurred one, which
227
+ matters for scanner reliability (soft edges reduce contrast at module
228
+ boundaries a camera decoder relies on). `chart-ui` sets no `shape-rendering`
229
+ override anywhere — its curves (`smoothPath`'s Catmull-Rom bezier
230
+ conversion, `chart.class.js:158-180`) are meant to anti-alias normally.
231
+ **When authoring a new SVG primitive rendering a hard pixel/module grid
232
+ (a matrix code, a pixel-art preview, anything where edge crispness affects
233
+ correctness rather than just aesthetics), set `shape-rendering: crispEdges`
234
+ explicitly** — the browser default (`auto`, effectively anti-aliased) is
235
+ correct for everything else and should stay the default.
236
+
237
+ ## 8. Attribute-shadowing on SVG-adjacent primitives (ADR-0053/0054/0070)
238
+
239
+ Two of the global-attribute-grammar collisions gh#1335 surfaced are
240
+ specifically SVG-rendered primitives; both exemptions are **GRANTED**
241
+ (ADR-0070, ratified 2026-08-17), closing the last 2 of gh#1335's 17 and
242
+ emptying `check-attribute-shadowing.mjs`'s `KNOWN_FINDINGS` — the gate now
243
+ enforces via `attribute-api-system.md`'s ratified §11 table alone:
244
+
245
+ - **`qr-code-ui[color]`** — a free-form CSS color string (drives the raw
246
+ `fill` baked into the generated matrix SVG, §5 above), structurally
247
+ identical to the ratified `swatch-ui`/`noodles-ui[color]` §11 exemptions
248
+ ("the component's entire subject is a color"). The exemption covers the
249
+ free-form value space AND the hardcoded `#000000` scanability fallback
250
+ (deliberately never theme-derived — light-on-dark won't scan), plus its
251
+ contract pairing with `[background]`.
252
+ - **`icon-ui[weight]`** — Phosphor's own glyph-variant vocabulary
253
+ (`thin/light/regular/bold/fill/duotone`, selecting which pre-rendered SVG
254
+ set `getIcon()` loads) is a DIFFERENT CONCEPT from CSS `font-weight`
255
+ despite the shared name; the global utility's `font-weight` is inert on
256
+ the inline SVG, so the collision has no cascade effect.
257
+
258
+ Known residual (documented in the §11 justification, accepted not fixed):
259
+ global-only `weight` values (`normal|medium|semibold`) select no Phosphor
260
+ set and fall back to `regular`; a semantic-enum value on `qr-code-ui[color]`
261
+ (`color="danger"`) passes to the SVG fill verbatim, unresolved through
262
+ tokens. Cite this section — ADR-0070 — not a fresh investigation, if either
263
+ attribute surfaces again in a yaml audit; the next colliding
264
+ `color`/`weight` on any OTHER component still needs its own ADR.
265
+
266
+ ## When to load this file
267
+
268
+ Any authoring task touching `chart-ui`, `qr-code-ui`, `icon-ui`, or a new
269
+ primitive whose `class.js` emits `<svg>` markup — a new chart type, a
270
+ label-positioning fix, a card-bleed interaction, a hit-target bug, or a
271
+ color/theming prop on an SVG-rendered primitive. NOT for `chart-legend-ui`
272
+ or `swatch-ui` (§0) — those are HTML/CSS primitives despite the chart-family
273
+ name; their authoring questions route through the general
274
+ [css-patterns.md](css-patterns.md) / [api-contract.md](api-contract.md)
275
+ same as any other component. The `--chart-*` and `--qr-code-*` TOKEN
276
+ declarations themselves (naming, `:where(:scope)` placement) still follow
277
+ [token-contract.md](token-contract.md) — this file covers only what's
278
+ SVG-specific once those tokens reach the render path. The ≤2px raw
279
+ `stroke-width` carve-out in [css-patterns.md](css-patterns.md)'s "Raw
280
+ values" section is the general rule this file's §2 explains the SVG-specific
281
+ mechanism behind — cite both together when a stroke-width literal comes up
282
+ in review.
@@ -133,12 +133,57 @@ mechanics; never work from this summary alone):
133
133
  *explicit* per-element overrides (non-inheriting), while `[size]`/`[density]`
134
134
  are *ambient* context-setters (inheriting by design). Which axis an attribute
135
135
  sits on decides its `@property` registration and how components read it.
136
- - **No shadowing** (`docs/adr/adr-0053-no-shadowing-global-attributes.md`): a
136
+ - **No shadowing** (`docs/ops/adr/adr-0053-no-shadowing-global-attributes.md`): a
137
137
  component-local attribute may not share a name with any global attribute —
138
138
  the global name always means the global thing. Before minting any attribute
139
139
  in a component yaml, check it against the spec's attribute inventory; the
140
140
  only sanctioned collisions live in the spec's §11 Global-Attribute Exemption
141
- List (first entries: `swatch-ui[color]` / `noodles-ui[color]`, ADR-0054).
141
+ List (first entries: `swatch-ui[color]` / `noodles-ui[color]`, ADR-0054;
142
+ granted since: `qr-code-ui[color]` / `icon-ui[weight]`, ADR-0070).
143
+
144
+ Beyond the global grammar, ADR-0063
145
+ (`docs/ops/adr/adr-0063-attribute-grammar-addendum.md`) ratifies six
146
+ CROSS-SIBLING conventions for component-local attribute naming — the axis
147
+ ADR-0053/0054 don't cover. Any new attribute follows these:
148
+
149
+ - **`no-*` is the canonical negation prefix** — `hide-*` retires
150
+ (`chart[hideAverage|hideGrid|hideValues]` → `no*`, `stream[hide-cursor]`
151
+ → `[no-cursor]`).
152
+ - **Interactivity is opt-in by a positively-named boolean, default `false`**
153
+ (the `stepper-item[interactive]` / `swatch[selectable]` shape) — never
154
+ opt-out. `chart-legend[static]` retires to `[interactive]` (inverted
155
+ default). Applies to single-purpose toggles only — `noodles`'s
156
+ `editable`/`readonly` pair is two orthogonal booleans and stays.
157
+ - **Two sanctioned multi-value encodings, never a hand-rolled delimited
158
+ string:** (a) `multiple` boolean + a structured option model (`select`'s
159
+ `options`/`<option>` shape); (b) a native array-typed JS property,
160
+ JSON-serialized for attribute/form participation (`tags-input`'s
161
+ `.value`). One explicit, operator-ratified EXCEPTION for existing
162
+ components (recorded on gh#1563, 2026-08-17): `segmented-ui[multiple]`'s
163
+ comma-separated value IS accepted as the sanctioned multiple+options
164
+ encoding, and toggle-group's comma-string resolves under that same ruling
165
+ rather than being re-ticketed. This is a ruling on those two existing
166
+ components, not a loophole — a NEW component's multi-value attribute still
167
+ uses form (a) or (b) above, never a delimited string, unless a separate
168
+ ruling says otherwise.
169
+ - **Cross-sibling naming convergence:** `pane[side]` → `pane[edge]`
170
+ (drawer keeps physical `side`); `menu-item[subtitle]` →
171
+ `menu-item[description]` (the ~20-component majority word); `placement`
172
+ ratified as-is for anchored popovers. Documented exception:
173
+ `input[maxlength]` — a primitive wrapping a genuine native `<input>`
174
+ keeps the native attribute's exact casing (ADR-0025/0055 conformance);
175
+ new non-native length constraints use the framework's
176
+ camelCase-property/kebab-attribute pair.
177
+ - **`completed` is the stage-terminal word** for a `status` lifecycle enum
178
+ (`agent-reasoning[status]`'s `done` converges).
179
+ - **`-picker` is reserved for the outer trigger+popover form-associated
180
+ composite** — never the inline substrate it composes. `color-picker-ui`
181
+ (the inline substrate) renames to `color-area-ui`.
182
+
183
+ The renames above are follow-on BUILD work tracked as gh#1563, not yet
184
+ landed in code (compat shims in 0.8.42; breaking cut rides 0.9.0) — when a
185
+ yaml still shows an old name, the ADR is ratified and the code is behind,
186
+ not the other way around.
142
187
 
143
188
  ## When to update this reference
144
189