@adia-ai/adia-ui-forge 0.8.40 → 0.8.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/scripts/release-pretag-docs-gate +1 -1
- package/skills/a2ui-maintenance/SKILL.md +43 -6
- package/skills/a2ui-maintenance/references/anti-patterns.md +1 -1
- package/skills/a2ui-maintenance/references/chunk-authoring.md +2 -2
- package/skills/a2ui-maintenance/references/eval-diagnostics.md +1 -1
- package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +2 -2
- package/skills/a2ui-maintenance/references/pipeline-overview.md +2 -2
- package/skills/a2ui-maintenance/references/surface-lifecycle.md +73 -0
- package/skills/demo-audit/references/admin-shell-anatomy.md +1 -1
- package/skills/demo-audit/scripts/analyze.mjs +1 -1
- package/skills/package-release/SKILL.md +1 -1
- package/skills/package-release/references/cut-procedure.md +14 -4
- package/skills/package-release/references/gates-catalog.md +7 -1
- package/skills/package-release/scripts/gate-roster.mjs +28 -1
- package/skills/package-release/scripts/release-pack.mjs +43 -4
- package/skills/primitive-authoring/SKILL.md +1 -0
- package/skills/primitive-authoring/references/INDEX.md +2 -1
- package/skills/primitive-authoring/references/anti-patterns.md +27 -0
- package/skills/primitive-authoring/references/api-contract.md +39 -0
- package/skills/primitive-authoring/references/composite-demo-protocol.md +1 -1
- package/skills/primitive-authoring/references/css-patterns.md +7 -1
- package/skills/primitive-authoring/references/shell-patterns.md +1 -1
- package/skills/primitive-authoring/references/svg-authoring.md +282 -0
- package/skills/primitive-authoring/references/token-contract.md +47 -2
- package/skills/primitive-authoring/references/yaml-contract.md +239 -6
- package/skills/site-deployment/references/deploy-playbooks.md +12 -4
- package/skills/ssr-compatibility/SKILL.md +10 -5
- package/skills/ssr-compatibility/references/consumer-workarounds.md +14 -6
- package/skills/ssr-compatibility/references/failure-shapes.md +30 -7
- package/skills/ssr-compatibility/references/guard-patterns.md +28 -0
- package/skills/ssr-compatibility/references/status-ledger.md +13 -0
- package/skills/ssr-compatibility/references/test-without-linkedom.md +29 -14
|
@@ -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,106 @@ 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
|
+
|
|
180
|
+
## Semantic color-family axis — two role-classes, two names (ADR-0044, ADR-0064)
|
|
181
|
+
|
|
182
|
+
Every component carries at most one STYLE axis and one FAMILY axis (the
|
|
183
|
+
semantic color family: `default | info | success | warning | danger`,
|
|
184
|
+
`+primary` where the role-class already carries brand emphasis), and every
|
|
185
|
+
enum value belongs to exactly one axis. The family axis's ATTRIBUTE NAME is
|
|
186
|
+
decided by role-class — never one universal name (a same-name meaning-flip
|
|
187
|
+
is a silent-failure migration and poisons the trained corpus, per ADR-0044
|
|
188
|
+
LLD §3):
|
|
189
|
+
|
|
190
|
+
- **Role-class A — `variant` is unclaimed:** the family axis is named
|
|
191
|
+
`variant`. Badge/tag's ratified shape (ADR-0044), plus `rating-ui` and the
|
|
192
|
+
`variant`-only siblings (inline-message, feed-item, empty-state, menu-item,
|
|
193
|
+
progress-row).
|
|
194
|
+
- **Role-class B — an existing identity/style axis already claims the
|
|
195
|
+
selector slot:** the family axis is named `color`. Button's shape
|
|
196
|
+
(`variant`=style, `color`=family), extended by ADR-0064 to `text-ui`
|
|
197
|
+
(`variant`=typography role), `chart-ui`/`heatmap-ui` (`type`=kind),
|
|
198
|
+
`icon-ui` (`weight`=glyph style), `spinner-ui` (`variant`=animation), and
|
|
199
|
+
`toggle-scheme-ui`. Renames owed by this ruling (follow-on build, gh#1376
|
|
200
|
+
— not yet landed): `icon-ui[tone]` → `[color]`, `heatmap-ui[colorScheme]`
|
|
201
|
+
→ `[color]`, `spinner-ui[tone]` → `[color]`.
|
|
202
|
+
|
|
203
|
+
`accent` is RETIRED from the family enum everywhere — ADR-0044 removed it
|
|
204
|
+
from badge/tag/button; ADR-0064 removes it from the seven stragglers
|
|
205
|
+
(text, chart, icon, heatmap, rating, spinner, toggle-scheme) with no
|
|
206
|
+
replacement value. Never mint `accent` in a new enum.
|
|
207
|
+
|
|
208
|
+
Two ratified non-family exceptions — the name without the semantics:
|
|
209
|
+
|
|
210
|
+
- `swatch-ui[color]` / `noodles-ui[color]` — an arbitrary CSS color string,
|
|
211
|
+
not a semantic enum (ADR-0054 §11 exemption, unchanged).
|
|
212
|
+
- `spinner-ui[color]` (post-rename) — a closed contrast-mode enum
|
|
213
|
+
(`current | subtle | inverse`), NOT the family vocabulary; never assume it
|
|
214
|
+
accepts `info`/`success`/`warning`/`danger` by analogy.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Catalog tiers — L0–L4, `origin`, and the promotion rule (ADR-0050, ADR-0066)
|
|
219
|
+
|
|
220
|
+
ADR-0050's L0–L4 ladder is the ONLY tier grammar — never mint a second
|
|
221
|
+
manifest format. ADR-0066 refines it three ways:
|
|
222
|
+
|
|
223
|
+
- **`origin: primitive | module` on every L0 entry** — both YAML source
|
|
224
|
+
roots (web-components primitives AND web-modules composites) compile into
|
|
225
|
+
the same `catalog-a2ui_0_9.json` through one shared contract;
|
|
226
|
+
`derive-catalog-tiers.mjs` stamps which root an entry came from onto
|
|
227
|
+
`tier-index.json`. A module is legitimately a member of TWO rungs at two
|
|
228
|
+
grains: its component API (props/events/slots) is L0, its assembled shell
|
|
229
|
+
composition is L3 — ruled correct, not a modeling defect.
|
|
230
|
+
- **The promotion rule, stated once:** patterns/zettel compositions are the
|
|
231
|
+
SOLE promotion source, and they enter the ladder at exactly one point —
|
|
232
|
+
pattern → L1 widget, through `curate-l1-widgets.mjs`'s gates (which writes
|
|
233
|
+
only `l1-widgets.json`). The higher rungs (L1 → L2 → L3 → L4) are AUTHORED
|
|
234
|
+
edges — hand-written `tiers/l*-*.json` manifests whose `composes` reference
|
|
235
|
+
the rung below, reserved/unblocked per ADR-0050's own phasing; a module's
|
|
236
|
+
L3 membership comes from an authored L3 manifest, never from its yaml
|
|
237
|
+
(which contributes only the L0 entry + `origin`). There is no
|
|
238
|
+
patterns↔module edge, primitives and modules never "promote" into each
|
|
239
|
+
other, and nothing promotes automatically or in reverse — curation is the
|
|
240
|
+
one-way valve (ADR-0050: "corpus derives from catalogs, never the
|
|
241
|
+
reverse").
|
|
242
|
+
- **The two pattern-facing outputs stay separate by design:**
|
|
243
|
+
`site/patterns-index.json` / `pattern-index.md` are a generated,
|
|
244
|
+
developer-facing index over the FULL pattern/template census, intentionally
|
|
245
|
+
independent of the L0–L4 machine-validated schema — a different audience,
|
|
246
|
+
never a convergence gap to "fix".
|
|
247
|
+
|
|
248
|
+
(`status:` above is orthogonal to the tier ladder — tier = what an entry is
|
|
249
|
+
composed of; status = how much to trust its contract today.)
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
60
253
|
## `props:` field — prop schemas
|
|
61
254
|
|
|
62
255
|
Each prop is a top-level key inside `props:`. The full prop schema:
|
|
@@ -146,7 +339,7 @@ string-list schema is the exact silent-mismatch shape.
|
|
|
146
339
|
| --- | --- | --- |
|
|
147
340
|
| `[data-col]` | `display: flex; flex-direction: column; gap: var(--page-grid-gap)` | column layout helper |
|
|
148
341
|
| `[data-row]` | `display: flex; align-items: center; gap: var(--page-grid-gap)` | row layout helper |
|
|
149
|
-
| `[data-grid]` | `display: grid; grid-template-columns: 1fr 1fr` (or `1fr 1fr 1fr` for `data-grid="3"`) | 2- or 3-col grid helper |
|
|
342
|
+
| `[data-layout-grid]` | `display: grid; grid-template-columns: 1fr 1fr` (or `1fr 1fr 1fr` for `data-layout-grid="3"`) | 2- or 3-col grid helper |
|
|
150
343
|
| `[data-actions]` | `display: flex; align-items: center; gap: var(--page-actions-gap)` | action button cluster |
|
|
151
344
|
| `[data-spacer]` | `flex: 1` | flex spacer for pushing content to edges |
|
|
152
345
|
|
|
@@ -160,6 +353,46 @@ string-list schema is the exact silent-mismatch shape.
|
|
|
160
353
|
|
|
161
354
|
---
|
|
162
355
|
|
|
356
|
+
## `data-msg-*` — exempt component-side config family (gh#1332/#1464, ADR-0060)
|
|
357
|
+
|
|
358
|
+
`data-msg-required` / `data-msg-pattern` / `data-msg-minlength` /
|
|
359
|
+
`data-msg-maxlength` / `data-msg-min` / `data-msg-max` / `data-msg-bad-input`
|
|
360
|
+
are read directly by form-associated components (`core/form.js`'s shared
|
|
361
|
+
`UIFormElement` validation path, plus `input`, `select`, `tags-input`,
|
|
362
|
+
`code`, `date-range-picker`, `datetime-picker`, and
|
|
363
|
+
`payment-method-form.class.js`) to override a native constraint-violation's
|
|
364
|
+
default message with a consumer-supplied string.
|
|
365
|
+
|
|
366
|
+
**Disposition: EXEMPT — never declared as a yaml `states:`/`props:` entry.**
|
|
367
|
+
Both yaml surfaces this contract offers are the wrong shape for this family:
|
|
368
|
+
|
|
369
|
+
- `states:` declares **presence-boolean host state the component itself
|
|
370
|
+
reflects outward** (idle/loaded/error, this file's own §Reserved section's
|
|
371
|
+
neighbor pattern) — `data-msg-*` carries no state at all; it is a
|
|
372
|
+
consumer-authored string the component only ever *reads*, never sets.
|
|
373
|
+
- `props:` would need one string prop per validation-message key, repeated
|
|
374
|
+
across every one of the 7+ consuming components — but the read path is
|
|
375
|
+
`core/form.js`'s shared mixin, not any single component's own yaml SoT.
|
|
376
|
+
Declaring it per-component would multiply one shared mixin contract across
|
|
377
|
+
every consumer's yaml with no single owning SoT to declare it once — a
|
|
378
|
+
cross-cutting mixin-contract change, not a per-component yaml edit.
|
|
379
|
+
|
|
380
|
+
ADR-0060's own boundary discriminator (§Decision 3) already places this
|
|
381
|
+
family outside the trait-tier `data-*` ratification and explicitly routes it
|
|
382
|
+
"to gh#1332's Category A/D triage for its own converge-or-ratify call" — this
|
|
383
|
+
section IS that call. The family stays `data-*`, undeclared in any yaml,
|
|
384
|
+
with its contract documented at the shared source instead: `core/form.js`'s
|
|
385
|
+
own header JSDoc (the mixin all consumers share) and the canonical
|
|
386
|
+
`form-system` pattern doc (`packages/web-components/patterns/form-system/
|
|
387
|
+
form-system.examples.html`, mirrored at `site-a2ui/pages/
|
|
388
|
+
site__patterns__form-system.a2ui.json`) — both already enumerate the full
|
|
389
|
+
family with a worked example. A future architectural pass that wants to
|
|
390
|
+
promote this to a declared per-component contract needs its own ADR (the
|
|
391
|
+
scope is a mixin-wide contract change, not a small-ticket edit); nothing
|
|
392
|
+
here forecloses that, it only records today's call.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
163
396
|
## Build pipeline
|
|
164
397
|
|
|
165
398
|
```bash
|
|
@@ -171,7 +404,7 @@ node scripts/build/components.mjs --verify # same as above, direct invocation
|
|
|
171
404
|
The build:
|
|
172
405
|
|
|
173
406
|
1. Reads every `<name>.yaml` under `packages/web-components/components/` and `packages/web-modules/<cluster>/`
|
|
174
|
-
2.
|
|
407
|
+
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
408
|
3. Emits `<name>.a2ui.json` (the sidecar) co-located with the yaml + js + css
|
|
176
409
|
4. Emits the `traits/_catalog.json` aggregate
|
|
177
410
|
5. `--verify` mode: re-runs steps 1-4 in-memory and fails if any sidecar drifts from disk content (CI hard-fail)
|
|
@@ -263,4 +496,4 @@ After the playbook, the component is consumable by the docs site, the A2UI runti
|
|
|
263
496
|
- [css-patterns.md](css-patterns.md) — light-DOM CSS cascade rules
|
|
264
497
|
- [api-contract.md](api-contract.md) — props/events/slots conventions
|
|
265
498
|
- [authoring-cycle.md](authoring-cycle.md) — the 5-step authoring procedure
|
|
266
|
-
- `scripts/schemas/component.yaml.schema.json` — JSON Schema (
|
|
499
|
+
- `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)
|
|
@@ -139,10 +139,18 @@ itself is unavailable.
|
|
|
139
139
|
- **`build:site` copies packages but does NOT rebuild their dist bundles** — after
|
|
140
140
|
component `.css`/`.js` source changes, rebuild first (`npm run build -w
|
|
141
141
|
@adia-ai/llm`, then `npm run build:bundles`) or the deployed bundles are stale.
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
- **Package registration is one manifest, `scripts/build/site-package-registry.mjs`
|
|
143
|
+
(ADR-0062)** — every `@adia-ai/*` package the site ships is one registry entry, from
|
|
144
|
+
which the dist copy, BOTH importmaps (one shared `renderImportMapBlock()` — the
|
|
145
|
+
`site/index.html` copy is a generated artifact, `npm run build:site-importmap`),
|
|
146
|
+
the `dist/node_modules` symlinks, and the CI build order all derive. The old
|
|
147
|
+
failure class (per-package `copyX()` hand-edits; local Vite works, prod 404s —
|
|
148
|
+
v0.3.0 llm/a2ui-runtime, v0.8.27 persona+agent) is gated mechanically:
|
|
149
|
+
`check:site-packages-registered` scans every shipped source root for an
|
|
150
|
+
unregistered bare `@adia-ai/*` import and fails naming file:line, alongside
|
|
151
|
+
`check:site-importmap-fresh` and `check:deploy-workflow-build-order` — all three
|
|
152
|
+
in `npm run check` and early in `deploy-site.yml`. After any package add/rename,
|
|
153
|
+
add the registry entry; the gates say the rest.
|
|
146
154
|
2. **Dry-run first, and adjudicate every delete — BEFORE the real rsync, never after:**
|
|
147
155
|
```sh
|
|
148
156
|
rsync -azni --delete --exclude='packages/gen-ui/a2ui/corpus/feedback/' \
|
|
@@ -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
|
|
|
@@ -19,14 +19,37 @@ or a write to `document.adoptedStyleSheets`.
|
|
|
19
19
|
not a partial/quirky implementation, an absence. Any unconditional call throws
|
|
20
20
|
`TypeError` or `ReferenceError` (undefined global) immediately.
|
|
21
21
|
|
|
22
|
-
**Status: FIXED
|
|
23
|
-
(`packages/web-components/core/element.js`)
|
|
24
|
-
|
|
22
|
+
**Status: FIXED, twice — and now GATED.** First wave gh#285 (PR #292, merged
|
|
23
|
+
2026-07-17): `UIElement`'s constructor (`packages/web-components/core/element.js`)
|
|
24
|
+
plus a sweep of component/trait/module files — the per-file tally is
|
|
25
25
|
[`status-ledger.md`](status-ledger.md)'s #285 row (the ledger, not this line, is
|
|
26
|
-
the count of record).
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
the count of record). Second wave gh#1430 + gh#1436 (2026-08-17, found by adiav2's
|
|
27
|
+
first SSR admission trial at 0.8.40): the sweep had missed
|
|
28
|
+
- **environment-detection instead of feature-detection at MODULE scope** —
|
|
29
|
+
`core/responsive.js` guarded `window.matchMedia(...)` behind `typeof window !==
|
|
30
|
+
'undefined'`; linkedom HAS a `window`, just no `matchMedia` and no numeric
|
|
31
|
+
`innerWidth`, so six components (`block`/`col`/`grid`/`row`/`text`/`demo-toggle`)
|
|
32
|
+
could not be IMPORTED server-side at all;
|
|
33
|
+
- **bare `instanceof Node|Element|HTMLElement`** (15 sites — `core/template.js`
|
|
34
|
+
`applyValue`, table/list-window renderer results, event-target checks) — a
|
|
35
|
+
`ReferenceError`, not `false`, because custom-elements-ssr installs ONLY
|
|
36
|
+
`HTMLElement` as a global (never `Node`, `Element`, `Text`, `DocumentFragment`);
|
|
37
|
+
now `core/dom.js` `isNode()`/`isElement()` (structural `nodeType` tests);
|
|
38
|
+
- **`requestAnimationFrame` / `MutationObserver` / `getBoundingClientRect` at
|
|
39
|
+
connect** in feed-item/toast, noodles, preview, toolbar (rAF), nav-group,
|
|
40
|
+
stepper (MutationObserver — the #292 sweep guarded four Observer sites, not
|
|
41
|
+
these two) and nav-ui (`getBoundingClientRect` is absent, not zero, on linkedom).
|
|
42
|
+
|
|
43
|
+
**Why happy-dom could not see any of it:** it implements every one of those APIs.
|
|
44
|
+
The gate that closes the class is `scripts/dev/ssr-linkedom-smoke.mjs`, run by
|
|
45
|
+
`packages/web-components/test/ssr-linkedom-smoke.test.js` — a real `linkedom`
|
|
46
|
+
devDependency installing EXACTLY the six globals `custom-elements-ssr/server-shim.js`
|
|
47
|
+
installs, then importing every `components/*/*.js` entry and constructing +
|
|
48
|
+
connecting every registered tag (124/124 import, every tag renders). A new
|
|
49
|
+
shape-1 instance fails that test, not a consumer's build. The fix pattern
|
|
50
|
+
(feature-detect + fallback matched to how the reference is used downstream) is
|
|
51
|
+
[`guard-patterns.md`](guard-patterns.md) — apply it to any NEW call site; don't
|
|
52
|
+
re-derive the shape from scratch, and don't guard on `typeof window`.
|
|
30
53
|
|
|
31
54
|
## 2 · `connectedCallback` destructively re-stamps existing DOM → content loss
|
|
32
55
|
|
|
@@ -51,6 +51,34 @@ const observer = typeof MutationObserver !== 'undefined'
|
|
|
51
51
|
// every later use: observer?.observe(...) / bootstrap() no-ops via the same check
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
**(d) Feature, not environment — `typeof window` is NOT a guard (gh#1430).**
|
|
55
|
+
linkedom defines `window` (an object) but neither `matchMedia` nor a numeric
|
|
56
|
+
`innerWidth`, so `typeof window !== 'undefined'` passes and the call still throws —
|
|
57
|
+
at MODULE scope that takes every importer off the server. Test the member:
|
|
58
|
+
```js
|
|
59
|
+
// packages/web-components/core/responsive.js
|
|
60
|
+
const w = typeof window !== 'undefined' ? window.innerWidth : undefined;
|
|
61
|
+
if (typeof w !== 'number') return 'lg'; // fallback
|
|
62
|
+
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { … }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**(e) `instanceof <DOM constructor>` — use `core/dom.js` (gh#1436).** Only
|
|
66
|
+
`HTMLElement` is a global under custom-elements-ssr; `v instanceof Node` /
|
|
67
|
+
`Element` / `Text` / `DocumentFragment` is a `ReferenceError`, not `false`.
|
|
68
|
+
`isNode(v)` / `isElement(v)` test `nodeType` structurally (and also accept nodes
|
|
69
|
+
from another realm, which the constructor identity check never did):
|
|
70
|
+
```js
|
|
71
|
+
import { isNode, isElement } from '../../core/dom.js';
|
|
72
|
+
if (isNode(result)) cell.replaceChildren(result); // renderer output
|
|
73
|
+
const row = isElement(e.target) ? e.target.closest('…') : null; // event target
|
|
74
|
+
```
|
|
75
|
+
`isElement` is deliberately the SUPERSET of a former `instanceof HTMLElement` (it
|
|
76
|
+
admits SVG elements too) — every swept site only needed "an element", none needed
|
|
77
|
+
"an HTML element specifically". Absent-method form of the same idea:
|
|
78
|
+
`typeof this.getBoundingClientRect === 'function' && …` (nav-ui) — linkedom has no
|
|
79
|
+
rect API at all, so an unguarded call is a TypeError, not a zero rect (that zero-rect
|
|
80
|
+
case is §3).
|
|
81
|
+
|
|
54
82
|
**The `this.internals` special case — a shim, not `undefined`.** `UIElement`'s
|
|
55
83
|
constructor (`packages/web-components/core/element.js`) needed a DIFFERENT answer
|
|
56
84
|
than "guard and leave unset," because ~12 files across the framework call
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# SSR support — status ledger
|
|
2
2
|
|
|
3
|
+
**Filing a new downstream SSR-seam report?** Use the `ssr` label plus
|
|
4
|
+
[`.github/ISSUE_TEMPLATE/ssr-seam-report.md`](../../../../../../.github/ISSUE_TEMPLATE/ssr-seam-report.md)
|
|
5
|
+
(gh#1384) — it captures the resolved `@adia-ai/web-components` version, the
|
|
6
|
+
downstream kit version, the component + fixture, an SSR-vs-CSR diff or
|
|
7
|
+
minimal repro, and a cosmetic/behavioral/blocked-upstream classification, so
|
|
8
|
+
a report lands triageable instead of free-form (the archaeology the #284/#288
|
|
9
|
+
rows below needed). `docs/ops/spec/spec-ssr-kit.md` is referenced by some
|
|
10
|
+
downstream reports but does not exist in this repo — it lives in adiav2's own
|
|
11
|
+
repo (`@adiav2/ssr-kit`'s spec); do not look for it here.
|
|
12
|
+
|
|
3
13
|
**This file drifts fast — re-verify against the live issue tracker
|
|
4
14
|
(`gh issue view <n>`) before trusting it for anything beyond a quick orientation.**
|
|
5
15
|
Every row was true as of 2026-07-17; a closed issue below may have re-opened, a PR
|
|
@@ -9,6 +19,9 @@ the territory — the territory is `gh issue list` / `gh issue view`.
|
|
|
9
19
|
| Issue | Failure shape | Status | Shipped in | Notes |
|
|
10
20
|
|---|---|---|---|---|
|
|
11
21
|
| [#285](https://github.com/adiahealth/gen-ui-kit/issues/285) | §1 — browser-only API called unconditionally | **CLOSED** | PR #292 (merged 2026-07-17) | `UIElement` constructor + `adoptStyles()` + 22 component/trait/module files guarded; 10 more already correct from an earlier pass |
|
|
22
|
+
| [#1430](https://github.com/adiahealth/gen-ui-kit/issues/1430) | §1 — browser-only API called unconditionally (module scope: `window.matchMedia` behind a `typeof window` guard) | **FIXED 2026-08-17** | `core/responsive.js` feature-detects `matchMedia` + `innerWidth`; `swiper.class.js` `play()` guarded; `scripts/dev/ssr-linkedom-smoke.mjs` + `test/ssr-linkedom-smoke.test.js` gate the whole catalog under linkedom | 6 components (`block`/`col`/`grid`/`row`/`text`/`demo-toggle`) were un-importable server-side; 118→124 of 124. Side findings on the issue: gh#285's "shim no longer necessary" over-reached (two of the consumer's shim sections patch `custom-elements-ssr` itself, not this library — see `consumer-workarounds.md`); `audit-template-child-conflict.mjs` isn't shipped in the package. |
|
|
23
|
+
| [#1436](https://github.com/adiahealth/gen-ui-kit/issues/1436) | §1 — bare `instanceof Node/Element/HTMLElement` (ReferenceError under linkedom, only `HTMLElement` is global) | **FIXED 2026-08-17** | `core/dom.js` `isNode()`/`isElement()`; 15 sites swept; plus rAF/MutationObserver/getBoundingClientRect guards in feed, noodles, preview, toolbar, nav-group, stepper, nav found by the same gate's render sweep | Same PR as #1430 — one root-cause class. |
|
|
24
|
+
| [#1435](https://github.com/adiahealth/gen-ui-kit/issues/1435) | reported as §2 — check/radio/switch "lose" a `<span slot="label">` child | **NOT an SSR seam** (2026-08-17) | yaml/a2ui `slots` descriptions clarified for the three (separate follow-up PR — a docs-contract change, not a shape-1 guard) | The fixture is not a supported form in CSR either: `label` on all three is ATTRIBUTE-driven (`label="…"` → `aria-label` + `::after content: attr(label)` / template-owned span); a late CSR upgrade replaces the foreign child the same way. The yaml `slots:` entries name template-OWNED anatomy parts (`box`/`dot`/`track`/`thumb`/`label`), which a downstream reimplementation of the audit read as consumer insertion points — the audit's own definition (`slots.default` + non-null template) is what "consumer children" means here. |
|
|
12
25
|
| [#286](https://github.com/adiahealth/gen-ui-kit/issues/286) | §3 — connect-time measurement treated as confirmed | **CLOSED** (for `admin-sidebar` specifically) | PR #290 (merged 2026-07-17) | The GENERAL pattern (any other component reading a rect/size synchronously at connect) is NOT swept — only this one instance is fixed |
|
|
13
26
|
| [#284](https://github.com/adiahealth/gen-ui-kit/issues/284) | §2 — destructive `stamp()` on connect | **CLOSED 2026-07-18 — narrowed AND separately fixed** | `scripts/dev/audit-template-child-conflict.mjs` (PR #295) + `packages/web-components/core/element.js` connectedCallback resync (PR #309) | Two distinct findings. (1) The ORIGINAL diagnosis (destructive `stamp()`) was narrowed 2026-07-17: every component the issue names has `static template = () => null`, so `stamp()` never runs for them — zero shipped components exposed; a static audit gate catches a future regression instead of a lifecycle rewrite. (2) A SEPARATE, real, live bug was found in the same investigation area and fixed 2026-07-18: happy-dom/linkedom don't replay `attributeChangedCallback` for attributes already present at custom-element upgrade (spec §4.13.5 step 6) — any `reflect: true` property seeded only from pre-parsed/SSR HTML stayed at its class default after upgrade (e.g. `<nav-item-ui text="Profile">` rendered with an empty label). `connectedCallback` now re-syncs every declared property from its live attribute before `connected()` runs. See `failure-shapes.md` §2 for the full narrative. |
|
|
14
27
|
| [#288](https://github.com/adiahealth/gen-ui-kit/issues/288) | §4 — property-only components can't seed from SSR HTML | **CLOSED 2026-07-18** | `table.class.js` `data="[…]"` attribute hydration | Never actually blocked on #284 (table-ui/chart-ui/select-ui all use `static template = () => null`, so the stamp() question never applied). Scope was narrower than filed: `select-ui` already parsed declarative `<option>` children, `chart-ui` already hydrated `.data` from a `data="[…]"` attribute — only `table-ui`'s `.data` had no declarative form. Fixed in the same attribute-hydration shape as chart-ui, not the JSON-script-child form originally proposed. |
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
# Testing an SSR gap
|
|
1
|
+
# Testing an SSR gap — the linkedom gate first, then the deletion pattern
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
**UPDATE 2026-08-17 (gh#1430/#1436): this repo now HAS a `linkedom` root
|
|
4
|
+
devDependency and a shipped shim gate.** `scripts/dev/ssr-linkedom-smoke.mjs`
|
|
5
|
+
installs exactly the six globals `custom-elements-ssr/server-shim.js` installs
|
|
6
|
+
(document, window, customElements, HTMLElement, Event, CustomEvent — and nothing
|
|
7
|
+
else: no Node/Element/Text/DocumentFragment, no matchMedia, no
|
|
8
|
+
requestAnimationFrame, no getBoundingClientRect), imports every
|
|
9
|
+
`components/*/*.js` entry, and constructs + connects every registered tag the way
|
|
10
|
+
`CustomElementRender` does; `packages/web-components/test/ssr-linkedom-smoke.test.js`
|
|
11
|
+
runs it as a Node child process and fails on any import or render throw. **For a
|
|
12
|
+
shape-1 question ("does this crash under SSR"), run that first** — `node
|
|
13
|
+
scripts/dev/ssr-linkedom-smoke.mjs` — it is the consumer's environment, not an
|
|
14
|
+
approximation. Add a row to its `FIXTURES` table when a guard lands on a branch only
|
|
15
|
+
some attribute value reaches (`swiper-ui[autoplay]` is the model). The rest of this
|
|
16
|
+
file is the UNIT-level pattern for a focused regression test in the happy-dom
|
|
17
|
+
suite; it stays valid, but it is no longer the only proof available.
|
|
18
|
+
|
|
19
|
+
The main test suite still runs on `happy-dom` (`vitest.config.js`), which
|
|
20
|
+
implements most of the APIs linkedom lacks — so a naive test using the default
|
|
21
|
+
environment will not reproduce an SSR-shaped bug. Two consequences:
|
|
8
22
|
|
|
9
23
|
1. **You cannot trust "the tests pass" as proof an SSR fix works** unless the test
|
|
10
24
|
itself removes the API under test. A guard around `attachInternals` that's never
|
|
@@ -118,12 +132,13 @@ actually present. Both gh#285 and gh#286's fixes were verified this second way b
|
|
|
118
132
|
shipping (a real `ElementInternals` instance still used and functional; a real
|
|
119
133
|
`ResizeObserver` tick still corrects state) — do both, never one instead of the other.
|
|
120
134
|
|
|
121
|
-
##
|
|
135
|
+
## The deeper simulation — DONE 2026-08-17
|
|
122
136
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
The real-`linkedom` simulation this section used to propose is now the shipped
|
|
138
|
+
gate described at the top of this file (`scripts/dev/ssr-linkedom-smoke.mjs`,
|
|
139
|
+
`linkedom` pinned as a root devDependency, used only there — the main suite stays
|
|
140
|
+
on happy-dom). It sweeps import + connect for the whole catalog; a shape-2/4
|
|
141
|
+
question (content preserved across a late upgrade, declarative data seeding) is
|
|
142
|
+
still answered by a targeted test — extend the script's `FIXTURES` table with the
|
|
143
|
+
attrs/children in question, or write the assertion in
|
|
144
|
+
`test/ssr-upgrade-adoption-contract.test.js`'s late-upgrade shape.
|