@adia-ai/adia-ui-forge 0.8.39 → 0.8.41
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 +21 -0
- package/package.json +1 -1
- package/scripts/release-pretag-docs-gate +1 -1
- package/skills/a2ui-maintenance/SKILL.md +26 -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/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 +277 -0
- package/skills/primitive-authoring/references/token-contract.md +1 -1
- package/skills/primitive-authoring/references/yaml-contract.md +165 -5
- 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
|
@@ -0,0 +1,277 @@
|
|
|
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)
|
|
238
|
+
|
|
239
|
+
Two of the pre-existing global-attribute-grammar collisions gh#1335 surfaced
|
|
240
|
+
are specifically SVG-rendered primitives, and both cite the SVG-specific
|
|
241
|
+
reasoning for their proposed resolution rather than the generic
|
|
242
|
+
rename/converge path:
|
|
243
|
+
|
|
244
|
+
- **`qr-code-ui[color]`** — a free-form CSS color string (drives the raw
|
|
245
|
+
`fill` baked into the generated matrix SVG, §5 above), structurally
|
|
246
|
+
identical to the already-ratified `swatch-ui`/`noodles-ui[color]` §11
|
|
247
|
+
exemptions ("the component's entire subject is a color") — proposed for
|
|
248
|
+
the same exemption rather than a rename.
|
|
249
|
+
- **`icon-ui[weight]`** — Phosphor's icon-rendering weight vocabulary
|
|
250
|
+
(`thin/light/regular/bold/fill/duotone`, selecting which pre-rendered SVG
|
|
251
|
+
asset variant `getIcon()` loads) is a DIFFERENT CONCEPT from CSS
|
|
252
|
+
`font-weight` (`thin/light/normal/medium/semibold/bold`) despite the
|
|
253
|
+
shared attribute name — gh#1335 names it "the strongest candidate for a
|
|
254
|
+
ratified §11 exemption rather than a rename," the same reasoning as
|
|
255
|
+
`swatch-ui[color]`.
|
|
256
|
+
|
|
257
|
+
Neither is resolved yet (both remain in `check-attribute-shadowing.mjs`'s
|
|
258
|
+
`KNOWN_FINDINGS` list per gh#1335) — cite this section, not a fresh
|
|
259
|
+
investigation, if either surfaces again in a yaml audit.
|
|
260
|
+
|
|
261
|
+
## When to load this file
|
|
262
|
+
|
|
263
|
+
Any authoring task touching `chart-ui`, `qr-code-ui`, `icon-ui`, or a new
|
|
264
|
+
primitive whose `class.js` emits `<svg>` markup — a new chart type, a
|
|
265
|
+
label-positioning fix, a card-bleed interaction, a hit-target bug, or a
|
|
266
|
+
color/theming prop on an SVG-rendered primitive. NOT for `chart-legend-ui`
|
|
267
|
+
or `swatch-ui` (§0) — those are HTML/CSS primitives despite the chart-family
|
|
268
|
+
name; their authoring questions route through the general
|
|
269
|
+
[css-patterns.md](css-patterns.md) / [api-contract.md](api-contract.md)
|
|
270
|
+
same as any other component. The `--chart-*` and `--qr-code-*` TOKEN
|
|
271
|
+
declarations themselves (naming, `:where(:scope)` placement) still follow
|
|
272
|
+
[token-contract.md](token-contract.md) — this file covers only what's
|
|
273
|
+
SVG-specific once those tokens reach the render path. The ≤2px raw
|
|
274
|
+
`stroke-width` carve-out in [css-patterns.md](css-patterns.md)'s "Raw
|
|
275
|
+
values" section is the general rule this file's §2 explains the SVG-specific
|
|
276
|
+
mechanism behind — cite both together when a stroke-width literal comes up
|
|
277
|
+
in review.
|
|
@@ -133,7 +133,7 @@ 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
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Authoritative source-of-truth fields for `packages/web-components/components/<name>/<name>.yaml` and `packages/web-modules/<cluster>/<name>/<name>.yaml`. The build pipeline (`scripts/build/components.mjs`) reads these yamls + emits sidecar JSON (`<name>.a2ui.json`) that feeds the docs site, the A2UI runtime registries, and consumer harnesses.
|
|
4
4
|
|
|
5
|
-
This is the authoritative schema reference for the authoring lane. The
|
|
5
|
+
This is the authoritative schema reference for the authoring lane. The JSON Schema lives at `scripts/schemas/component.yaml.schema.json` (referenced by every yaml's `$schema:` key). Amended 2026-08-16 per ADR-0057: that schema is the documented contract + IDE aid, not a run validator — no build step evaluates it against the yamls. The build-time checks that DO exist are hand-written throws in `compileComponent()` (`scripts/build/components.mjs`): a missing `component:` field, a `component: Surface` (reserved — the A2UI v1.0 implicit root container, SPEC REQ-011/gh#1353), a `status:` value outside the five-value enum (see §`status:` below), a missing `category:` field OR a `category:` value outside the twelve-value enum (see §`category:` below, ADR-0065), and malformed `a2ui.allowedParents`/`a2ui.allowedChildren` composition constraints (see §composition constraints below — plus a full-build cross-reference check that every referenced name is a real `component:` in the catalog). Every other schema constraint (`required: [name, tag, component, description]`, `minLength`, …) is IDE-visible only. This file covers the human-facing contract: what each field means, when to use which value, and the canonical shape of a complete yaml.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ $schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
|
14
14
|
name: UIMyComponent # Class name (PascalCase, UI-prefixed)
|
|
15
15
|
tag: my-component-ui # Custom element tag (kebab-case, -ui-suffixed)
|
|
16
16
|
component: MyComponent # Short component name (no UI- prefix)
|
|
17
|
-
category: form # Category —
|
|
17
|
+
category: form # Category — see §category field below (ADR-0065, twelve-value enum)
|
|
18
18
|
version: 1 # Schema version (always 1 for now)
|
|
19
19
|
status: stable # Stability tier — see §status field below
|
|
20
20
|
description: >-
|
|
@@ -26,13 +26,104 @@ props:
|
|
|
26
26
|
events:
|
|
27
27
|
… # Event schemas — fired by the component
|
|
28
28
|
slots:
|
|
29
|
-
… #
|
|
29
|
+
… # Consumer-fillable light-DOM insertion points — see §slots vs parts below
|
|
30
|
+
parts:
|
|
31
|
+
… # Template-owned anatomy — see §slots vs parts below
|
|
30
32
|
css-vars:
|
|
31
33
|
… # CSS custom properties the component reads
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
---
|
|
35
37
|
|
|
38
|
+
## `slots:` vs `parts:` — consumer-fillable vs template-owned anatomy (ADR-0067)
|
|
39
|
+
|
|
40
|
+
**Decision rule**: does an author (a human, or an LLM generating an A2UI
|
|
41
|
+
document) ever place their OWN content at this named span? If yes — even
|
|
42
|
+
with a stamped fallback when nothing is supplied — it's `slots:`. If the
|
|
43
|
+
component's own `render()`/template ALWAYS stamps it itself, from a prop or
|
|
44
|
+
attribute, and no author-supplied content is ever accepted there — it's
|
|
45
|
+
`parts:`. **Check element source, never the description prose alone**
|
|
46
|
+
(AGENTS.md: source wins) — a name that *sounds* internal
|
|
47
|
+
(`actions`, `text`, `leading`) can still be a real insertion point in a
|
|
48
|
+
given component; `table-toolbar.yaml`'s `actions` slot LOOKS stamped by
|
|
49
|
+
name but its `class.js` explicitly absorbs pre-existing `[slot="actions"]`
|
|
50
|
+
children (a real, author-fillable insertion point) — the opposite of
|
|
51
|
+
`check.yaml`'s `box`, which `static template = () => html\`<span
|
|
52
|
+
slot="box"></span>\`` stamps unconditionally every render.
|
|
53
|
+
|
|
54
|
+
Both keys share the identical `Slot` schema shape (`description:` required,
|
|
55
|
+
`fallback:` optional) — the only difference is which key an entry lives
|
|
56
|
+
under. `scripts/build/components.mjs` forwards both verbatim onto the
|
|
57
|
+
sidecar (`x-adiaui.slots` / `x-adiaui.parts`) with no other processing.
|
|
58
|
+
|
|
59
|
+
**Why the split matters — three real consumers read `slots:` and present
|
|
60
|
+
every entry as fillable, with no code-level filtering for anything under
|
|
61
|
+
`parts:`:**
|
|
62
|
+
|
|
63
|
+
- `packages/gen-ui/engine/retrieval/component-entry.js`'s
|
|
64
|
+
`serializeReference()` — feeds the LLM-facing `reference`-detail catalog
|
|
65
|
+
entry (MCP tools, `getComponentAPI()`).
|
|
66
|
+
- `packages/gen-ui/engine/compose/strategies/monolithic/_shared.js`'s
|
|
67
|
+
`adaptV09Component()` — feeds the monolithic engine's prompt catalog.
|
|
68
|
+
- `scripts/docs/anatomy-sweep.mjs`'s `genSlots()` — renders the docs-site
|
|
69
|
+
"slots" anatomy section.
|
|
70
|
+
|
|
71
|
+
A `parts:` entry never reaches any of the three above — moving template-owned
|
|
72
|
+
anatomy there is a structural fix, not a naming convention alone. An
|
|
73
|
+
existing entry under `slots:` that's actually template-owned (e.g. a
|
|
74
|
+
component predating this ADR) is a real bug: it advertises to an LLM that
|
|
75
|
+
filling it does something, when the component's own template replaces
|
|
76
|
+
whatever's there on the next render — the exact gh#284 destructive-replace
|
|
77
|
+
shape, applied to a *documented* slot instead of an undocumented one.
|
|
78
|
+
|
|
79
|
+
`scripts/dev/audit-slot-vocab-vs-css.mjs` (the yaml-vs-CSS `[slot="X"]`
|
|
80
|
+
cross-check) reads BOTH `slots:` and `parts:` — a `parts:` entry is still a
|
|
81
|
+
real `slot="X"` DOM attribute the component's own CSS may position, just
|
|
82
|
+
never author-fillable, so it stays in that audit's declared-vocabulary set.
|
|
83
|
+
`scripts/dev/audit-template-child-conflict.mjs` (the gh#284 container-shape
|
|
84
|
+
check) is unaffected either way — it only checks for a slot literally named
|
|
85
|
+
`default`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## `a2ui.allowedParents:` / `a2ui.allowedChildren:` — composition constraints (SPEC REQ-011, gh#1353)
|
|
90
|
+
|
|
91
|
+
Optional keys inside the `a2ui:` block, alongside `rules:`. Each is a
|
|
92
|
+
non-empty list of catalog `component:` names (NOT tags) naming the direct
|
|
93
|
+
parents this component may sit under / the direct children it may contain.
|
|
94
|
+
The reserved name `Surface` (the A2UI v1.0 implicit root container) is legal
|
|
95
|
+
only in `allowedParents` and means "may sit at the surface root". **Omitted
|
|
96
|
+
means unconstrained** — never write an empty list (that would mean "allowed
|
|
97
|
+
nowhere"; the build refuses it).
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
a2ui:
|
|
101
|
+
allowedParents:
|
|
102
|
+
- Accordion # AccordionItem only makes sense inside an Accordion
|
|
103
|
+
rules:
|
|
104
|
+
- …
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Authoring rule — verify against element source, exactly like the
|
|
108
|
+
`slots:`/`parts:` decision above.** Declare a constraint only when the
|
|
109
|
+
component's own source enforces or assumes it (e.g. `stepper.class.js`
|
|
110
|
+
queries `stepper-item-ui`; `segmented.class.js` warns on non-`segment-ui`
|
|
111
|
+
children). A parent that adopts items through wrappers (menu.class.js's
|
|
112
|
+
deliberate descendant query) must NOT constrain — a declared constraint
|
|
113
|
+
stricter than the source is a defect, not documentation.
|
|
114
|
+
|
|
115
|
+
Pipeline: `components.mjs` validates the shape per-yaml, cross-checks every
|
|
116
|
+
referenced name against the full catalog on a full build, and forwards the
|
|
117
|
+
lists onto `x-adiaui` → `catalog-a2ui_0_9.json`.
|
|
118
|
+
`scripts/build/derive-genui-catalog.mjs` then translates them into the
|
|
119
|
+
canonical v1.0 key space (yaml `Segmented` → catalog `SegmentedControl`) on
|
|
120
|
+
`base.json`/`adia-pack.json`, where the vendored `@genui/core` validator
|
|
121
|
+
enforces them (`UNALLOWED_PARENT`/`UNALLOWED_CHILD`). Module-tier yamls
|
|
122
|
+
(web-modules) carry constraints as dialect-catalog metadata only — modules
|
|
123
|
+
have no v1.0 sidecar.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
36
127
|
## `status:` field — stability tier
|
|
37
128
|
|
|
38
129
|
**Required** for all new components. Existing components default to `stable` if unset, but new yamls MUST set this explicitly.
|
|
@@ -45,6 +136,8 @@ css-vars:
|
|
|
45
136
|
| `deprecated` | Has a replacement; check the component's `related:` section. Docs site shows a `danger`-variant badge labeled "deprecated". |
|
|
46
137
|
| `early-access` | Customer-preview tier; release notes gate. Docs site shows an `info`-variant badge labeled "early access". |
|
|
47
138
|
|
|
139
|
+
**Ratified, closed enum — compiler-enforced at build time, mirrored in the schema (ADR-0057, ratified 2026-08-15).** The five values above are the whole vocabulary; `draft` is NOT a value (the one `draft` in the estate, `embed-shell.yaml`, was corrected to `experimental` when the enum went live — a sixth value on a single occurrence is data-entry drift, not a vocabulary gap). Enforcement: `scripts/build/components.mjs:105` holds `STATUS_VALUES` and `compileComponent()` (`components.mjs:258-260`) throws on any out-of-enum `status:` at the same place it throws on a missing `component:` — so `npm run verify:components` (`node scripts/build/components.mjs --verify`, a member of the `npm run check` aggregate) hard-fails the yaml with a file-and-value error. An invalid status no longer merely skips a docs badge; it stops the build. `scripts/schemas/component.yaml.schema.json:23-27` declares the same enum (default `stable`) for the `$schema:` IDE contract, but no validator runs that file — the hand-synced constant in `components.mjs` is the live gate. `status` is orthogonal to the ADR-0050 L0–L4 tier ladder (tier = what a component is composed of; status = how much to trust its contract today), and nothing in `packages/gen-ui/engine/retrieval/` filters or ranks on it. Source: ADR-0057.
|
|
140
|
+
|
|
48
141
|
**Guidance**:
|
|
49
142
|
|
|
50
143
|
- Set `beta` or `experimental` at FIRST AUTHORING for any component that's not in the stable API contract yet. Don't default to `stable` and bump later — the badge is consumer-facing, and stable→beta is a downgrade signal.
|
|
@@ -57,6 +150,33 @@ css-vars:
|
|
|
57
150
|
|
|
58
151
|
---
|
|
59
152
|
|
|
153
|
+
## `category:` field — functional grouping
|
|
154
|
+
|
|
155
|
+
**Required.** Every yaml sets this; `scripts/build/components.mjs` forwards it verbatim onto the sidecar as `x-adiaui.category`, and `packages/gen-ui/engine/retrieval/catalog.js` reads it from there for every YAML-backed component — no second, hand-maintained category list for anything with a yaml SoT. `catalog.js` does still carry one small, DELIBERATE exception: a 3-entry `PSEUDO_TYPE_CATEGORY` map (`section`/`header`/`footer` → `card-child`) for `@adia-ai/a2ui` registry pseudo-types that have no yaml SoT at all (v0.9 composition slot-children, not real primitives) — nothing to derive from, so this one small map stays hand-maintained by design, not drift.
|
|
156
|
+
|
|
157
|
+
| Value | When to use |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `action` | A standalone, click-to-fire trigger (`button-ui`, `toggle-scheme-ui`). |
|
|
160
|
+
| `agent` | AI/agent-facing surfaces — chat, trace, tool output, tabular/chart data views (`chat-thread-ui`, `agent-trace-ui`, `table-ui`, `chart-ui`, `embed-ui`). |
|
|
161
|
+
| `container` | A chrome/wrapping surface that holds other content (`card-ui`, `modal-ui`, `drawer-ui`, `menu-ui`, `command-ui`). |
|
|
162
|
+
| `data` | Structured/tabular data display, not a full agent surface (`tree-ui`, `heatmap-ui`). |
|
|
163
|
+
| `display` | Passive content rendering — text, media, status glyphs (`text-ui`, `icon-ui`, `badge-ui`, `avatar-ui`, `link-ui`, `mark-ui`, `richtext-ui` — a non-editable renderer, not a form field). |
|
|
164
|
+
| `feedback` | Status/notification/progress communication (`spinner-ui`, `inline-message-ui`, `progress-ui`, `progress-row-ui`, `step-progress-ui`, `feed-ui`, `feed-item-ui`). |
|
|
165
|
+
| `form` | Data-entry composite/field-level components, not raw bindable controls (`field-ui`, `fields-ui`, `rating-ui`, `toggle-option-ui`). |
|
|
166
|
+
| `input` | Bindable form controls (`input-ui`, `select-ui`, `check-ui`, `switch-ui`, `textarea-ui`, `radio-ui`). |
|
|
167
|
+
| `layout` | Pure structural/spatial primitives — no content semantics of their own (`row-ui`, `col-ui`, `grid-ui`, `stack-ui`, `list-ui`). |
|
|
168
|
+
| `navigation` | Wayfinding/switcher controls, including a switcher family's child items (`breadcrumb-ui`, `pagination-ui`, `menu-item-ui`, `segmented-ui`/`segment-ui`, `tabs-ui`/`tab-ui`, `stepper-ui`/`stepper-item-ui`). `toggle-group-ui` is `navigation` too, but its child `toggle-option-ui` is `form` (a wrapper/item split, like `menu-ui`/`menu-item-ui` — not a same-category pair). |
|
|
169
|
+
| `shells` | Page-level app-shell composites (`simple-shell-ui` and its siblings). |
|
|
170
|
+
| `utility` | Non-visual/accessibility helpers (`skip-nav-ui`, `visually-hidden-ui`). |
|
|
171
|
+
|
|
172
|
+
**Ratified, closed enum — compiler-enforced at build time, mirrored in the schema (ADR-0065).** These twelve values are the whole vocabulary; the census that ratified them found 18 free-form values in live use (typos like `forms`/`data-display`, one-off singletons, and three named misclassifications) — all folded or corrected onto this set as part of the same change. Enforcement: `scripts/build/components.mjs` holds `CATEGORY_VALUES` and `compileComponent()` throws on a MISSING `category:` field (unlike `status:`, `category:` is required, not defaulted) as well as on any out-of-enum value, the same place and severity as the `status:` check above — so `npm run verify:components` hard-fails an invalid OR absent category. `scripts/schemas/component.yaml.schema.json`'s `category` enum mirrors this list for the `$schema:` IDE contract; the hand-synced constant in `components.mjs` is the live gate, same relationship as `status`. Source: ADR-0065.
|
|
173
|
+
|
|
174
|
+
**A sibling family (a wrapper + its child items, e.g. `tabs-ui`/`tab-ui`) is not required to share one category by default** — `menu-ui` (`container`) + `menu-item-ui` (`navigation`) is a deliberate, working split. Where a family's sibling values disagreed with no evident rationale, ADR-0065 unified them; new families should pick per-component, not assume unification is required.
|
|
175
|
+
|
|
176
|
+
**Sidecar emission**: `x-adiaui.category` field in `<name>.a2ui.json`. `packages/gen-ui/engine/retrieval/catalog.js`'s `buildCatalog()` reads this directly per entry — no separate registration step.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
60
180
|
## `props:` field — prop schemas
|
|
61
181
|
|
|
62
182
|
Each prop is a top-level key inside `props:`. The full prop schema:
|
|
@@ -160,6 +280,46 @@ string-list schema is the exact silent-mismatch shape.
|
|
|
160
280
|
|
|
161
281
|
---
|
|
162
282
|
|
|
283
|
+
## `data-msg-*` — exempt component-side config family (gh#1332/#1464, ADR-0060)
|
|
284
|
+
|
|
285
|
+
`data-msg-required` / `data-msg-pattern` / `data-msg-minlength` /
|
|
286
|
+
`data-msg-maxlength` / `data-msg-min` / `data-msg-max` / `data-msg-bad-input`
|
|
287
|
+
are read directly by form-associated components (`core/form.js`'s shared
|
|
288
|
+
`UIFormElement` validation path, plus `input`, `select`, `tags-input`,
|
|
289
|
+
`code`, `date-range-picker`, `datetime-picker`, and
|
|
290
|
+
`payment-method-form.class.js`) to override a native constraint-violation's
|
|
291
|
+
default message with a consumer-supplied string.
|
|
292
|
+
|
|
293
|
+
**Disposition: EXEMPT — never declared as a yaml `states:`/`props:` entry.**
|
|
294
|
+
Both yaml surfaces this contract offers are the wrong shape for this family:
|
|
295
|
+
|
|
296
|
+
- `states:` declares **presence-boolean host state the component itself
|
|
297
|
+
reflects outward** (idle/loaded/error, this file's own §Reserved section's
|
|
298
|
+
neighbor pattern) — `data-msg-*` carries no state at all; it is a
|
|
299
|
+
consumer-authored string the component only ever *reads*, never sets.
|
|
300
|
+
- `props:` would need one string prop per validation-message key, repeated
|
|
301
|
+
across every one of the 7+ consuming components — but the read path is
|
|
302
|
+
`core/form.js`'s shared mixin, not any single component's own yaml SoT.
|
|
303
|
+
Declaring it per-component would multiply one shared mixin contract across
|
|
304
|
+
every consumer's yaml with no single owning SoT to declare it once — a
|
|
305
|
+
cross-cutting mixin-contract change, not a per-component yaml edit.
|
|
306
|
+
|
|
307
|
+
ADR-0060's own boundary discriminator (§Decision 3) already places this
|
|
308
|
+
family outside the trait-tier `data-*` ratification and explicitly routes it
|
|
309
|
+
"to gh#1332's Category A/D triage for its own converge-or-ratify call" — this
|
|
310
|
+
section IS that call. The family stays `data-*`, undeclared in any yaml,
|
|
311
|
+
with its contract documented at the shared source instead: `core/form.js`'s
|
|
312
|
+
own header JSDoc (the mixin all consumers share) and the canonical
|
|
313
|
+
`form-system` pattern doc (`packages/web-components/patterns/form-system/
|
|
314
|
+
form-system.examples.html`, mirrored at `site-a2ui/pages/
|
|
315
|
+
site__patterns__form-system.a2ui.json`) — both already enumerate the full
|
|
316
|
+
family with a worked example. A future architectural pass that wants to
|
|
317
|
+
promote this to a declared per-component contract needs its own ADR (the
|
|
318
|
+
scope is a mixin-wide contract change, not a small-ticket edit); nothing
|
|
319
|
+
here forecloses that, it only records today's call.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
163
323
|
## Build pipeline
|
|
164
324
|
|
|
165
325
|
```bash
|
|
@@ -171,7 +331,7 @@ node scripts/build/components.mjs --verify # same as above, direct invocation
|
|
|
171
331
|
The build:
|
|
172
332
|
|
|
173
333
|
1. Reads every `<name>.yaml` under `packages/web-components/components/` and `packages/web-modules/<cluster>/`
|
|
174
|
-
2.
|
|
334
|
+
2. Hand-checks the source yaml in `compileComponent()` — missing `component:` and out-of-enum `status:` both throw (`components.mjs:255-260`). Amended 2026-08-16 per ADR-0057: it does NOT run `scripts/schemas/component.yaml.schema.json` as a validator (an earlier revision of this list claimed it did); the schema file is documentation + IDE contract only.
|
|
175
335
|
3. Emits `<name>.a2ui.json` (the sidecar) co-located with the yaml + js + css
|
|
176
336
|
4. Emits the `traits/_catalog.json` aggregate
|
|
177
337
|
5. `--verify` mode: re-runs steps 1-4 in-memory and fails if any sidecar drifts from disk content (CI hard-fail)
|
|
@@ -263,4 +423,4 @@ After the playbook, the component is consumable by the docs site, the A2UI runti
|
|
|
263
423
|
- [css-patterns.md](css-patterns.md) — light-DOM CSS cascade rules
|
|
264
424
|
- [api-contract.md](api-contract.md) — props/events/slots conventions
|
|
265
425
|
- [authoring-cycle.md](authoring-cycle.md) — the 5-step authoring procedure
|
|
266
|
-
- `scripts/schemas/component.yaml.schema.json` — JSON Schema (
|
|
426
|
+
- `scripts/schemas/component.yaml.schema.json` — JSON Schema (documented contract + IDE aid; not run as a validator — `compileComponent()` enforces only `component:` and the `status` enum, see §Build pipeline)
|
|
@@ -3,9 +3,10 @@ name: ssr-compatibility
|
|
|
3
3
|
description: >-
|
|
4
4
|
Answers why an AdiaUI component crashes, drops content, or renders wrong
|
|
5
5
|
under SSR (linkedom/Astro consumers) — the four known failure shapes,
|
|
6
|
-
what's fixed vs open, how to
|
|
6
|
+
what's fixed vs open, how to prove a fix under the linkedom shim gate. Use when
|
|
7
7
|
asked "does this work under SSR", why a component crashes on
|
|
8
|
-
attachInternals/ResizeObserver/adoptedStyleSheets
|
|
8
|
+
attachInternals/ResizeObserver/adoptedStyleSheets/matchMedia/`instanceof Node`
|
|
9
|
+
under a DOM shim, why
|
|
9
10
|
table-ui/chart-ui/select-ui or a container CE renders empty or loses its
|
|
10
11
|
nested children when server-rendered, whether it's safe to call
|
|
11
12
|
getBoundingClientRect() synchronously in connectedCallback, or whether a
|
|
@@ -64,10 +65,10 @@ Full symptom → root-cause → status detail, cited to the actual shipped/open
|
|
|
64
65
|
| Ask | Answer from |
|
|
65
66
|
| --- | --- |
|
|
66
67
|
| "why does `<text-ui>`/`<avatar-ui>`/a container lose its content under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §2 — shape 2, NARROWED (doesn't currently reproduce against any shipped component; see the survey before assuming a new report fits this shape) |
|
|
67
|
-
| "why does this crash / throw at construction under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §1 — shape 1, FIXED
|
|
68
|
+
| "why does this crash / throw at import, construction or connect under SSR" | [`failure-shapes.md`](references/failure-shapes.md) §1 — shape 1, FIXED twice (gh#285, gh#1430/#1436) and now GATED by `scripts/dev/ssr-linkedom-smoke.mjs`; the exact guard shape to copy for a NEW instance is [`guard-patterns.md`](references/guard-patterns.md) §1 — feature-detect the API, never `typeof window` |
|
|
68
69
|
| "is this connect-time `getBoundingClientRect()`/measurement read safe" | [`failure-shapes.md`](references/failure-shapes.md) §3 + [`guard-patterns.md`](references/guard-patterns.md) §3 — the fixed component's exact shape, and the "unknown ≠ confirmed" principle to apply elsewhere |
|
|
69
70
|
| "what's fixed vs still open for SSR support" | [`status-ledger.md`](references/status-ledger.md) — re-verify against `gh issue view` before trusting it, it drifts |
|
|
70
|
-
| "how do I test an SSR gap
|
|
71
|
+
| "how do I test / prove an SSR gap or fix" | [`test-without-linkedom.md`](references/test-without-linkedom.md) — run the linkedom shim gate first (`node scripts/dev/ssr-linkedom-smoke.mjs`, the consumer's exact global surface), then the unit-level delete/try/finally pattern and what it does NOT prove |
|
|
71
72
|
| "what's the consumer's current workaround, and can they drop it yet" | [`consumer-workarounds.md`](references/consumer-workarounds.md) |
|
|
72
73
|
| "table/chart/select renders empty in the SSR response" | [`failure-shapes.md`](references/failure-shapes.md) §4 — shape 5, CLOSED (table-ui's `data="[…]"` attribute); check whether the reporting component is registered server-side first if it still reproduces |
|
|
73
74
|
|
|
@@ -76,7 +77,11 @@ Full symptom → root-cause → status detail, cited to the actual shipped/open
|
|
|
76
77
|
Every fix pattern this pack cites ([`guard-patterns.md`](references/guard-patterns.md))
|
|
77
78
|
carries the reasoning for why it looks the way it does — the no-op `ElementInternals`
|
|
78
79
|
shim exists because leaving the field `undefined` would relocate a crash, not remove
|
|
79
|
-
it;
|
|
80
|
+
it; the linkedom shim gate (`scripts/dev/ssr-linkedom-smoke.mjs`, gh#1430) exists
|
|
81
|
+
because happy-dom implements every API in the documented shape-1 cases that linkedom
|
|
82
|
+
lacks (matchMedia, rAF, the Observers, `Node`/`Element` globals, rect APIs), so the
|
|
83
|
+
unit suite is blind to that class by construction — deletion-based testing is the unit-level complement, not
|
|
84
|
+
the proof.
|
|
80
85
|
If a new case doesn't fit an existing pattern's reasoning, that's a signal to design a
|
|
81
86
|
new pattern. Route it through `primitive-authoring` — don't force-fit the nearest existing
|
|
82
87
|
shape.
|
|
@@ -21,12 +21,20 @@ and force-assigned `document.adoptedStyleSheets = []`. Two named fragilities:
|
|
|
21
21
|
`globalThis.ResizeObserver` affects EVERY component and every other library in the
|
|
22
22
|
same process, not just AdiaUI's.
|
|
23
23
|
|
|
24
|
-
**Status as of gh#285's fix (PR #292, merged 2026-07-17):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
**Status as of gh#285's fix (PR #292, merged 2026-07-17): the AdiaUI-facing sections
|
|
25
|
+
of this shim are no longer necessary** — the framework guards its own call sites now.
|
|
26
|
+
**Consumer-confirmed 2026-08-17 (gh#1430 §Additional context, against 0.8.40):**
|
|
27
|
+
`attachInternals`, the four Observer stubs and `adoptedStyleSheets` are all confirmed
|
|
28
|
+
unnecessary. **But gh#285's closing comment over-reached** in saying the shim "as a
|
|
29
|
+
whole" can go: two of its sections patch `CustomElementRender.prototype` — a
|
|
30
|
+
`setAttribute` null-guard and a `renderShadow` null-guard — i.e. they patch
|
|
31
|
+
`custom-elements-ssr` itself, which reads `shadowRoot.innerHTML` unconditionally and
|
|
32
|
+
AdiaUI is light-DOM-only, so `shadowRoot` is always `null`. Remove those two and every
|
|
33
|
+
SSR'd fixture dies with `Cannot read properties of null (reading 'innerHTML')`. No fix
|
|
34
|
+
in this repo can retire them; they belong to the renderer. Tell the next consumer that
|
|
35
|
+
explicitly rather than "the shim is unnecessary". (Also from that trial: `matchMedia`
|
|
36
|
+
was a further AdiaUI-side gap the #292 sweep missed — fixed by gh#1430, so a
|
|
37
|
+
`matchMedia` stub is not needed either from the version carrying it.)
|
|
30
38
|
|
|
31
39
|
## Attribute-only SSR registration restriction (gh#284 — likely no longer necessary, unconfirmed)
|
|
32
40
|
|