@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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/scripts/release-pretag-docs-gate +1 -1
- package/skills/a2ui-maintenance/SKILL.md +43 -6
- package/skills/a2ui-maintenance/references/anti-patterns.md +1 -1
- package/skills/a2ui-maintenance/references/chunk-authoring.md +2 -2
- package/skills/a2ui-maintenance/references/eval-diagnostics.md +1 -1
- package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +2 -2
- package/skills/a2ui-maintenance/references/pipeline-overview.md +2 -2
- package/skills/a2ui-maintenance/references/surface-lifecycle.md +73 -0
- package/skills/demo-audit/references/admin-shell-anatomy.md +1 -1
- package/skills/demo-audit/scripts/analyze.mjs +1 -1
- package/skills/package-release/SKILL.md +1 -1
- package/skills/package-release/references/cut-procedure.md +14 -4
- package/skills/package-release/references/gates-catalog.md +7 -1
- package/skills/package-release/scripts/gate-roster.mjs +28 -1
- package/skills/package-release/scripts/release-pack.mjs +43 -4
- package/skills/primitive-authoring/SKILL.md +1 -0
- package/skills/primitive-authoring/references/INDEX.md +2 -1
- package/skills/primitive-authoring/references/anti-patterns.md +27 -0
- package/skills/primitive-authoring/references/api-contract.md +39 -0
- package/skills/primitive-authoring/references/composite-demo-protocol.md +1 -1
- package/skills/primitive-authoring/references/css-patterns.md +7 -1
- package/skills/primitive-authoring/references/shell-patterns.md +1 -1
- package/skills/primitive-authoring/references/svg-authoring.md +282 -0
- package/skills/primitive-authoring/references/token-contract.md +47 -2
- package/skills/primitive-authoring/references/yaml-contract.md +239 -6
- package/skills/site-deployment/references/deploy-playbooks.md +12 -4
- package/skills/ssr-compatibility/SKILL.md +10 -5
- package/skills/ssr-compatibility/references/consumer-workarounds.md +14 -6
- package/skills/ssr-compatibility/references/failure-shapes.md +30 -7
- package/skills/ssr-compatibility/references/guard-patterns.md +28 -0
- package/skills/ssr-compatibility/references/status-ledger.md +13 -0
- 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?
|
|
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
|
|
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
|
|