@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.
Files changed (32) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/package.json +1 -1
  4. package/scripts/release-pretag-docs-gate +1 -1
  5. package/skills/a2ui-maintenance/SKILL.md +26 -6
  6. package/skills/a2ui-maintenance/references/anti-patterns.md +1 -1
  7. package/skills/a2ui-maintenance/references/chunk-authoring.md +2 -2
  8. package/skills/a2ui-maintenance/references/eval-diagnostics.md +1 -1
  9. package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +2 -2
  10. package/skills/a2ui-maintenance/references/pipeline-overview.md +2 -2
  11. package/skills/demo-audit/scripts/analyze.mjs +1 -1
  12. package/skills/package-release/SKILL.md +1 -1
  13. package/skills/package-release/references/cut-procedure.md +14 -4
  14. package/skills/package-release/references/gates-catalog.md +7 -1
  15. package/skills/package-release/scripts/gate-roster.mjs +28 -1
  16. package/skills/package-release/scripts/release-pack.mjs +43 -4
  17. package/skills/primitive-authoring/SKILL.md +1 -0
  18. package/skills/primitive-authoring/references/INDEX.md +2 -1
  19. package/skills/primitive-authoring/references/anti-patterns.md +27 -0
  20. package/skills/primitive-authoring/references/api-contract.md +39 -0
  21. package/skills/primitive-authoring/references/composite-demo-protocol.md +1 -1
  22. package/skills/primitive-authoring/references/css-patterns.md +7 -1
  23. package/skills/primitive-authoring/references/shell-patterns.md +1 -1
  24. package/skills/primitive-authoring/references/svg-authoring.md +277 -0
  25. package/skills/primitive-authoring/references/token-contract.md +1 -1
  26. package/skills/primitive-authoring/references/yaml-contract.md +165 -5
  27. package/skills/ssr-compatibility/SKILL.md +10 -5
  28. package/skills/ssr-compatibility/references/consumer-workarounds.md +14 -6
  29. package/skills/ssr-compatibility/references/failure-shapes.md +30 -7
  30. package/skills/ssr-compatibility/references/guard-patterns.md +28 -0
  31. package/skills/ssr-compatibility/references/status-ledger.md +13 -0
  32. 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 full validator + JSON Schema lives at `scripts/schemas/component.yaml.schema.json` (referenced by every yaml's `$schema:` key). This file covers the human-facing contract: what each field means, when to use which value, and the canonical shape of a complete yaml.
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 — form / display / layout / chrome / a2ui / shell
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
- … # Named slot semantics
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. Validates against `scripts/schemas/component.yaml.schema.json`
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 (authoritative)
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 test without a linkedom install. Use when
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 under a DOM shim, why
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; the exact guard shape to copy for a NEW instance is [`guard-patterns.md`](references/guard-patterns.md) §1 |
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 we have no linkedom installed" | [`test-without-linkedom.md`](references/test-without-linkedom.md) — the delete/try/finally pattern, and what it does NOT prove |
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; deletion-based testing exists because this repo has no `linkedom` devDependency.
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): this shim should no longer
25
- be necessary** — the framework guards its own call sites now. This pack's own routing
26
- corpus / the issue thread is where confirmation from the consumer would land; don't
27
- assume it's been removed without checking the issue's comment thread for that
28
- confirmation, since a consumer removing a workaround is THEIR change, not something
29
- this fix does automatically.
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