@adia-ai/adia-ui-forge 0.1.2
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 +23 -0
- package/CHANGELOG.md +26 -0
- package/README.md +54 -0
- package/bin/forge-lint +263 -0
- package/bin/lib/audit-axes.mjs +555 -0
- package/bin/lib/dry-run-irreversible.mjs +236 -0
- package/bin/lib/run-skill-evals.mjs +487 -0
- package/bin/lib/teach-router.mjs +250 -0
- package/commands/adia-forge-a2ui.md +10 -0
- package/commands/adia-forge-author.md +10 -0
- package/commands/adia-forge-dogfood.md +8 -0
- package/commands/adia-forge-llm.md +8 -0
- package/commands/adia-forge-orient.md +10 -0
- package/commands/adia-forge-release.md +8 -0
- package/commands/adia-forge-review.md +10 -0
- package/hooks/hooks.json +15 -0
- package/package.json +41 -0
- package/references/shared/content-trust.md +76 -0
- package/references/shared/pev-rationale.md +64 -0
- package/references/shared/skill-conventions.md +133 -0
- package/skills/adia-ui-a2ui/CHANGELOG.md +32 -0
- package/skills/adia-ui-a2ui/SKILL.md +243 -0
- package/skills/adia-ui-a2ui/evals/adversarial-corpus.json +75 -0
- package/skills/adia-ui-a2ui/evals/routing-corpus.json +31 -0
- package/skills/adia-ui-a2ui/evals/teach-routing-cases.json +100 -0
- package/skills/adia-ui-a2ui/references/anti-patterns.md +24 -0
- package/skills/adia-ui-a2ui/references/chunk-authoring.md +88 -0
- package/skills/adia-ui-a2ui/references/corpus-discipline.md +56 -0
- package/skills/adia-ui-a2ui/references/eval-diagnostics.md +127 -0
- package/skills/adia-ui-a2ui/references/fragment-graph.md +91 -0
- package/skills/adia-ui-a2ui/references/mcp-pipeline-ops.md +106 -0
- package/skills/adia-ui-a2ui/references/mcp-tool-reference.md +398 -0
- package/skills/adia-ui-a2ui/references/pipeline-overview.md +175 -0
- package/skills/adia-ui-a2ui/references/semantic-fail-lifting.md +120 -0
- package/skills/adia-ui-a2ui/references/strategy-engines.md +111 -0
- package/skills/adia-ui-a2ui/references/teach-protocol.md +220 -0
- package/skills/adia-ui-a2ui/references/zettel-calibration.md +93 -0
- package/skills/adia-ui-a2ui/scripts/audit-a2ui-roster.mjs +96 -0
- package/skills/adia-ui-a2ui/scripts/teach-route.mjs +157 -0
- package/skills/adia-ui-a2ui/skill.json +38 -0
- package/skills/adia-ui-authoring/CHANGELOG.md +32 -0
- package/skills/adia-ui-authoring/SKILL.md +256 -0
- package/skills/adia-ui-authoring/assets/case-studies/admin-shell-decomposition.md +101 -0
- package/skills/adia-ui-authoring/assets/case-studies/maxtokens-32768-discovery.md +109 -0
- package/skills/adia-ui-authoring/assets/case-studies/theme-panel-promotion.md +113 -0
- package/skills/adia-ui-authoring/evals/adversarial-design-plan-gates.json +138 -0
- package/skills/adia-ui-authoring/evals/routing-corpus.json +245 -0
- package/skills/adia-ui-authoring/references/anti-patterns.md +606 -0
- package/skills/adia-ui-authoring/references/api-contract.md +205 -0
- package/skills/adia-ui-authoring/references/authoring-cycle.md +211 -0
- package/skills/adia-ui-authoring/references/canonical-pattern-index.md +179 -0
- package/skills/adia-ui-authoring/references/code-style.md +329 -0
- package/skills/adia-ui-authoring/references/common-gotchas.md +93 -0
- package/skills/adia-ui-authoring/references/composite-demo-protocol.md +1084 -0
- package/skills/adia-ui-authoring/references/css-patterns.md +364 -0
- package/skills/adia-ui-authoring/references/lifecycle-patterns.md +302 -0
- package/skills/adia-ui-authoring/references/llm-bridge.md +254 -0
- package/skills/adia-ui-authoring/references/module-promotion.md +289 -0
- package/skills/adia-ui-authoring/references/primitive-audit.md +123 -0
- package/skills/adia-ui-authoring/references/shell-patterns.md +561 -0
- package/skills/adia-ui-authoring/references/teach-protocol.md +428 -0
- package/skills/adia-ui-authoring/references/token-contract.md +120 -0
- package/skills/adia-ui-authoring/references/worked-example.md +351 -0
- package/skills/adia-ui-authoring/references/yaml-contract.md +224 -0
- package/skills/adia-ui-authoring/scripts/audit-authoring-roster.mjs +148 -0
- package/skills/adia-ui-authoring/scripts/build-canonical-pattern-index.mjs +199 -0
- package/skills/adia-ui-authoring/skill.json +45 -0
- package/skills/adia-ui-dogfood/CHANGELOG.md +17 -0
- package/skills/adia-ui-dogfood/README.md +62 -0
- package/skills/adia-ui-dogfood/SKILL.md +866 -0
- package/skills/adia-ui-dogfood/scripts/analyze.mjs +603 -0
- package/skills/adia-ui-dogfood/skill.json +40 -0
- package/skills/adia-ui-forge/SKILL.md +88 -0
- package/skills/adia-ui-forge/evals/routing-corpus.json +30 -0
- package/skills/adia-ui-gen-review/CHANGELOG.md +108 -0
- package/skills/adia-ui-gen-review/SKILL.md +266 -0
- package/skills/adia-ui-gen-review/references/loop-protocol.md +712 -0
- package/skills/adia-ui-gen-review/references/rubric-cosmetic.md +144 -0
- package/skills/adia-ui-gen-review/references/rubric-decompose.md +117 -0
- package/skills/adia-ui-gen-review/references/rubric-score.md +249 -0
- package/skills/adia-ui-gen-review/references/scores.schema.json +125 -0
- package/skills/adia-ui-gen-review/references/teach-protocol.md +214 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-coverage-audit.mjs +127 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-decompose.mjs +569 -0
- package/skills/adia-ui-gen-review/scripts/gen-review-status.mjs +176 -0
- package/skills/adia-ui-gen-review/scripts/validate-cycle-scores.mjs +198 -0
- package/skills/adia-ui-gen-review/skill.json +22 -0
- package/skills/adia-ui-llm/CHANGELOG.md +25 -0
- package/skills/adia-ui-llm/SKILL.md +165 -0
- package/skills/adia-ui-llm/evals/adversarial-corpus.json +75 -0
- package/skills/adia-ui-llm/evals/routing-corpus.json +30 -0
- package/skills/adia-ui-llm/evals/teach-routing-cases.json +73 -0
- package/skills/adia-ui-llm/references/adapter-contract.md +99 -0
- package/skills/adia-ui-llm/references/add-a-provider.md +90 -0
- package/skills/adia-ui-llm/references/bridge-facade.md +94 -0
- package/skills/adia-ui-llm/references/browser-proxy-boundary.md +89 -0
- package/skills/adia-ui-llm/references/model-registry.md +75 -0
- package/skills/adia-ui-llm/references/streaming-sse.md +95 -0
- package/skills/adia-ui-llm/references/teach-protocol.md +78 -0
- package/skills/adia-ui-llm/scripts/audit-llm-roster.mjs +93 -0
- package/skills/adia-ui-llm/scripts/teach-route.mjs +119 -0
- package/skills/adia-ui-llm/skill.json +33 -0
- package/skills/adia-ui-release/CHANGELOG.md +23 -0
- package/skills/adia-ui-release/SKILL.md +295 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-batch-push-v0.6.14-v0.6.15.md +144 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-corpus-drift-remediation-v0.6.15.md +155 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-20-version-skip-correction-v0.6.12.md +114 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-author-from-scratch-v0.6.18.md +139 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-feedback37-retraction-v0.6.21.md +124 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-fn1-enrichment-pass-v0.6.19.md +125 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-21-stale-test-detection-v0.6.20.md +142 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-23-freshness-gate-recovery-v0.6.32.md +97 -0
- package/skills/adia-ui-release/assets/case-studies/2026-05-26-catalog-drift-recurring-v0.6.40.md +147 -0
- package/skills/adia-ui-release/assets/templates/stub-changelog.template.md +22 -0
- package/skills/adia-ui-release/evals/evals.json +164 -0
- package/skills/adia-ui-release/references/changelog-discipline.md +250 -0
- package/skills/adia-ui-release/references/cycle-happy-path.md +520 -0
- package/skills/adia-ui-release/references/exe-deploy.md +149 -0
- package/skills/adia-ui-release/references/gates-catalog.md +778 -0
- package/skills/adia-ui-release/references/ledger-discipline.md +232 -0
- package/skills/adia-ui-release/references/migration-guide-authoring.md +174 -0
- package/skills/adia-ui-release/references/multi-agent-baseline.md +207 -0
- package/skills/adia-ui-release/references/notes-authoring.md +212 -0
- package/skills/adia-ui-release/references/recovery-paths.md +215 -0
- package/skills/adia-ui-release/references/rollup-notes.md +208 -0
- package/skills/adia-ui-release/references/teach-protocol.md +468 -0
- package/skills/adia-ui-release/scripts/assert-monorepo-root.mjs +49 -0
- package/skills/adia-ui-release/scripts/audit-gate-roster.mjs +196 -0
- package/skills/adia-ui-release/scripts/bump.mjs +118 -0
- package/skills/adia-ui-release/scripts/dispatch-publish.mjs +151 -0
- package/skills/adia-ui-release/scripts/insert-stub.mjs +127 -0
- package/skills/adia-ui-release/scripts/make-ledger.mjs +179 -0
- package/skills/adia-ui-release/scripts/promote-unreleased.mjs +88 -0
- package/skills/adia-ui-release/scripts/release-pack.mjs +350 -0
- package/skills/adia-ui-release/scripts/tag-lockstep.mjs +110 -0
- package/skills/adia-ui-release/skill.json +75 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
# Anti-Pattern Catalogue
|
|
2
|
+
|
|
3
|
+
Every entry here is a real bug caught in the framework's 5-iteration coherence audit. They're named and cited so future authors recognize the _shape_ of the mistake before repeating it.
|
|
4
|
+
|
|
5
|
+
Organized by axis.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## API / Attributes
|
|
10
|
+
|
|
11
|
+
### AP-01 · `default: true` on a Boolean prop
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
// WRONG
|
|
15
|
+
closable: { type: Boolean, default: true, reflect: true }
|
|
16
|
+
```
|
|
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).
|
|
19
|
+
|
|
20
|
+
**Fix:** flip the name. Default `true` behavior = name the prop for the opt-out.
|
|
21
|
+
|
|
22
|
+
| Before | After |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `modal-ui[closable]` (default `true`) | `modal-ui[permanent]` (default `false`) |
|
|
25
|
+
| `skeleton-ui[animate]` | `skeleton-ui[static]` |
|
|
26
|
+
| `stream-ui[cursor]` | `stream-ui[hideCursor]` |
|
|
27
|
+
| `chart-ui[average]` | `chart-ui[hideAverage]` |
|
|
28
|
+
| `toggle-group-ui[multiple]` | `toggle-group-ui[single]` |
|
|
29
|
+
| `swiper-ui[pause-on-hover]` | `swiper-ui[noPauseOnHover]` |
|
|
30
|
+
|
|
31
|
+
**Recognition:** if you find yourself writing `default: true` in a `static properties` block, stop — the name is wrong.
|
|
32
|
+
|
|
33
|
+
### AP-02 · Magic-value sentinel instead of `null`
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// WRONG
|
|
37
|
+
value: { type: Number, default: -1, reflect: true } // -1 means "indeterminate"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**What went wrong:** `-1` is a magic number. Consumers branch on `this.value !== -1`, which breaks the day someone assigns `-1` to mean something else, or decides `-0.5` is also indeterminate.
|
|
41
|
+
|
|
42
|
+
**Fix:** `null` for indeterminate. Check `value == null`.
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
value: { type: Number, default: null, reflect: true }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Real fix:** `progress-ui.value` carried `-1` for months; fixed with a back-compat coercion (`if (v === -1) v = null`) to avoid breaking existing consumers.
|
|
49
|
+
|
|
50
|
+
### AP-03 · `attr:` silent typo
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
// WRONG — silently ignored
|
|
54
|
+
allowHalf: { type: Boolean, default: false, reflect: true, attr: 'allow-half' }
|
|
55
|
+
```
|
|
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.
|
|
58
|
+
|
|
59
|
+
**Fix:** `attribute:` always.
|
|
60
|
+
|
|
61
|
+
**Real fix:** `rating-ui` had this typo for several commits. Found by grep in axis-1 audit.
|
|
62
|
+
|
|
63
|
+
### AP-04 · `title` as a prop name
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<cot-ui title="Analyzing intent"></cot-ui>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**What went wrong:** HTML's global `title` attribute is the browser-native tooltip. A custom element with a `title` attribute gets a tooltip on hover by default, which is almost never intended.
|
|
70
|
+
|
|
71
|
+
**Fix:** rename to `heading` (or something domain-specific that doesn't collide).
|
|
72
|
+
|
|
73
|
+
### AP-05 · `active` on a parent component
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<timeline-ui active="3"> <!-- parent taking an index -->
|
|
77
|
+
<timeline-item-ui active> <!-- child per-item state -->
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**What went wrong:** `active` on the parent carries a non-boolean payload (an index or a selection). The name `active` implies a Boolean.
|
|
81
|
+
|
|
82
|
+
**Fix:** rename by intent:
|
|
83
|
+
|
|
84
|
+
- Parent holds a current index → `step` (e.g. `timeline-ui[step="3"]`)
|
|
85
|
+
- Parent holds a selection → `value` (e.g. `inspector-ui[value="item-42"]`)
|
|
86
|
+
- Child Boolean state stays as `active` — that's fine.
|
|
87
|
+
|
|
88
|
+
### AP-06 · `error` as a variant
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<tag-ui variant="error">Failed</tag-ui>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**What went wrong:** `error` is the validation-state family (`[error]` on inputs; `--a-error-*` tokens). A variant named `error` conflates visual variant with validation state.
|
|
95
|
+
|
|
96
|
+
**Fix:** use the semantic-family name for the variant: `danger` (matches `--a-danger-*` tokens). Reserve `error` for validation state.
|
|
97
|
+
|
|
98
|
+
### AP-07 · `disabled` on a non-form component
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<noodles-ui disabled>...</noodles-ui> <!-- diagram, not form-participating -->
|
|
102
|
+
```
|
|
103
|
+
|
|
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
|
+
|
|
106
|
+
**Fix:** `readonly` — matches the actual intent.
|
|
107
|
+
|
|
108
|
+
### AP-08 · `multiple` with exclusion semantics
|
|
109
|
+
|
|
110
|
+
```html
|
|
111
|
+
<toggle-group-ui multiple> <!-- default: multi-select -->
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**What went wrong:** implies multi-select is the default (matches HTML's `<select multiple>`). But then the prop violates the Boolean-default-false rule (AP-01).
|
|
115
|
+
|
|
116
|
+
**Fix:** negate to `single` so absent-attribute = multi-select default = Boolean false.
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<toggle-group-ui> <!-- default: multi-select -->
|
|
120
|
+
<toggle-group-ui single> <!-- opt-in to single-select -->
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Tokens
|
|
126
|
+
|
|
127
|
+
### AP-T1 · Variant body consumes L2 directly
|
|
128
|
+
|
|
129
|
+
```css
|
|
130
|
+
/* WRONG — button.css */
|
|
131
|
+
:scope[variant="danger"]:not([disabled]):hover {
|
|
132
|
+
--button-fg: var(--a-danger); /* L2 */
|
|
133
|
+
--button-border: var(--a-danger); /* L2 */
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**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.
|
|
138
|
+
|
|
139
|
+
**Fix:** alias from the L3 matrix:
|
|
140
|
+
|
|
141
|
+
```css
|
|
142
|
+
:scope[variant="danger"]:not([disabled]):hover {
|
|
143
|
+
--button-fg: var(--a-danger-fg-hover);
|
|
144
|
+
--button-border: var(--a-danger-border-hover);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or, if the component already defines state-aware tokens in `:where(:scope)`, reuse those:
|
|
149
|
+
|
|
150
|
+
```css
|
|
151
|
+
:scope[variant="danger"]:not([disabled]):hover {
|
|
152
|
+
--button-fg: var(--button-fg-hover);
|
|
153
|
+
--button-border: var(--button-border-hover);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### AP-T2 · Raw hex / rgb / oklch in component CSS
|
|
158
|
+
|
|
159
|
+
```css
|
|
160
|
+
/* WRONG */
|
|
161
|
+
:scope {
|
|
162
|
+
background: #f4f4f5;
|
|
163
|
+
color: oklch(0.2 0.01 264);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**What went wrong:** theme/scheme/contrast switching can't reach a hardcoded color. The component breaks silently in dark mode.
|
|
168
|
+
|
|
169
|
+
**Fix:** consume tokens. Always.
|
|
170
|
+
|
|
171
|
+
### AP-T3 · Raw px ≥ 3 without justification
|
|
172
|
+
|
|
173
|
+
```css
|
|
174
|
+
/* WRONG — 12px is neither a stroke nor intrinsic */
|
|
175
|
+
:scope { padding: 12px; }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Fix:** use the spacing scale.
|
|
179
|
+
|
|
180
|
+
```css
|
|
181
|
+
:scope { padding: var(--a-space-3); }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Allowed carve-out** (with comment):
|
|
185
|
+
|
|
186
|
+
```css
|
|
187
|
+
:where(:scope) {
|
|
188
|
+
/* Component-intrinsic visual constant; no --a-space-* equivalent */
|
|
189
|
+
--noodles-port-size: 10px;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### AP-T4 · Component token name mismatches scope
|
|
194
|
+
|
|
195
|
+
```css
|
|
196
|
+
/* WRONG — button.css */
|
|
197
|
+
@scope (button-ui) {
|
|
198
|
+
:where(:scope) {
|
|
199
|
+
--btn-bg: var(--a-accent-bg); /* ← stem mismatch */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Fix:** stem matches the scope's tag-stem exactly.
|
|
205
|
+
|
|
206
|
+
```css
|
|
207
|
+
@scope (button-ui) {
|
|
208
|
+
:where(:scope) {
|
|
209
|
+
--button-bg: var(--a-accent-bg);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## CSS Patterns
|
|
217
|
+
|
|
218
|
+
### AP-S1 · Variant body sets layout properties
|
|
219
|
+
|
|
220
|
+
```css
|
|
221
|
+
/* WRONG — pagination.css (pre-fix) */
|
|
222
|
+
:scope[variant="button"] [slot="nav"] button {
|
|
223
|
+
width: var(--pagination-button-size);
|
|
224
|
+
height: var(--pagination-button-size);
|
|
225
|
+
padding: 0;
|
|
226
|
+
border-radius: var(--pagination-radius);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**What went wrong:** `variant="button"` changes the layout (square buttons with specific dimensions) — that's a mode, not a variant.
|
|
231
|
+
|
|
232
|
+
**Fix:** add `pagination-ui[variant="button"]` to the Sanctioned Mode Attributes table in `docs/specs/component-token-contract.md`. Document the layout change as intentional.
|
|
233
|
+
|
|
234
|
+
Alternative: refactor to make the attribute cosmetic — if possible.
|
|
235
|
+
|
|
236
|
+
### AP-S2 · Single-block `:scope` (tokens and styles interleaved)
|
|
237
|
+
|
|
238
|
+
```css
|
|
239
|
+
/* WRONG — missing the :where(:scope) token block */
|
|
240
|
+
@scope (button-ui) {
|
|
241
|
+
:scope {
|
|
242
|
+
--button-bg: var(--a-accent-bg);
|
|
243
|
+
background: var(--button-bg);
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**What went wrong:** tokens inherit `:scope`'s `(0,1,0)` specificity. Parent overrides fail because they can't out-specify.
|
|
250
|
+
|
|
251
|
+
**Fix:** two blocks.
|
|
252
|
+
|
|
253
|
+
### AP-S3 · `::slotted()` in light-DOM component
|
|
254
|
+
|
|
255
|
+
```css
|
|
256
|
+
/* WRONG — shadow DOM syntax */
|
|
257
|
+
::slotted([slot="icon"]) { margin-inline-end: var(--gap); }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Fix:** attribute selector on child.
|
|
261
|
+
|
|
262
|
+
```css
|
|
263
|
+
:scope > [slot="icon"] { margin-inline-end: var(--component-gap); }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### AP-S4 · `!important`
|
|
267
|
+
|
|
268
|
+
Ever. If you need it, specificity is wrong somewhere. Fix the layering.
|
|
269
|
+
|
|
270
|
+
### AP-S5 · Inline `grid-template-columns` for asymmetric splits
|
|
271
|
+
|
|
272
|
+
```html
|
|
273
|
+
<!-- WRONG — escape hatch that bypasses the grid-ui vocabulary -->
|
|
274
|
+
<grid-ui gap="4" style="grid-template-columns: 2fr 1fr">
|
|
275
|
+
<card-ui>Chart</card-ui>
|
|
276
|
+
<card-ui>Sidebar</card-ui>
|
|
277
|
+
</grid-ui>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**What went wrong:** the inline style works visually but opts out of the component's attribute API. It doesn't reflect to `[style]`-based selectors, doesn't compose with theme overrides, and reads as a local hack when agents scan the page for grid conventions. Every inline style is a precedent the next author cargo-cults.
|
|
281
|
+
|
|
282
|
+
**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"`.
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
<!-- RIGHT — 2:1 via columns=3 and a span=2 child -->
|
|
286
|
+
<grid-ui columns="3" gap="4">
|
|
287
|
+
<card-ui span="2">Chart</card-ui>
|
|
288
|
+
<card-ui>Sidebar</card-ui>
|
|
289
|
+
</grid-ui>
|
|
290
|
+
|
|
291
|
+
<!-- RIGHT — 3:2 via columns=5 and span=3 + span=2 -->
|
|
292
|
+
<grid-ui columns="5" gap="4">
|
|
293
|
+
<card-ui span="3">Overview</card-ui>
|
|
294
|
+
<card-ui span="2">Recent Sales</card-ui>
|
|
295
|
+
</grid-ui>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**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.
|
|
299
|
+
|
|
300
|
+
**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.
|
|
301
|
+
|
|
302
|
+
### AP-S6 · `:scope >` child combinator on a conditionally-rendered part
|
|
303
|
+
|
|
304
|
+
```css
|
|
305
|
+
/* WRONG — [data-part="empty"] is rendered behind a `${isEmpty ? … : null}` branch */
|
|
306
|
+
:scope > [data-part="empty"] { display: grid; place-items: center; }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**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.
|
|
310
|
+
|
|
311
|
+
**Fix:** use a **descendant** combinator for any conditionally-rendered part; keep `:scope >` only for parts that render unconditionally (those stay direct children).
|
|
312
|
+
|
|
313
|
+
```css
|
|
314
|
+
/* RIGHT — descendant combinator survives the display:contents wrapper */
|
|
315
|
+
:scope [data-part="empty"] { display: grid; place-items: center; }
|
|
316
|
+
:scope > [data-part="header"] { … } /* static part — child combinator OK */
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**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 >`".
|
|
320
|
+
|
|
321
|
+
**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.
|
|
322
|
+
|
|
323
|
+
### AP-S7 · `align-self: stretch` to vertically center a fixed-height flex child
|
|
324
|
+
|
|
325
|
+
```css
|
|
326
|
+
/* WRONG — stretch can't size a child that carries a definite height */
|
|
327
|
+
[slot="field"] > [slot="trailing"] { align-self: stretch; } /* a 20px <kbd-ui> pins to the TOP */
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**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).
|
|
331
|
+
|
|
332
|
+
**Fix:** to vertically center a row of mixed-height affordances, use `align-self: center` and let each child keep its own token height.
|
|
333
|
+
|
|
334
|
+
```css
|
|
335
|
+
/* RIGHT — center keeps each child's intrinsic height on the vertical center */
|
|
336
|
+
[slot="field"] > [slot="trailing"] { align-self: center; }
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**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.
|
|
340
|
+
|
|
341
|
+
**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).
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## JS Lifecycle
|
|
346
|
+
|
|
347
|
+
### AP-L1 · Inline arrow passed to `addEventListener`
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
// WRONG — cot.js (pre-fix)
|
|
351
|
+
this.#summaryEl.addEventListener('click', () => {
|
|
352
|
+
this.collapsed = !this.collapsed;
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**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.
|
|
357
|
+
|
|
358
|
+
**Fix:** stable `#field` arrow.
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
#onSummaryClick = () => {
|
|
362
|
+
this.collapsed = !this.collapsed;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
connected() {
|
|
366
|
+
this.#summaryEl.addEventListener('click', this.#onSummaryClick);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
disconnected() {
|
|
370
|
+
this.#summaryEl?.removeEventListener('click', this.#onSummaryClick);
|
|
371
|
+
this.#summaryEl = null;
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### AP-L2 · Duplicate `disconnected()` method
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
// WRONG — chart.js (pre-fix)
|
|
379
|
+
class AdiaChart extends AdiaElement {
|
|
380
|
+
connected() { /* ... */ this.#ro = new ResizeObserver(...); }
|
|
381
|
+
|
|
382
|
+
disconnected() {
|
|
383
|
+
this.#ro?.disconnect(); // ← this one gets SHADOWED
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ... 100 lines later ...
|
|
387
|
+
|
|
388
|
+
disconnected() {
|
|
389
|
+
// second declaration silently wins; ResizeObserver never disconnected
|
|
390
|
+
this.#tooltipEl?.remove();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**What went wrong:** JavaScript silently takes the last method definition. The first `disconnected()` (with the observer cleanup) is discarded. No error, no warning.
|
|
396
|
+
|
|
397
|
+
**Fix:** merge into one method. Always.
|
|
398
|
+
|
|
399
|
+
```javascript
|
|
400
|
+
disconnected() {
|
|
401
|
+
this.#ro?.disconnect();
|
|
402
|
+
this.#ro = null;
|
|
403
|
+
this.#tooltipEl?.remove();
|
|
404
|
+
this.#tooltipEl = null;
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### AP-L3 · Missing `super.connected()` / `super.disconnected()`
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
// WRONG — AdiaFormElement subclass missing super call
|
|
412
|
+
class AdiaInput extends AdiaFormElement {
|
|
413
|
+
connected() {
|
|
414
|
+
// super.connected() NOT called
|
|
415
|
+
this.#inputEl = this.querySelector('input');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**What went wrong:** `ElementInternals` registration skipped. The component doesn't participate in forms — no value submission, no validation, no form-reset handling. Silent.
|
|
421
|
+
|
|
422
|
+
**Fix:** `super.connected()` first line of the method. `super.disconnected()` first line of its method too.
|
|
423
|
+
|
|
424
|
+
### AP-L4 · Listener added to global `document` without cleanup
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
// WRONG
|
|
428
|
+
connected() {
|
|
429
|
+
document.addEventListener('click', this.#onDocumentClick);
|
|
430
|
+
// disconnected() doesn't removeEventListener
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**What went wrong:** the listener survives the component. Every time the component mounts, another listener is added. Over an SPA's lifetime, dozens accumulate. Each fires on every document click.
|
|
435
|
+
|
|
436
|
+
**Fix:** mirror the addListener with a removeListener in `disconnected()`.
|
|
437
|
+
|
|
438
|
+
### AP-L5 · Timer not cleared
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
// WRONG
|
|
442
|
+
connected() {
|
|
443
|
+
this.#timer = setInterval(() => this.tick(), 1000);
|
|
444
|
+
}
|
|
445
|
+
// disconnected() doesn't clearInterval
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**What went wrong:** timer keeps firing on a removed component. Can cause null-dereferences (DOM refs are nulled), phantom renders, or memory leaks.
|
|
449
|
+
|
|
450
|
+
**Fix:** `clearInterval(this.#timer)` in `disconnected()`.
|
|
451
|
+
|
|
452
|
+
### AP-L6 · Observer recreated without disconnecting old one
|
|
453
|
+
|
|
454
|
+
```javascript
|
|
455
|
+
// WRONG
|
|
456
|
+
connected() {
|
|
457
|
+
this.#ro = new ResizeObserver(...);
|
|
458
|
+
this.#ro.observe(this);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Later — new options, recreated
|
|
462
|
+
updateConfig(opts) {
|
|
463
|
+
this.#ro = new ResizeObserver(...); // ← old one leaks
|
|
464
|
+
this.#ro.observe(this);
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Fix:** disconnect first, then recreate.
|
|
469
|
+
|
|
470
|
+
```javascript
|
|
471
|
+
updateConfig(opts) {
|
|
472
|
+
this.#ro?.disconnect();
|
|
473
|
+
this.#ro = new ResizeObserver(...);
|
|
474
|
+
this.#ro.observe(this);
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### AP-L7 · Empty template wipes authored light-DOM children
|
|
479
|
+
|
|
480
|
+
```javascript
|
|
481
|
+
// WRONG — drawer.js (pre-fix)
|
|
482
|
+
class AdiaDrawer extends AdiaElement {
|
|
483
|
+
static template = () => html``; // ← looks harmless. It isn't.
|
|
484
|
+
|
|
485
|
+
connected() {
|
|
486
|
+
this.addEventListener('press', this.#onPress);
|
|
487
|
+
// ... intent: migrate authored [slot="header|body|footer"] into the panel
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**What went wrong:** `AdiaElement`'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.
|
|
493
|
+
|
|
494
|
+
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.
|
|
495
|
+
|
|
496
|
+
**Fix:** drop the override entirely. The base class already defines `static template = () => null;`, and `stamp()` is skipped when the result is null:
|
|
497
|
+
|
|
498
|
+
```javascript
|
|
499
|
+
// RIGHT — no template override
|
|
500
|
+
class AdiaDrawer extends AdiaElement {
|
|
501
|
+
// base AdiaElement.template returns null → stamp() is skipped →
|
|
502
|
+
// authored light-DOM children survive through render().
|
|
503
|
+
|
|
504
|
+
connected() { /* ... */ }
|
|
505
|
+
render() { /* migrate children into the panel */ }
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**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.
|
|
510
|
+
|
|
511
|
+
### AP-L8 · `setAttribute('value', …)` on `input-ui` after first render
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
// WRONG — expected to update the visible text
|
|
515
|
+
input.setAttribute('value', 'Liam Johnson');
|
|
516
|
+
// …user still sees an empty field
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**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.
|
|
520
|
+
|
|
521
|
+
**Fix:** set the attribute AND mirror the text slot when you need to update the displayed value from outside:
|
|
522
|
+
|
|
523
|
+
```javascript
|
|
524
|
+
input.setAttribute('value', 'Liam Johnson');
|
|
525
|
+
const textSlot = input.querySelector('[slot="text"]');
|
|
526
|
+
if (textSlot) textSlot.textContent = 'Liam Johnson';
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**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.
|
|
530
|
+
|
|
531
|
+
**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.
|
|
532
|
+
|
|
533
|
+
### AP-L9 · Radio cards without a `role="radiogroup"` parent
|
|
534
|
+
|
|
535
|
+
```html
|
|
536
|
+
<!-- WRONG — radios never cross-deselect -->
|
|
537
|
+
<list-ui>
|
|
538
|
+
<list-item-ui>
|
|
539
|
+
<card-ui>
|
|
540
|
+
<header>
|
|
541
|
+
<radio-ui slot="icon" name="plan" value="pro" checked></radio-ui>
|
|
542
|
+
<span slot="heading">Pro</span>
|
|
543
|
+
</header>
|
|
544
|
+
</card-ui>
|
|
545
|
+
</list-item-ui>
|
|
546
|
+
<!-- …more radio-ui name="plan"… -->
|
|
547
|
+
</list-ui>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**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.
|
|
551
|
+
|
|
552
|
+
**Fix:** wrap the whole group in an element `role="radiogroup"`:
|
|
553
|
+
|
|
554
|
+
```html
|
|
555
|
+
<div role="radiogroup" aria-label="Plan" data-plan-picker
|
|
556
|
+
onclick="const r = event.target.closest('card-ui')?.querySelector('radio-ui'); r && r.click()">
|
|
557
|
+
<list-ui divider>
|
|
558
|
+
<list-item-ui>
|
|
559
|
+
<card-ui>
|
|
560
|
+
<header>
|
|
561
|
+
<radio-ui slot="icon" name="plan" value="pro" checked></radio-ui>
|
|
562
|
+
<span slot="heading">Pro · $49/mo</span>
|
|
563
|
+
<span slot="description">…</span>
|
|
564
|
+
</header>
|
|
565
|
+
</card-ui>
|
|
566
|
+
</list-item-ui>
|
|
567
|
+
<!-- additional options -->
|
|
568
|
+
</list-ui>
|
|
569
|
+
</div>
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
The inline delegate on the group lets users click anywhere on a card to toggle its radio; the radio's own click handler no-ops on a card-click when it's already checked.
|
|
573
|
+
|
|
574
|
+
**Recognition:** any time `radio-ui`s with the same `name` are separated by container elements (cards, list-items, grid cells), they need a `role="radiogroup"` ancestor or they won't behave as a group.
|
|
575
|
+
|
|
576
|
+
### AP-L10 · Popover / tooltip not removed from `document.body`
|
|
577
|
+
|
|
578
|
+
```javascript
|
|
579
|
+
// WRONG
|
|
580
|
+
showTooltip() {
|
|
581
|
+
this.#tip = document.createElement('div');
|
|
582
|
+
document.body.appendChild(this.#tip);
|
|
583
|
+
// disconnected() doesn't remove #tip
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**What went wrong:** tip stays in `<body>` after the component is destroyed. Visible zombie element.
|
|
588
|
+
|
|
589
|
+
**Fix:**
|
|
590
|
+
|
|
591
|
+
```javascript
|
|
592
|
+
disconnected() {
|
|
593
|
+
this.#tip?.remove();
|
|
594
|
+
this.#tip = null;
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## Meta-pattern: the "just this once" trap
|
|
601
|
+
|
|
602
|
+
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.
|
|
603
|
+
|
|
604
|
+
The audit found 100 findings in iteration 1. Fifteen were novel; eighty-five were different authors cargo-culting the same five shapes.
|
|
605
|
+
|
|
606
|
+
**Convention is an exponential.** Breaking it once multiplies; enforcing it once multiplies in the other direction. Treat every authoring decision as setting a precedent, because it is.
|