@adia-ai/adia-ui-forge 0.8.62 → 0.8.64

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 (143) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +3 -3
  3. package/CHANGELOG.md +317 -275
  4. package/README.md +54 -18
  5. package/__init__.py +8 -8
  6. package/agents/a2ui-maintenance-agent.corpus.json +7 -7
  7. package/agents/a2ui-maintenance-agent.md +6 -6
  8. package/agents/a2ui-planner-agent.md +11 -11
  9. package/agents/demo-audit-agent.corpus.json +4 -4
  10. package/agents/demo-audit-agent.md +8 -9
  11. package/agents/framework-checker.md +10 -11
  12. package/agents/framework-planner-agent.md +9 -10
  13. package/agents/gen-ui-review-agent.corpus.json +4 -4
  14. package/agents/gen-ui-review-agent.md +8 -8
  15. package/agents/package-release-agent.corpus.json +4 -4
  16. package/agents/package-release-agent.md +9 -10
  17. package/agents/primitive-authoring-agent.corpus.json +1 -1
  18. package/agents/primitive-authoring-agent.md +5 -5
  19. package/agents/routing-corpus.json +46 -46
  20. package/codex.interface.json +1 -1
  21. package/commands/gen-ui-review.md +1 -1
  22. package/commands/package-release.md +2 -2
  23. package/package.json +1 -1
  24. package/plugin.yaml +2 -2
  25. package/prompts/gen-ui-review.md +1 -1
  26. package/prompts/package-release.md +2 -2
  27. package/references/contracts/a2ui-mcp-surface.md +7 -7
  28. package/references/contracts/migration-guide-format.md +6 -7
  29. package/references/shared/content-trust.md +3 -5
  30. package/skills/a2ui-maintenance/SKILL.md +13 -16
  31. package/skills/a2ui-maintenance/evals/evals.json +2 -2
  32. package/skills/a2ui-maintenance/evals/routing-corpus.json +1 -1
  33. package/skills/a2ui-maintenance/references/anti-patterns.md +10 -10
  34. package/skills/a2ui-maintenance/references/chunk-authoring.md +11 -12
  35. package/skills/a2ui-maintenance/references/corpus-discipline.md +13 -14
  36. package/skills/a2ui-maintenance/references/data-model-reactivity.md +22 -25
  37. package/skills/a2ui-maintenance/references/eval-diagnostics.md +21 -21
  38. package/skills/a2ui-maintenance/references/format-extension-decisions.md +12 -13
  39. package/skills/a2ui-maintenance/references/leverage-rules.md +6 -6
  40. package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +9 -10
  41. package/skills/a2ui-maintenance/references/mcp-tool-reference.md +11 -12
  42. package/skills/a2ui-maintenance/references/pipeline-overview.md +33 -38
  43. package/skills/a2ui-maintenance/references/semantic-fail-lifting.md +13 -14
  44. package/skills/a2ui-maintenance/references/strategy-engines.md +14 -16
  45. package/skills/a2ui-maintenance/references/surface-lifecycle.md +10 -11
  46. package/skills/a2ui-maintenance/references/zettel-calibration.md +14 -16
  47. package/skills/component-md-authoring/SKILL.md +22 -22
  48. package/skills/component-md-authoring/agents/openai.yaml +1 -1
  49. package/skills/cross-harness-authoring-standards/SKILL.md +19 -21
  50. package/skills/cross-harness-authoring-standards/agents/openai.yaml +1 -1
  51. package/skills/cross-harness-compatibility-standards/SKILL.md +35 -34
  52. package/skills/cross-harness-compatibility-standards/agents/openai.yaml +1 -1
  53. package/skills/demo-audit/SKILL.md +38 -38
  54. package/skills/demo-audit/evals/routing-corpus.json +1 -1
  55. package/skills/demo-audit/references/admin-shell-anatomy.md +21 -25
  56. package/skills/demo-audit/references/app-shell-pitfalls.md +11 -13
  57. package/skills/demo-audit/references/auto-fix-allowlist.md +3 -3
  58. package/skills/demo-audit/references/card-anatomy-sweep.md +10 -11
  59. package/skills/demo-audit/references/chat-shell-anatomy.md +25 -26
  60. package/skills/demo-audit/references/editor-shell-anatomy.md +25 -28
  61. package/skills/demo-audit/references/html-attr-sweep.md +12 -13
  62. package/skills/demo-audit/references/mode7-status-battery.md +4 -4
  63. package/skills/demo-audit/references/native-leak-annotations.md +8 -8
  64. package/skills/demo-audit/references/output-contract-worked-example.md +8 -8
  65. package/skills/demo-audit/references/probe-discipline-and-escalation.md +2 -2
  66. package/skills/demo-audit/references/visual-probe-triage.md +21 -23
  67. package/skills/gen-ui-review/SKILL.md +28 -28
  68. package/skills/gen-ui-review/agents/openai.yaml +1 -1
  69. package/skills/gen-ui-review/evals/evals.json +1 -1
  70. package/skills/gen-ui-review/evals/routing-corpus.json +1 -1
  71. package/skills/gen-ui-review/references/corpus-html-patterns.md +28 -29
  72. package/skills/gen-ui-review/references/exit-gate-mechanics.md +5 -5
  73. package/skills/gen-ui-review/references/lookup-maintenance.md +2 -2
  74. package/skills/gen-ui-review/references/loop-protocol.md +25 -25
  75. package/skills/gen-ui-review/references/rubric-cosmetic.md +10 -10
  76. package/skills/gen-ui-review/references/rubric-decompose.md +15 -16
  77. package/skills/gen-ui-review/references/rubric-score.md +23 -24
  78. package/skills/gen-ui-review/references/scorecard-worked-examples.md +12 -12
  79. package/skills/gen-ui-review/references/scores.schema.json +2 -2
  80. package/skills/llm-client-maintenance/SKILL.md +12 -12
  81. package/skills/llm-client-maintenance/evals/routing-corpus.json +1 -1
  82. package/skills/llm-client-maintenance/references/adapter-contract.md +27 -27
  83. package/skills/llm-client-maintenance/references/add-a-provider.md +25 -25
  84. package/skills/llm-client-maintenance/references/bridge-facade.md +31 -31
  85. package/skills/llm-client-maintenance/references/browser-proxy-boundary.md +17 -17
  86. package/skills/llm-client-maintenance/references/model-registry.md +14 -14
  87. package/skills/llm-client-maintenance/references/streaming-sse.md +21 -21
  88. package/skills/package-release/SKILL.md +21 -23
  89. package/skills/package-release/evals/evals.json +2 -2
  90. package/skills/package-release/evals/routing-corpus.json +2 -2
  91. package/skills/package-release/references/authorization-model.md +6 -8
  92. package/skills/package-release/references/changelog-discipline.md +25 -27
  93. package/skills/package-release/references/cut-procedure.md +129 -136
  94. package/skills/package-release/references/gates-catalog.md +42 -43
  95. package/skills/package-release/references/independent-package-release.md +14 -15
  96. package/skills/package-release/references/invariants-detail.md +9 -9
  97. package/skills/package-release/references/mechanization.md +11 -12
  98. package/skills/package-release/references/migration-guide-authoring.md +20 -20
  99. package/skills/package-release/references/notes-authoring.md +20 -20
  100. package/skills/package-release/references/recovery-paths.md +28 -28
  101. package/skills/primitive-authoring/SKILL.md +19 -19
  102. package/skills/primitive-authoring/agents/openai.yaml +1 -1
  103. package/skills/primitive-authoring/evals/evals.json +2 -2
  104. package/skills/primitive-authoring/evals/routing-corpus.json +2 -2
  105. package/skills/primitive-authoring/references/INDEX.md +22 -22
  106. package/skills/primitive-authoring/references/anti-patterns.md +76 -77
  107. package/skills/primitive-authoring/references/api-contract.md +63 -65
  108. package/skills/primitive-authoring/references/authoring-cycle.md +47 -50
  109. package/skills/primitive-authoring/references/canonical-pattern-index.md +118 -118
  110. package/skills/primitive-authoring/references/code-style.md +50 -52
  111. package/skills/primitive-authoring/references/common-gotchas.md +23 -23
  112. package/skills/primitive-authoring/references/composite-demo-protocol.md +42 -45
  113. package/skills/primitive-authoring/references/css-patterns.md +97 -97
  114. package/skills/primitive-authoring/references/for-attribute-event-contract.md +12 -12
  115. package/skills/primitive-authoring/references/form-control-sizing.md +16 -18
  116. package/skills/primitive-authoring/references/lifecycle-patterns.md +42 -42
  117. package/skills/primitive-authoring/references/llm-bridge.md +21 -21
  118. package/skills/primitive-authoring/references/module-promotion.md +51 -51
  119. package/skills/primitive-authoring/references/primitive-audit.md +16 -16
  120. package/skills/primitive-authoring/references/shell-patterns.md +81 -81
  121. package/skills/primitive-authoring/references/svg-authoring.md +62 -66
  122. package/skills/primitive-authoring/references/token-contract.md +45 -49
  123. package/skills/primitive-authoring/references/trait-pages.md +8 -8
  124. package/skills/primitive-authoring/references/worked-example.md +39 -39
  125. package/skills/primitive-authoring/references/yaml-contract.md +139 -143
  126. package/skills/site-deployment/SKILL.md +25 -26
  127. package/skills/site-deployment/agents/openai.yaml +1 -1
  128. package/skills/site-deployment/evals/routing-corpus.json +1 -1
  129. package/skills/site-deployment/references/deploy-playbooks.md +51 -53
  130. package/skills/site-docs-authoring/SKILL.md +14 -14
  131. package/skills/site-docs-authoring/agents/openai.yaml +1 -1
  132. package/skills/site-docs-authoring/evals/evals.json +1 -1
  133. package/skills/site-docs-authoring/evals/routing-corpus.json +1 -1
  134. package/skills/site-docs-authoring/intent.md +18 -18
  135. package/skills/ssr-compatibility/SKILL.md +24 -25
  136. package/skills/ssr-compatibility/agents/openai.yaml +1 -1
  137. package/skills/ssr-compatibility/evals/evals.json +1 -1
  138. package/skills/ssr-compatibility/evals/routing-corpus.json +1 -1
  139. package/skills/ssr-compatibility/references/consumer-workarounds.md +22 -23
  140. package/skills/ssr-compatibility/references/failure-shapes.md +84 -91
  141. package/skills/ssr-compatibility/references/guard-patterns.md +54 -58
  142. package/skills/ssr-compatibility/references/status-ledger.md +17 -17
  143. package/skills/ssr-compatibility/references/test-without-linkedom.md +26 -27
@@ -15,7 +15,7 @@ Organized by axis.
15
15
  closable: { type: Boolean, default: true, reflect: true }
16
16
  ```
17
17
 
18
- **What went wrong:** the component did something by default, and the prop named the default behavior. Users couldn't opt out by just setting the attribute they had to pass `closable="false"` which doesn't even work for Boolean attributes (presence = true; absent = false is the HTML model).
18
+ **What went wrong:** the component did something by default, and the prop named the default behavior. Users couldn't opt out by just setting the attribute, they had to pass `closable="false"` which doesn't even work for Boolean attributes (presence = true; absent = false is the HTML model).
19
19
 
20
20
  **Fix:** flip the name. Default `true` behavior = name the prop for the opt-out.
21
21
 
@@ -28,7 +28,7 @@ closable: { type: Boolean, default: true, reflect: true }
28
28
  | `toggle-group-ui[multiple]` | `toggle-group-ui[single]` |
29
29
  | `swiper-ui[pause-on-hover]` | `swiper-ui[noPauseOnHover]` |
30
30
 
31
- **Recognition:** if you find yourself writing `default: true` in a `static properties` block, stop the name is wrong.
31
+ **Recognition:** if you find yourself writing `default: true` in a `static properties` block, stop: the name is wrong.
32
32
 
33
33
  ### AP-02 · Magic-value sentinel instead of `null`
34
34
 
@@ -50,11 +50,11 @@ value: { type: Number, default: null, reflect: true }
50
50
  ### AP-03 · `attr:` silent typo
51
51
 
52
52
  ```javascript
53
- // WRONG silently ignored
53
+ // WRONG, silently ignored
54
54
  allowHalf: { type: Boolean, default: false, reflect: true, attr: 'allow-half' }
55
55
  ```
56
56
 
57
- **What went wrong:** the mapper looks for `attribute:`, not `attr:`. The typo is silently dropped. The kebab-case HTML attribute _happens_ to auto- convert to `allow-half` so the bug hides until someone renames `allowHalf` and the attribute never wires.
57
+ **What went wrong:** the mapper looks for `attribute:`, not `attr:`. The typo is silently dropped. The kebab-case HTML attribute _happens_ to auto- convert to `allow-half` so the bug hides, until someone renames `allowHalf` and the attribute never wires.
58
58
 
59
59
  **Fix:** `attribute:` always.
60
60
 
@@ -83,7 +83,7 @@ allowHalf: { type: Boolean, default: false, reflect: true, attr: 'allow-half' }
83
83
 
84
84
  - Parent holds a current index → `step` (e.g. `timeline-ui[step="3"]`)
85
85
  - Parent holds a selection → `value` (e.g. `inspector-ui[value="item-42"]`)
86
- - Child Boolean state stays as `active` that's fine.
86
+ - Child Boolean state stays as `active`, that's fine.
87
87
 
88
88
  ### AP-06 · `error` as a variant
89
89
 
@@ -103,7 +103,7 @@ allowHalf: { type: Boolean, default: false, reflect: true, attr: 'allow-half' }
103
103
 
104
104
  **What went wrong:** `disabled` has specific form semantics (removed from tab order, blocks submission). On a non-form component, the implicit meaning is "read-only interaction."
105
105
 
106
- **Fix:** `readonly` matches the actual intent.
106
+ **Fix:** `readonly`, matches the actual intent.
107
107
 
108
108
  ### AP-08 · `multiple` with exclusion semantics
109
109
 
@@ -123,7 +123,7 @@ allowHalf: { type: Boolean, default: false, reflect: true, attr: 'allow-half' }
123
123
  ### AP-09 · Native `<input>` / `<select>` / `<textarea>` stamped inside a primitive
124
124
 
125
125
  ```javascript
126
- // WRONG command.class.js before gh#1302 (a9d282461^); select.class.js's
126
+ // WRONG, command.class.js before gh#1302 (a9d282461^); select.class.js's
127
127
  // searchable branch and drilldown.class.js carried the same shape
128
128
  this.innerHTML = `
129
129
  <header>
@@ -132,20 +132,20 @@ this.innerHTML = `
132
132
  </header>…`;
133
133
  ```
134
134
 
135
- **What went wrong:** AdiaUI interactive primitives own their affordance the host IS the interactive surface: `contenteditable="plaintext-only"` (or a custom non-editable surface), form participation via `UIFormElement` + `ElementInternals`, explicit ARIA on the host, a JS keyboard model per the WAI-ARIA APG, and `--a-*`-token chrome (ADR-0025, 2026-05-11). A native form control under the hood leaks browser chrome the token contract cannot uniformly restyle, imports per-browser keyboard quirks, and splits the value contract. Three primitives stamped a raw `<input>` anyway with no recorded exception a search field is exactly the case the rule exists for (ADR-0055, gh#1302).
135
+ **What went wrong:** AdiaUI interactive primitives own their affordance: the host IS the interactive surface: `contenteditable="plaintext-only"` (or a custom non-editable surface), form participation via `UIFormElement` + `ElementInternals`, explicit ARIA on the host, a JS keyboard model per the WAI-ARIA APG, and `--a-*`-token chrome (ADR-0025, 2026-05-11). A native form control under the hood leaks browser chrome the token contract cannot uniformly restyle, imports per-browser keyboard quirks, and splits the value contract. Three primitives stamped a raw `<input>` anyway with no recorded exception, a search field is exactly the case the rule exists for (ADR-0055, gh#1302).
136
136
 
137
- **Fix:** compose an in-house surface already in the tree pick per component:
137
+ **Fix:** compose an in-house surface already in the tree, pick per component:
138
138
 
139
- - the `combobox-ui` shape `contenteditable="plaintext-only"` + `role="combobox"` + list filtering (`combobox.class.js:22-23`); now used by `command-ui`'s palette field (`command.class.js:108`) and `select-ui`'s searchable trigger (`select.class.js:442`);
140
- - composition of `<input-ui>` the `search-ui` precedent (`search.class.js:19,44`); now used by `drilldown-ui`'s filter field (`drilldown.class.js:184-201`).
139
+ - the `combobox-ui` shape, `contenteditable="plaintext-only"` + `role="combobox"` + list filtering (`combobox.class.js:22-23`); now used by `command-ui`'s palette field (`command.class.js:108`) and `select-ui`'s searchable trigger (`select.class.js:442`);
140
+ - composition of `<input-ui>`, the `search-ui` precedent (`search.class.js:19,44`); now used by `drilldown-ui`'s filter field (`drilldown.class.js:184-201`).
141
141
 
142
- Behavior parity is part of done: focus order, IME/paste, `inputmode`, and APG-combobox ARIA, verified by a browser probe on the surface not by tests alone.
142
+ Behavior parity is part of done: focus order, IME/paste, `inputmode`, and APG-combobox ARIA, verified by a browser probe on the surface, not by tests alone.
143
143
 
144
- **The exception list is singular and closed:** `<input-ui type="password">` wraps a native `<input type="password">` (`input.class.js:116,213`) because `-webkit-text-security: disc` masking only paints on a real input a mechanical justification, not an aesthetic one. Nothing else qualifies by convenience; a new exception is its own ADR, not a comment in the class. **Out of scope:** disclosure widgets (`<details>/<summary>`, e.g. `agent-trace.js:170`'s row expand) the rule covers surfaces that gather user input, participate in `<form>`, or carry a value; a `<details>` toggle does none of these. Widening the rule to all native interactive chrome would be a new decision with its own blast radius.
144
+ **The exception list is singular and closed:** `<input-ui type="password">` wraps a native `<input type="password">` (`input.class.js:116,213`) because `-webkit-text-security: disc` masking only paints on a real input, a mechanical justification, not an aesthetic one. Nothing else qualifies by convenience; a new exception is its own ADR, not a comment in the class. **Out of scope:** disclosure widgets (`<details>/<summary>`, e.g. `agent-trace.js:170`'s row expand), the rule covers surfaces that gather user input, participate in `<form>`, or carry a value; a `<details>` toggle does none of these. Widening the rule to all native interactive chrome would be a new decision with its own blast radius.
145
145
 
146
- **Gate:** `npm run audit:stamped-native-buttons` (`scripts/dev/audit-stamped-native-buttons.mjs`; runs in PR CI via `.github/workflows/ci.yml`, NOT in the `npm run check` aggregate) scans `packages/web-components/components/**` + `packages/web-modules/**` `.js` for all four control classes ADR-0055 §Consequences named `<button` / `createElement('button')` (gh#276, `SANCTIONED_BUTTON` map), `<input` (ADR-0055 / gh#1302, `SANCTIONED_INPUT`), and since gh#1495 `<select` (`SANCTIONED_SELECT`) and `<textarea` (`SANCTIONED_TEXTAREA`) and fails any hit outside the matching map. The two stamps that widening surfaced are both dispositioned in the maps: `table.class.js` (`createElement('select')` the numeric-filter operator picker, a value-gathering surface squarely inside the rule, PENDING the same gh#276 filter-popover migration blocker as this file's button/input entries) and `color-picker.class.js` (`createElement('textarea')` DECIDED gh#1495, keep: an off-screen `execCommand('copy')` clipboard-transport shim, appended and removed within one call, is out of ADR-0025's scope by ADR-0055 §Decision 4's own test it gathers no user input, joins no `<form>`, carries no user-facing value; the primary path is `navigator.clipboard.writeText` and the shim only runs where the Clipboard API is absent). Entries carry per-file reasons: password is the ADR-0025 exception; `type="file"` pickers (chat-input, upload) and the otp-input / table stamps are listed PENDING their own decision sanctioned-with-reason is not the same as ratified. A sanctioned entry whose stamp disappears is reported stale so the list only shrinks, and the script's `selftest` proves each element class's positive + negative controls on scratch fixtures (also run in PR CI).
146
+ **Gate:** `npm run audit:stamped-native-buttons` (`scripts/dev/audit-stamped-native-buttons.mjs`; runs in PR CI via `.github/workflows/ci.yml`, NOT in the `npm run check` aggregate) scans `packages/web-components/components/**` + `packages/web-modules/**` `.js` for all four control classes ADR-0055 §Consequences named, `<button` / `createElement('button')` (gh#276, `SANCTIONED_BUTTON` map), `<input` (ADR-0055 / gh#1302, `SANCTIONED_INPUT`), and since gh#1495 `<select` (`SANCTIONED_SELECT`) and `<textarea` (`SANCTIONED_TEXTAREA`), and fails any hit outside the matching map. The two stamps that widening surfaced are both dispositioned in the maps: `table.class.js` (`createElement('select')`, the numeric-filter operator picker, a value-gathering surface squarely inside the rule, PENDING the same gh#276 filter-popover migration blocker as this file's button/input entries) and `color-picker.class.js` (`createElement('textarea')`, DECIDED gh#1495, keep: an off-screen `execCommand('copy')` clipboard-transport shim, appended and removed within one call, is out of ADR-0025's scope by ADR-0055 §Decision 4's own test, it gathers no user input, joins no `<form>`, carries no user-facing value; the primary path is `navigator.clipboard.writeText` and the shim only runs where the Clipboard API is absent). Entries carry per-file reasons: password is the ADR-0025 exception; `type="file"` pickers (chat-input, upload) and the otp-input / table stamps are listed PENDING their own decision, sanctioned-with-reason is not the same as ratified. A sanctioned entry whose stamp disappears is reported stale so the list only shrinks, and the script's `selftest` proves each element class's positive + negative controls on scratch fixtures (also run in PR CI).
147
147
 
148
- Source: ADR-0025 (the rule) · ADR-0055 (conformance amendment, ratified 2026-08-15, gh#1302 appends to ADR-0025's record; the ADR-0025 text itself is not edited).
148
+ Source: ADR-0025 (the rule) · ADR-0055 (conformance amendment, ratified 2026-08-15, gh#1302, appends to ADR-0025's record; the ADR-0025 text itself is not edited).
149
149
 
150
150
  ---
151
151
 
@@ -154,14 +154,14 @@ Source: ADR-0025 (the rule) · ADR-0055 (conformance amendment, ratified 2026-08
154
154
  ### AP-T1 · Variant body consumes L2 directly
155
155
 
156
156
  ```css
157
- /* WRONG button.css */
157
+ /* WRONG, button.css */
158
158
  :scope[variant="danger"]:not([disabled]):hover {
159
159
  --button-fg: var(--a-danger); /* L2 */
160
160
  --button-border: var(--a-danger); /* L2 */
161
161
  }
162
162
  ```
163
163
 
164
- **What went wrong:** the variant bypassed the L3 state matrix. When a user enables high-contrast mode, the contrast-mode overrides on `--a-danger-fg-hover` don't reach this component it's reading the flat L2 token instead of the stateful L3 one.
164
+ **What went wrong:** the variant bypassed the L3 state matrix. When a user enables high-contrast mode, the contrast-mode overrides on `--a-danger-fg-hover` don't reach this component, it's reading the flat L2 token instead of the stateful L3 one.
165
165
 
166
166
  **Fix:** alias from the L3 matrix:
167
167
 
@@ -198,7 +198,7 @@ Or, if the component already defines state-aware tokens in `:where(:scope)`, reu
198
198
  ### AP-T3 · Raw px ≥ 3 without justification
199
199
 
200
200
  ```css
201
- /* WRONG 12px is neither a stroke nor intrinsic */
201
+ /* WRONG, 12px is neither a stroke nor intrinsic */
202
202
  :scope { padding: 12px; }
203
203
  ```
204
204
 
@@ -220,7 +220,7 @@ Or, if the component already defines state-aware tokens in `:where(:scope)`, reu
220
220
  ### AP-T4 · Component token name mismatches scope
221
221
 
222
222
  ```css
223
- /* WRONG button.css */
223
+ /* WRONG, button.css */
224
224
  @scope (button-ui) {
225
225
  :where(:scope) {
226
226
  --btn-bg: var(--a-primary-bg); /* ← stem mismatch */
@@ -245,7 +245,7 @@ Or, if the component already defines state-aware tokens in `:where(:scope)`, reu
245
245
  ### AP-S1 · Variant body sets layout properties
246
246
 
247
247
  ```css
248
- /* WRONG pagination.css (pre-fix) */
248
+ /* WRONG, pagination.css (pre-fix) */
249
249
  :scope[variant="button"] [slot="nav"] button {
250
250
  width: var(--pagination-button-size);
251
251
  height: var(--pagination-button-size);
@@ -254,16 +254,16 @@ Or, if the component already defines state-aware tokens in `:where(:scope)`, reu
254
254
  }
255
255
  ```
256
256
 
257
- **What went wrong:** `variant="button"` changes the layout (square buttons with specific dimensions) that's a mode, not a variant.
257
+ **What went wrong:** `variant="button"` changes the layout (square buttons with specific dimensions), that's a mode, not a variant.
258
258
 
259
259
  **Fix:** add `pagination-ui[variant="button"]` to the Sanctioned Mode Attributes table in `.claude/docs/specs/component-token-contract.md`. Document the layout change as intentional.
260
260
 
261
- Alternative: refactor to make the attribute cosmetic if possible.
261
+ Alternative: refactor to make the attribute cosmetic, if possible.
262
262
 
263
263
  ### AP-S2 · Single-block `:scope` (tokens and styles interleaved)
264
264
 
265
265
  ```css
266
- /* WRONG missing the :where(:scope) token block */
266
+ /* WRONG, missing the :where(:scope) token block */
267
267
  @scope (button-ui) {
268
268
  :scope {
269
269
  --button-bg: var(--a-primary-bg);
@@ -280,7 +280,7 @@ Alternative: refactor to make the attribute cosmetic — if possible.
280
280
  ### AP-S3 · `::slotted()` in light-DOM component
281
281
 
282
282
  ```css
283
- /* WRONG shadow DOM syntax */
283
+ /* WRONG, shadow DOM syntax */
284
284
  ::slotted([slot="icon"]) { margin-inline-end: var(--gap); }
285
285
  ```
286
286
 
@@ -297,7 +297,7 @@ Ever. If you need it, specificity is wrong somewhere. Fix the layering.
297
297
  ### AP-S5 · Inline `grid-template-columns` for asymmetric splits
298
298
 
299
299
  ```html
300
- <!-- WRONG escape hatch that bypasses the grid-ui vocabulary -->
300
+ <!-- WRONG, escape hatch that bypasses the grid-ui vocabulary -->
301
301
  <grid-ui gap="4" style="grid-template-columns: 2fr 1fr">
302
302
  <card-ui>Chart</card-ui>
303
303
  <card-ui>Sidebar</card-ui>
@@ -309,61 +309,61 @@ Ever. If you need it, specificity is wrong somewhere. Fix the layering.
309
309
  **Fix:** use `grid-ui[columns="N"]` plus `[span="M"]` on the children. The component ships column variants 1-6, auto-fill, auto-fit, and a per-child `span="2|3|4|5|6|full"` attribute. Compose a 2:1 split as `columns="3"` + the wide child gets `span="2"`; compose a 3:2 split as `columns="5"` + `span="3"` + `span="2"`.
310
310
 
311
311
  ```html
312
- <!-- RIGHT 2:1 via columns=3 and a span=2 child -->
312
+ <!-- RIGHT, 2:1 via columns=3 and a span=2 child -->
313
313
  <grid-ui columns="3" gap="4">
314
314
  <card-ui span="2">Chart</card-ui>
315
315
  <card-ui>Sidebar</card-ui>
316
316
  </grid-ui>
317
317
 
318
- <!-- RIGHT 3:2 via columns=5 and span=3 + span=2 -->
318
+ <!-- RIGHT, 3:2 via columns=5 and span=3 + span=2 -->
319
319
  <grid-ui columns="5" gap="4">
320
320
  <card-ui span="3">Overview</card-ui>
321
321
  <card-ui span="2">Recent Sales</card-ui>
322
322
  </grid-ui>
323
323
  ```
324
324
 
325
- **Recognition:** `style="grid-template-columns: …"` anywhere in a page is a signal that either (a) the author doesn't know the `span` attribute exists, or (b) the ratio truly can't be expressed in columns 1-6. The second case is rare most asymmetric splits approximate cleanly to `3:1`, `2:1`, `3:2`, or `4:1`. If the ratio genuinely can't be expressed, it probably should become a separate layout primitive with a named attribute rather than live as an inline style.
325
+ **Recognition:** `style="grid-template-columns: …"` anywhere in a page is a signal that either (a) the author doesn't know the `span` attribute exists, or (b) the ratio truly can't be expressed in columns 1-6. The second case is rare, most asymmetric splits approximate cleanly to `3:1`, `2:1`, `3:2`, or `4:1`. If the ratio genuinely can't be expressed, it probably should become a separate layout primitive with a named attribute rather than live as an inline style.
326
326
 
327
- **Real fix:** admin-dashboard had `style="grid-template-columns: 7fr 5fr"` and `"2fr 1fr"`, analytics-dashboard had one `"2fr 1fr"`. Replaced with `columns="5" span="3"+"2"` and `columns="3" span="2"+default` respectively; the 7:5 ratio shifted to 3:2 (60:40 vs 58:42) imperceptible visually, a win structurally.
327
+ **Real fix:** admin-dashboard had `style="grid-template-columns: 7fr 5fr"` and `"2fr 1fr"`, analytics-dashboard had one `"2fr 1fr"`. Replaced with `columns="5" span="3"+"2"` and `columns="3" span="2"+default` respectively; the 7:5 ratio shifted to 3:2 (60:40 vs 58:42), imperceptible visually, a win structurally.
328
328
 
329
329
  ### AP-S6 · `:scope >` child combinator on a conditionally-rendered part
330
330
 
331
331
  ```css
332
- /* WRONG [data-part="empty"] is rendered behind a `${isEmpty ? … : null}` branch */
332
+ /* WRONG, [data-part="empty"] is rendered behind a `${isEmpty ? … : null}` branch */
333
333
  :scope > [data-part="empty"] { display: grid; place-items: center; }
334
334
  ```
335
335
 
336
- **What went wrong:** the template engine wraps every conditional render branch `${cond ? … : null}` (also `?` / `.map()`) in a `<span style="display:contents">`. That span is a real DOM child, so the conditionally-rendered element is a _grandchild_ of `:scope`, not a direct child. `display:contents` removes the span from layout but NOT from selector matching, so `:scope > [data-part="empty"]` matches nothing and the rule silently no-ops no error, passes `components --verify`, looks fine in happy-dom. Only a live render shows the un-styled block.
336
+ **What went wrong:** the template engine wraps every conditional render branch, `${cond ? … : null}` (also `?` / `.map()`), in a `<span style="display:contents">`. That span is a real DOM child, so the conditionally-rendered element is a _grandchild_ of `:scope`, not a direct child. `display:contents` removes the span from layout but NOT from selector matching, so `:scope > [data-part="empty"]` matches nothing and the rule silently no-ops, no error, passes `components --verify`, looks fine in happy-dom. Only a live render shows the un-styled block.
337
337
 
338
338
  **Fix:** use a **descendant** combinator for any conditionally-rendered part; keep `:scope >` only for parts that render unconditionally (those stay direct children).
339
339
 
340
340
  ```css
341
- /* RIGHT descendant combinator survives the display:contents wrapper */
341
+ /* RIGHT, descendant combinator survives the display:contents wrapper */
342
342
  :scope [data-part="empty"] { display: grid; place-items: center; }
343
- :scope > [data-part="header"] { … } /* static part child combinator OK */
343
+ :scope > [data-part="header"] { … } /* static part, child combinator OK */
344
344
  ```
345
345
 
346
- **Recognition:** a `[data-part]` rule that "doesn't apply" though the markup looks right, on a part rendered inside a `${cond ? … : null}` / `?` / `.map()` branch. Inspect the live DOM a `<span style="display:contents">` wrapping the part confirms it. **Recurring class:** integrations-page empty-state, onboarding-checklist complete-CTA, and earlier sightings (bug-51, bug-53). Positive-guidance version: [css-patterns.md](css-patterns.md) §"Conditional-render parts defeat `:scope >`".
346
+ **Recognition:** a `[data-part]` rule that "doesn't apply" though the markup looks right, on a part rendered inside a `${cond ? … : null}` / `?` / `.map()` branch. Inspect the live DOM, a `<span style="display:contents">` wrapping the part confirms it. **Recurring class:** integrations-page empty-state, onboarding-checklist complete-CTA, and earlier sightings (bug-51, bug-53). Positive-guidance version: [css-patterns.md](css-patterns.md) §"Conditional-render parts defeat `:scope >`".
347
347
 
348
348
  **Real fix:** integrations-page.css + onboarding-checklist.css had `:scope > [data-part]` on conditionally-rendered parts (empty-state / complete CTA); the layout grid and the separating margin silently dropped. Converted those rules to descendant combinators.
349
349
 
350
350
  ### AP-S7 · `align-self: stretch` to vertically center a fixed-height flex child
351
351
 
352
352
  ```css
353
- /* WRONG stretch can't size a child that carries a definite height */
353
+ /* WRONG, stretch can't size a child that carries a definite height */
354
354
  [slot="field"] > [slot="trailing"] { align-self: stretch; } /* a 20px <kbd-ui> pins to the TOP */
355
355
  ```
356
356
 
357
- **What went wrong:** a flex item with `align-self: stretch` **and** a definite cross-axis size (`height` / `block-size`) does not stretch the explicit size wins and the item positions at **flex-start** (top), not center. A `<kbd-ui>` (`height: 1.25rem`) in a 30px chrome sat ~4px high. `stretch` only sizes auto-height items; for a fixed-height child it silently degrades to top-alignment. A sibling `<button-ui>` looked fine only because it had no blocking height and genuinely filled the chrome (center vs. stretch identical for it).
357
+ **What went wrong:** a flex item with `align-self: stretch` **and** a definite cross-axis size (`height` / `block-size`) does not stretch, the explicit size wins and the item positions at **flex-start** (top), not center. A `<kbd-ui>` (`height: 1.25rem`) in a 30px chrome sat ~4px high. `stretch` only sizes auto-height items; for a fixed-height child it silently degrades to top-alignment. A sibling `<button-ui>` looked fine only because it had no blocking height and genuinely filled the chrome (center vs. stretch identical for it).
358
358
 
359
359
  **Fix:** to vertically center a row of mixed-height affordances, use `align-self: center` and let each child keep its own token height.
360
360
 
361
361
  ```css
362
- /* RIGHT center keeps each child's intrinsic height on the vertical center */
362
+ /* RIGHT, center keeps each child's intrinsic height on the vertical center */
363
363
  [slot="field"] > [slot="trailing"] { align-self: center; }
364
364
  ```
365
365
 
366
- **Recognition:** a fixed-height inline affordance (kbd / icon / badge) hugging the top of a taller flex container while a full-height sibling looks correct suspect `align-self: stretch` on a mixed-height row.
366
+ **Recognition:** a fixed-height inline affordance (kbd / icon / badge) hugging the top of a taller flex container while a full-height sibling looks correct, suspect `align-self: stretch` on a mixed-height row.
367
367
 
368
368
  **Real fix:** input-ui's leading/trailing affordance slots used `align-self: stretch`; the ⌘K `<kbd-ui>` hint sat 4px high. `center` fixed it and let `<button-ui>` children honor their own `--button-height` token (bug-60).
369
369
 
@@ -374,13 +374,13 @@ Ever. If you need it, specificity is wrong somewhere. Fix the layering.
374
374
  ### AP-L1 · Inline arrow passed to `addEventListener`
375
375
 
376
376
  ```javascript
377
- // WRONG cot.js (pre-fix)
377
+ // WRONG, cot.js (pre-fix)
378
378
  this.#summaryEl.addEventListener('click', () => {
379
379
  this.collapsed = !this.collapsed;
380
380
  });
381
381
  ```
382
382
 
383
- **What went wrong:** `removeEventListener(type, anInlineArrow)` can't match new function instance each time. The listener stays bound. If the component is re-attached, a SECOND listener binds too. Accumulates.
383
+ **What went wrong:** `removeEventListener(type, anInlineArrow)` can't match, new function instance each time. The listener stays bound. If the component is re-attached, a SECOND listener binds too. Accumulates.
384
384
 
385
385
  **Fix:** stable `#field` arrow.
386
386
 
@@ -402,7 +402,7 @@ disconnected() {
402
402
  ### AP-L2 · Duplicate `disconnected()` method
403
403
 
404
404
  ```javascript
405
- // WRONG chart.js (pre-fix)
405
+ // WRONG, chart.js (pre-fix)
406
406
  class UIChart extends UIElement {
407
407
  connected() { /* ... */ this.#ro = new ResizeObserver(...); }
408
408
 
@@ -435,7 +435,7 @@ disconnected() {
435
435
  ### AP-L3 · Missing `super.connected()` / `super.disconnected()`
436
436
 
437
437
  ```javascript
438
- // WRONG UIFormElement subclass missing super call
438
+ // WRONG, UIFormElement subclass missing super call
439
439
  class UIInput extends UIFormElement {
440
440
  connected() {
441
441
  // super.connected() NOT called
@@ -444,7 +444,7 @@ class UIInput extends UIFormElement {
444
444
  }
445
445
  ```
446
446
 
447
- **What went wrong:** `ElementInternals` registration skipped. The component doesn't participate in forms no value submission, no validation, no form-reset handling. Silent.
447
+ **What went wrong:** `ElementInternals` registration skipped. The component doesn't participate in forms, no value submission, no validation, no form-reset handling. Silent.
448
448
 
449
449
  **Fix:** `super.connected()` first line of the method. `super.disconnected()` first line of its method too.
450
450
 
@@ -485,7 +485,7 @@ connected() {
485
485
  this.#ro.observe(this);
486
486
  }
487
487
 
488
- // Later new options, recreated
488
+ // Later, new options, recreated
489
489
  updateConfig(opts) {
490
490
  this.#ro = new ResizeObserver(...); // ← old one leaks
491
491
  this.#ro.observe(this);
@@ -505,7 +505,7 @@ updateConfig(opts) {
505
505
  ### AP-L7 · Empty template wipes authored light-DOM children
506
506
 
507
507
  ```javascript
508
- // WRONG drawer.js (pre-fix)
508
+ // WRONG, drawer.js (pre-fix)
509
509
  class UIDrawer extends UIElement {
510
510
  static template = () => html``; // ← looks harmless. It isn't.
511
511
 
@@ -518,12 +518,12 @@ class UIDrawer extends UIElement {
518
518
 
519
519
  **What went wrong:** `UIElement`'s base `connectedCallback` runs an effect that calls `stamp(template(this), this)`. `stamp` calls `mount`, which calls `container.replaceChildren(fragment)`. Even an "empty" `html\`\``produces a non-null result, so`replaceChildren`runs and clears every authored child (header/section/footer) out of the host element. The component's own`render()` then tries to migrate children that no longer exist.
520
520
 
521
- Symptom: drawers render with a close button in an empty panel; all authored content is gone. Hard to debug because the children WERE in the DOM at `connected()` time they get wiped milliseconds later when the effect runs.
521
+ Symptom: drawers render with a close button in an empty panel; all authored content is gone. Hard to debug because the children WERE in the DOM at `connected()` time, they get wiped milliseconds later when the effect runs.
522
522
 
523
523
  **Fix:** drop the override entirely. The base class already defines `static template = () => null;`, and `stamp()` is skipped when the result is null:
524
524
 
525
525
  ```javascript
526
- // RIGHT no template override
526
+ // RIGHT, no template override
527
527
  class UIDrawer extends UIElement {
528
528
  // base UIElement.template returns null → stamp() is skipped →
529
529
  // authored light-DOM children survive through render().
@@ -533,17 +533,17 @@ class UIDrawer extends UIElement {
533
533
  }
534
534
  ```
535
535
 
536
- **Recognition:** light-DOM components that compose authored children (card-ui, drawer-ui, list-item-ui, menu-ui, anything with slots) must NOT declare any template not even an empty one. If the component needs to stamp structural parts (dialog, panel, scrim), use `static parts = {...}` and `this.ensure('name')` instead; parts append once and don't wipe siblings.
536
+ **Recognition:** light-DOM components that compose authored children (card-ui, drawer-ui, list-item-ui, menu-ui, anything with slots) must NOT declare any template, not even an empty one. If the component needs to stamp structural parts (dialog, panel, scrim), use `static parts = {...}` and `this.ensure('name')` instead; parts append once and don't wipe siblings.
537
537
 
538
538
  ### AP-L8 · `setAttribute('value', …)` on `input-ui` after first render
539
539
 
540
540
  ```javascript
541
- // WRONG expected to update the visible text
541
+ // WRONG, expected to update the visible text
542
542
  input.setAttribute('value', 'Liam Johnson');
543
543
  // …user still sees an empty field
544
544
  ```
545
545
 
546
- **What went wrong:** `input-ui.connected()` copies `this.value` to `#textEl.textContent` exactly once. `render()` does NOT re-sync when the `value` attribute changes externally this is intentional, to prevent external re-renders from clobbering in-progress user input. But it means declarative value updates after mount have no visible effect.
546
+ **What went wrong:** `input-ui.connected()` copies `this.value` to `#textEl.textContent` exactly once. `render()` does NOT re-sync when the `value` attribute changes externally: this is intentional, to prevent external re-renders from clobbering in-progress user input. But it means declarative value updates after mount have no visible effect.
547
547
 
548
548
  **Fix:** set the attribute AND mirror the text slot when you need to update the displayed value from outside:
549
549
 
@@ -555,12 +555,12 @@ if (textSlot) textSlot.textContent = 'Liam Johnson';
555
555
 
556
556
  **Real fix:** drawer-populating setup code (admin-dashboard transaction drawer, report drawer) hit this when filling input-ui fields from a clicked row. Worked around with an explicit text-slot mirror; a proper fix would be for `input-ui.render()` to re-sync `#textEl.textContent` when the `value` property changes while the field isn't focused.
557
557
 
558
- **Recognition:** if you're setting an `input-ui` / `textarea-ui` `value` from JavaScript and the field looks empty, you've hit this. `<stat-ui>` and `<select-ui>` do sync on attribute change the gap is only in the contenteditable-backed text field.
558
+ **Recognition:** if you're setting an `input-ui` / `textarea-ui` `value` from JavaScript and the field looks empty, you've hit this. `<stat-ui>` and `<select-ui>` do sync on attribute change: the gap is only in the contenteditable-backed text field.
559
559
 
560
560
  ### AP-L9 · Radio cards without a `role="radiogroup"` parent
561
561
 
562
562
  ```html
563
- <!-- WRONG radios never cross-deselect -->
563
+ <!-- WRONG, radios never cross-deselect -->
564
564
  <list-ui>
565
565
  <list-item-ui>
566
566
  <card-ui>
@@ -574,7 +574,7 @@ if (textSlot) textSlot.textContent = 'Liam Johnson';
574
574
  </list-ui>
575
575
  ```
576
576
 
577
- **What went wrong:** `radio-ui.#select` does a sibling lookup via `this.closest('fieldset, [role="radiogroup"]') || this.parentElement`. When each radio sits inside its own `<span slot="icon">`, the "parent" contains only that one radio no siblings to deselect. Clicking one doesn't uncheck the others, and the initial `checked` state may or may not render visually depending on mount order.
577
+ **What went wrong:** `radio-ui.#select` does a sibling lookup via `this.closest('fieldset, [role="radiogroup"]') || this.parentElement`. When each radio sits inside its own `<span slot="icon">`, the "parent" contains only that one radio, no siblings to deselect. Clicking one doesn't uncheck the others, and the initial `checked` state may or may not render visually depending on mount order.
578
578
 
579
579
  **Fix:** wrap the whole group in an element `role="radiogroup"`:
580
580
 
@@ -648,9 +648,8 @@ Source: ADR-0027.
648
648
 
649
649
  ### AP-R2 · Folding two visually-converging widgets into a shared module (ADR-0101)
650
650
 
651
- Two widgets can converge visually same tab-strip chrome, same header
652
- band, same "looks like the same component" impression on the catalog page
653
- — while carrying fundamentally different content models underneath. That
651
+ Two widgets can converge visually, same tab-strip chrome, same header
652
+ band, same "looks like the same component" impression on the catalog page, while carrying fundamentally different content models underneath. That
654
653
  visual overlap alone is not sufficient reason to extract a shared module
655
654
  or fold one into the other.
656
655
 
@@ -660,7 +659,7 @@ imperatively and repeatedly after connect via `update()`/`setHTML()`) and
660
659
  shown two fixed ways) look like the same tabbed-view widget. They aren't:
661
660
  folding one into the other would force the static-capture component to
662
661
  grow a second, incompatible imperative-update mode it doesn't need for the
663
- job it already does correctly everywhere it's used added surface, not
662
+ job it already does correctly everywhere it's used, added surface, not
664
663
  removed surface. Extracting a shared tab-view module was evaluated and
665
664
  rejected for the same reason: no common substrate exists beneath the
666
665
  visual layer that both widgets could share without also merging their
@@ -670,18 +669,18 @@ different content models (`tabs-ui`'s synthesized strip vs. a hand-rolled
670
669
  **The resolution is a mirror, not a merge:** keep both components on their
671
670
  own contracts; restyle the trailing one to match the leading one's chrome
672
671
  via scoped CSS only (`@scope (inspector-ui)` overrides copying
673
- `preview-ui`'s header-tab anatomy, tokens, and typography) no shared
672
+ `preview-ui`'s header-tab anatomy, tokens, and typography), no shared
674
673
  component, no deprecation. The tradeoff this accepts: because the
675
674
  mirroring is duplicated CSS, not a shared module, a future visual change
676
675
  to the pattern's source component doesn't automatically propagate: check
677
676
  the mirrored block for parallel drift when you next touch the pattern
678
- that's being mirrored the same "primitives that compose primitives
677
+ that's being mirrored, the same "primitives that compose primitives
679
678
  don't auto-import" discipline ADR-0027 already requires (AP-R1, above),
680
679
  now applied to a duplicated CSS block instead of a duplicated import.
681
680
 
682
681
  Before extracting a shared module for two things that "look the same,"
683
682
  verify they'd actually share the extracted contract (props, update
684
- semantics, lifecycle) not just its rendered chrome. If they wouldn't,
683
+ semantics, lifecycle), not just its rendered chrome. If they wouldn't,
685
684
  mirror the chrome and keep both components separate.
686
685
 
687
686
  Source: ADR-0101.
@@ -694,33 +693,33 @@ One line each; every entry is a shipped bug or a CRITICAL gate. Same axes as abo
694
693
 
695
694
  ### API / silent-failure class
696
695
 
697
- - **Components silently accept ANY made-up attribute and no-op** (`text-ui muted`, `card-ui hover-elevate`, `description-list-ui variant="columns"`) check the component's yaml for the real prop list before authoring any attribute.
698
- - **`empty-state-ui` takes `[heading]`, not `[title]`** `title=` becomes the invisible native tooltip and the message never renders (the AP-04 collision in the wild).
699
- - **A hardcoded `open` on a `showModal` overlay (`modal-ui` / `drawer-ui`) bricks the whole page** no console error; only a live click / `elementFromPoint` probe catches it.
700
- - **Set `select-ui.options = [...]` for dynamic options, never `appendChild(<option>)`** the listbox popover is stamped around the INITIAL `<option>` children at connect; later-appended options land outside it as visible flow content.
701
- - **`<thead>/<tbody>/<tr>/<th>/<td>` are foster-parented OUT of the DOM inside any non-`<table>` ancestor, including custom elements** gone before JS runs; never author them inside a custom element.
702
- - **The elements above still get foster-parented inside `table-ui` (no internal `<table>`, `table.class.js:18`) but their TEXT survives, and `table-ui` never clears it**, so a raw `<thead>/<tbody>` mistake renders its own "No data" empty-state stacked on a garbled text dump instead of erroring (gh#921/#923/#924, 3 separate demo files fix by rewriting to the real `col-def`/`data` contract, never by touching table-ui).
696
+ - **Components silently accept ANY made-up attribute and no-op** (`text-ui muted`, `card-ui hover-elevate`, `description-list-ui variant="columns"`), check the component's yaml for the real prop list before authoring any attribute.
697
+ - **`empty-state-ui` takes `[heading]`, not `[title]`**, `title=` becomes the invisible native tooltip and the message never renders (the AP-04 collision in the wild).
698
+ - **A hardcoded `open` on a `showModal` overlay (`modal-ui` / `drawer-ui`) bricks the whole page**, no console error; only a live click / `elementFromPoint` probe catches it.
699
+ - **Set `select-ui.options = [...]` for dynamic options, never `appendChild(<option>)`**: the listbox popover is stamped around the INITIAL `<option>` children at connect; later-appended options land outside it as visible flow content.
700
+ - **`<thead>/<tbody>/<tr>/<th>/<td>` are foster-parented OUT of the DOM inside any non-`<table>` ancestor, including custom elements**, gone before JS runs; never author them inside a custom element.
701
+ - **The elements above still get foster-parented inside `table-ui` (no internal `<table>`, `table.class.js:18`), but their TEXT survives, and `table-ui` never clears it**, so a raw `<thead>/<tbody>` mistake renders its own "No data" empty-state stacked on a garbled text dump instead of erroring (gh#921/#923/#924, 3 separate demo files, fix by rewriting to the real `col-def`/`data` contract, never by touching table-ui).
703
702
 
704
703
  ### UIElement / reactivity class
705
704
 
706
- - **Declaring `textContent` / `innerHTML` / `className` / `id` in `static properties` breaks the native accessor** `installProps` swaps it for a signal, so `el.textContent = ''` becomes a signal write, not a child-wipe.
707
- - **`customElements.whenDefined(tag)` never rejects; an unimported tag never resolves** a `Promise.all([...]).then(bootstrap)` gate hangs forever with no console error (chrome renders via tag-keyed CSS; the page is just "empty").
708
- - **A MutationObserver that watches its own host AND mutates it in the callback hangs** drain with `this.#observer.takeRecords()` after every absorb pass.
709
- - **A document-listening trait must NOT re-emit the same-named event on its host with `bubbles: true`** the bubble re-reaches document and re-fires the handler (infinite loop); use distinct incoming vs outgoing names.
710
- - **Parent `render()` that filters children by an attribute the child sets in its OWN `connected()` races the upgrade** match by tag name too.
711
- - **Sync icon-registry checks (`isIconName` / `hasIcon`) at connect race the async manifest on static (non-Vite) deploys** pair with `whenIconRegistryReady` deferred re-stamping.
705
+ - **Declaring `textContent` / `innerHTML` / `className` / `id` in `static properties` breaks the native accessor**, `installProps` swaps it for a signal, so `el.textContent = ''` becomes a signal write, not a child-wipe.
706
+ - **`customElements.whenDefined(tag)` never rejects; an unimported tag never resolves**, a `Promise.all([...]).then(bootstrap)` gate hangs forever with no console error (chrome renders via tag-keyed CSS; the page is just "empty").
707
+ - **A MutationObserver that watches its own host AND mutates it in the callback hangs**, drain with `this.#observer.takeRecords()` after every absorb pass.
708
+ - **A document-listening trait must NOT re-emit the same-named event on its host with `bubbles: true`**, the bubble re-reaches document and re-fires the handler (infinite loop); use distinct incoming vs outgoing names.
709
+ - **Parent `render()` that filters children by an attribute the child sets in its OWN `connected()` races the upgrade**, match by tag name too.
710
+ - **Sync icon-registry checks (`isIconName` / `hasIcon`) at connect race the async manifest on static (non-Vite) deploys**, pair with `whenIconRegistryReady` deferred re-stamping.
712
711
 
713
712
  ### Registration / packaging class
714
713
 
715
- - **Composites render internal `*-ui` tags the author never wrote** (`chat-input-ui` stamps `textarea-ui` + `select-ui`) a consumer importing only the composite must also import those primitives (or the cluster barrel `packages/web-modules/<cluster>/index.js`) or they stay unregistered and collapse to 0px.
716
- - **A NEW yaml prop with `reflect: true` is not load-bearing alone** the class's `static properties` block is hand-authored, not codegen'd from yaml; add both.
717
- - **Every `[slot="X"]` a component's CSS positions MUST be declared in the yaml `slots:` block** (including auto-stamped internal slots) the slot-vocab-vs-CSS audit in `npm run check` fails CRITICAL otherwise.
718
- - **A new primitive's CSS must be registered in `packages/web-components/styles/components.css`** (the hand-maintained CSS aggregator) separate from the `components/index.js` JS barrel; miss it and the element registers but renders unstyled.
714
+ - **Composites render internal `*-ui` tags the author never wrote** (`chat-input-ui` stamps `textarea-ui` + `select-ui`), a consumer importing only the composite must also import those primitives (or the cluster barrel `packages/web-modules/<cluster>/index.js`) or they stay unregistered and collapse to 0px.
715
+ - **A NEW yaml prop with `reflect: true` is not load-bearing alone**: the class's `static properties` block is hand-authored, not codegen'd from yaml; add both.
716
+ - **Every `[slot="X"]` a component's CSS positions MUST be declared in the yaml `slots:` block** (including auto-stamped internal slots), the slot-vocab-vs-CSS audit in `npm run check` fails CRITICAL otherwise.
717
+ - **A new primitive's CSS must be registered in `packages/web-components/styles/components.css`** (the hand-maintained CSS aggregator), separate from the `components/index.js` JS barrel; miss it and the element registers but renders unstyled.
719
718
  - **A primitive that programmatically creates a form-bearing primitive needs a `COMPOSITION_PAIR` entry in `scripts/release/check-composes-passthrough.mjs`** or the passthrough gate flags it.
720
719
 
721
720
  ## Meta-pattern: the "just this once" trap
722
721
 
723
- All of these bugs started as "just this once" exceptions a component author thought "I'll fix it later," or "this component is special," or "the linter isn't catching it so it's fine." Each "just this once" became a template for the next author.
722
+ All of these bugs started as "just this once" exceptions, a component author thought "I'll fix it later," or "this component is special," or "the linter isn't catching it so it's fine." Each "just this once" became a template for the next author.
724
723
 
725
724
  The audit found 100 findings in iteration 1. Fifteen were novel; eighty-five were different authors cargo-culting the same five shapes.
726
725