@adia-ai/adia-ui-factory 0.8.55 → 0.8.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +19 -3
- package/__init__.py +6 -1
- package/agents/surface-qa-agent.md +1 -0
- package/commands/theme-audit.md +28 -0
- package/hermes-mcp.yaml +1 -1
- package/package.json +4 -1
- package/plugin.yaml +1 -1
- package/prompts/theme-audit.md +25 -0
- package/scripts/adia-probe.mjs +89 -13
- package/scripts/adia-theme-audit.mjs +1637 -0
- package/scripts/theme-audit-fixtures/expected.json +16 -0
- package/scripts/theme-audit-fixtures/fixture-app/index.html +15 -0
- package/scripts/theme-audit-fixtures/fixture-app/sitemap.json +6 -0
- package/scripts/theme-audit-fixtures/fixture-theme.css +99 -0
- package/skills/find-unused/SKILL.md +3 -1
- package/skills/pattern-catalog/references/annotations.yaml +28 -0
- package/skills/pattern-catalog/references/pattern-index.md +19 -4
- package/skills/table-composition/references/base-table.md +1 -1
- package/skills/theme-audit/SKILL.md +129 -0
- package/skills/theme-audit/agents/openai.yaml +3 -0
- package/skills/theme-audit/evals/evals.json +20 -0
- package/skills/theme-audit/evals/routing-corpus.json +162 -0
- package/skills/theme-audit/references/report-shape.md +84 -0
- package/skills/token-selection/references/a-alias-layer.md +18 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adia-ui-kit-factory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.56",
|
|
4
4
|
"description": "Author and verify apps built ON the adia-ui (@adia-ai) light-DOM web-component framework \u2014 orient, scaffold, compose, wire, verify, and migrate across SPA and SSR rendering modes. Wires the a2ui MCP for catalog retrieval, UI generation, and validation.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Kim",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adia-ui-kit-factory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.56",
|
|
4
4
|
"description": "Author and verify apps built ON the adia-ui (@adia-ai) light-DOM web-component framework — orient, scaffold, compose, wire, verify, and migrate across SPA and SSR rendering modes. Wires the a2ui MCP for catalog retrieval, UI generation, and validation.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Kim",
|
package/.mcp.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog — adia-ui-kit-factory
|
|
2
2
|
|
|
3
|
+
## [0.8.56] — 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`theme-audit` skill, `/theme-audit` command, and `scripts/adia-theme-audit.mjs` runner (lld-0012 B1–B3, gh#2257).** Audits a consumer app's `theme.css` for the four redundancy classes (restated defaults, token re-derivations, dead selectors, hand-built components) across every sitemap route, with a Playwright class-1 toggle and route discovery (#2308, #2312, #2413). Fixtures under `scripts/theme-audit-fixtures/`; report shape in `skills/theme-audit/references/report-shape.md`; Codex/Pi/Hermes manifests derived (`skills/theme-audit/agents/openai.yaml`, `prompts/theme-audit.md`).
|
|
7
|
+
- **`HARNESS-NOTES.md`** pointer for non-Claude harnesses (gh#2449) — points at `AGENTS.md`/README rather than duplicating install prose.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **`scripts/adia-probe.mjs`** gains the route-discovery + class-1 toggle hooks the theme-audit runner drives (#2413); `agents/surface-qa-agent.md` cites the extended probe.
|
|
11
|
+
- **`skills/token-selection/references/a-alias-layer.md`** documents the 15 new `--a-*` aliases from tokens batch C0 and the advisory reroute gate (gh#2240, #2243).
|
|
12
|
+
- **`skills/pattern-catalog/references/{pattern-index.md,annotations.yaml}`** pick up the sidebar-modal pattern (#2226) and the registration/NPI-search and errors surfaces (#2242, #2270); `skills/table-composition/references/base-table.md` follows `table-toolbar-ui`'s count-cluster redesign (#2190); `skills/find-unused/SKILL.md` wording tightened.
|
|
13
|
+
|
|
14
|
+
### Maintenance
|
|
15
|
+
- **`.claude-plugin/plugin.json` version bump** — moves in lockstep with package.json (the `/plugin update` cache key).
|
|
16
|
+
- **`.codex-plugin/` touched in this release window** (1 file(s), e.g. `.codex-plugin/plugin.json`) — carried by the entries above.
|
|
17
|
+
- **`commands/` touched in this release window** (1 file(s), e.g. `commands/theme-audit.md`) — carried by the entries above.
|
|
18
|
+
|
|
3
19
|
## [0.8.55] — 2026-08-28
|
|
4
20
|
|
|
5
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ posture (ADR-0040) is about this plugin working standalone in any consumer
|
|
|
51
51
|
repo once installed some other way (no monorepo path assumptions in its own
|
|
52
52
|
scripts) — it does not add an npm-sourced marketplace entry.
|
|
53
53
|
|
|
54
|
-
## Skills (
|
|
54
|
+
## Skills (20 — `skills/` on disk is the roster; `check:plugin-count-claims` guards this header count)
|
|
55
55
|
|
|
56
56
|
| Skill | Species | Job |
|
|
57
57
|
|---|---|---|
|
|
@@ -74,6 +74,7 @@ scripts) — it does not add an npm-sourced marketplace entry.
|
|
|
74
74
|
| `app-migration` | procedural | version upgrades + port-to-adia sweeps (breaking changes) |
|
|
75
75
|
| `find-unused` | procedural | what a *non*-breaking upgrade left unused — inert opt-in layers, stale workarounds |
|
|
76
76
|
| `surface-qa` | procedural | browser + a11y + composition exit gate |
|
|
77
|
+
| `theme-audit` | procedural | classify a consumer theme.css against the framework's own defaults/tokens/primitives — restated defaults, re-derived tokens, dead selectors, hand-built component work; audit + report only, no rewrites (gh#2257) |
|
|
77
78
|
|
|
78
79
|
## Agents (4)
|
|
79
80
|
|
|
@@ -97,11 +98,20 @@ surface-qa-agent) · `component-model.md` · the `references/contracts/` twins.
|
|
|
97
98
|
|
|
98
99
|
## Commands
|
|
99
100
|
|
|
100
|
-
`/project-scaffolding` · `/app-planning` · `/surface-qa` · `/app-migration` · `/find-unused` · `/gen-ui-wiring` · `/app-audit`
|
|
101
|
+
`/project-scaffolding` · `/app-planning` · `/surface-qa` · `/app-migration` · `/find-unused` · `/gen-ui-wiring` · `/app-audit` · `/theme-audit`
|
|
102
|
+
|
|
103
|
+
## Scripts
|
|
104
|
+
|
|
105
|
+
Consumer-runnable checks shipped as plain Node scripts under `scripts/`, no
|
|
106
|
+
MCP server required: `adia-probe.mjs` (the browser-QA gate; see
|
|
107
|
+
`surface-qa`), `adia-preflight.mjs` (every CI-step prerequisite, each
|
|
108
|
+
failure named with its remedy), `adia-contract-check.mjs` (authored markup
|
|
109
|
+
attributes vs. the shipped component contracts), and `adia-theme-audit.mjs`
|
|
110
|
+
(the theme.css redundancy classifier; see `theme-audit`).
|
|
101
111
|
|
|
102
112
|
## MCP
|
|
103
113
|
|
|
104
|
-
`adia-gen-ui` server pinned: `@adia-ai/mcp@0.8.
|
|
114
|
+
`adia-gen-ui` server pinned: `@adia-ai/mcp@0.8.56` (tool SoT:
|
|
105
115
|
`packages/gen-ui/mcp/TOOLS.md` — the `gen-ui` section; stability rule in
|
|
106
116
|
`references/contracts/`; pin lives in `.mcp.json` — `check:plugin-count-claims`
|
|
107
117
|
guards this README copy against it). The server is `adia-mcp gen-ui`,
|
|
@@ -120,6 +130,12 @@ plug-and-play install** — read this section before promising Pi/Hermes
|
|
|
120
130
|
support for this plugin specifically, since its whole value proposition is
|
|
121
131
|
wiring the `adia-gen-ui` MCP server.
|
|
122
132
|
|
|
133
|
+
`theme-audit`'s `adia-theme-audit` bin runs identically on every harness — a
|
|
134
|
+
plain Node script with no Claude-specific runtime dependency; the
|
|
135
|
+
`/theme-audit` command degrades to invoking the skill directly on Codex and
|
|
136
|
+
Hermes, and becomes a genuine `prompts/theme-audit.md` Pi command via
|
|
137
|
+
`build:harness-manifests`.
|
|
138
|
+
|
|
123
139
|
- **Codex** (gh#1888): `.codex-plugin/plugin.json` + per-skill
|
|
124
140
|
`agents/openai.yaml`, derived; `mcpServers` points at the same
|
|
125
141
|
`.mcp.json` Claude Code uses. No manifest key for hooks/commands/agents —
|
package/__init__.py
CHANGED
|
@@ -263,7 +263,7 @@ def register(ctx):
|
|
|
263
263
|
ctx.register_skill(
|
|
264
264
|
name="find-unused",
|
|
265
265
|
path=Path(os.path.join(_HERE, "skills", "find-unused", "SKILL.md")),
|
|
266
|
-
description="Finds what a non-breaking @adia-ai upgrade shipped that this app never picked up — the opt-in layers nothing imports, the local workarounds a fixed bug made redundant, retired enum values still sitting in stored state, and the fixes that never reached MIGRATION.md. Use after \"we bumped and nothing broke\", \"what are we missing from 0.8.x\", \"are we actually using what we ship\", \"why is [scale]/[theme] doing nothing\", or when a PATCH span needs adoption rather than repair. NOT for breaking-change repair (app-migration); NOT for a first-time install (adia-scaffold); NOT for diagnosing a broken surface (app-audit).",
|
|
266
|
+
description="Finds what a non-breaking @adia-ai upgrade shipped that this app never picked up — the opt-in layers nothing imports, the local workarounds a fixed bug made redundant, retired enum values still sitting in stored state, and the fixes that never reached MIGRATION.md. Use after \"we bumped and nothing broke\", \"what are we missing from 0.8.x\", \"are we actually using what we ship\", \"why is [scale]/[theme] doing nothing\", or when a PATCH span needs adoption rather than repair. NOT for breaking-change repair (app-migration); NOT for a first-time install (adia-scaffold); NOT for diagnosing a broken surface (app-audit); NOT for CSS-only redundancy in a theme file (theme-audit — the CSS-side sibling).",
|
|
267
267
|
)
|
|
268
268
|
ctx.register_skill(
|
|
269
269
|
name="gen-ui-wiring",
|
|
@@ -310,6 +310,11 @@ def register(ctx):
|
|
|
310
310
|
path=Path(os.path.join(_HERE, "skills", "table-composition", "SKILL.md")),
|
|
311
311
|
description="Answers how to display/organize tabular data with table-ui: contained vs uncontained chrome, [raw] (consumer-owned body, a separate axis), opt-in striped rows, and resize/sort defaults that flip between JS .columns and declarative col-def forms, plus inline-edit-grid and tree/hierarchical-row patterns. Use for \"how do I show a table\", \"add sorting/filtering\", \"make this table striped\", \"columns aren't resizable\", \"card vs bare table\", \"editable/spreadsheet grid\", \"tree table / nested rows\", \"huge list of rows\". NOT for composing the surrounding screen (screen-composition), data wiring (data-wiring), OTHER non-table patterns (pattern-catalog), or 1000+-row virtualized lists (list-window-ui).",
|
|
312
312
|
)
|
|
313
|
+
ctx.register_skill(
|
|
314
|
+
name="theme-audit",
|
|
315
|
+
path=Path(os.path.join(_HERE, "skills", "theme-audit", "SKILL.md")),
|
|
316
|
+
description="Classifies a consumer app's theme.css against what the adia-ui framework already ships — restated defaults, re-derived tokens, dead selectors, and hand-built component work. Runs `adia-theme-audit` against a theme file + app root; audits and reports, never rewrites. Use when a theme.css is suspected of restating the kit, after bumping `@adia-ai/web-components`, or before a `find-unused` sweep. NOT for rendering/grading a surface (surface-qa); NOT for picking a token going forward (token-selection); NOT for JS/markup an upgrade left unused (find-unused — the CSS-side sibling); NOT for the framework's OWN token layer in packages/web-components (forge's component-token-audit).",
|
|
317
|
+
)
|
|
313
318
|
ctx.register_skill(
|
|
314
319
|
name="token-selection",
|
|
315
320
|
path=Path(os.path.join(_HERE, "skills", "token-selection", "SKILL.md")),
|
|
@@ -9,6 +9,7 @@ description: |
|
|
|
9
9
|
tools: Read, Grep, Glob, Bash
|
|
10
10
|
skills:
|
|
11
11
|
- surface-qa
|
|
12
|
+
- theme-audit
|
|
12
13
|
# Explicit pin (gh#618, tier corrected gh#1045): a review/critic seat's
|
|
13
14
|
# verdict must not depend on the caller's model tier — never `inherit`.
|
|
14
15
|
# Operator's explicit standing instruction for this seat family: sonnet + xhigh.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
wraps: theme-audit
|
|
3
|
+
description: Classify a consumer app's theme.css against what the adia-ui framework already ships — restated defaults, re-derived tokens, dead selectors, hand-built component work. Audit and report only; no rewrites.
|
|
4
|
+
argument-hint: "[--theme <file>] [--app <root>]"
|
|
5
|
+
disable-model-invocation: false
|
|
6
|
+
user-invocable: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Audit a theme stylesheet. **$ARGUMENTS**
|
|
10
|
+
|
|
11
|
+
Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/adia-theme-audit.mjs" --theme <file>
|
|
12
|
+
--app <root>` (add `--json`/`--json-out <path>` for a structured report,
|
|
13
|
+
`--strict` to exit non-zero on any `high`-confidence finding, `--class
|
|
14
|
+
1,2,3,4` to narrow scope). No `@adia-ai/web-components` resolvable from
|
|
15
|
+
`<app>` → the tool reports `E_NO_FRAMEWORK` with the install remedy; pass
|
|
16
|
+
`--framework-root <checkout>` to audit against an unreleased framework
|
|
17
|
+
checkout instead. No `--base-url`/Playwright unreachable → class 3's
|
|
18
|
+
rendered census degrades to `UNMEASURED` and the run still exits `0`
|
|
19
|
+
(unless `--strict`); static `last-wins` detection still runs.
|
|
20
|
+
|
|
21
|
+
Return the human-readable summary and per-class tables (theme-audit
|
|
22
|
+
§Reading the report). Findings route back to the owning skill — a
|
|
23
|
+
restated default or re-derived token deletes from the theme file directly;
|
|
24
|
+
a hand-built-component finding routes to `screen-composition` to swap in
|
|
25
|
+
the primitive; a dead selector still needs a human's confirmation before
|
|
26
|
+
removal (the census may not have covered every interaction state). Never
|
|
27
|
+
apply a fix inline from this command — theme-audit and its runner never
|
|
28
|
+
rewrite the file, and neither does this wrapper.
|
package/hermes-mcp.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/adia-ui-factory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.56",
|
|
4
4
|
"description": "Author and verify apps built ON the adia-ui (@adia-ai) light-DOM web-component framework — orient, scaffold, compose, wire, verify, and migrate across SPA and SSR rendering modes. Wires the a2ui MCP for catalog retrieval, UI generation, and validation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adia-ui",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"bugs": {
|
|
20
20
|
"email": "kim.granlund@adia.ai"
|
|
21
21
|
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"adia-theme-audit": "./scripts/adia-theme-audit.mjs"
|
|
24
|
+
},
|
|
22
25
|
"pi": {
|
|
23
26
|
"skills": ["./skills"],
|
|
24
27
|
"prompts": ["./prompts"],
|
package/plugin.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
name: "adia-ui-kit-factory"
|
|
2
|
-
version: "0.8.
|
|
2
|
+
version: "0.8.56"
|
|
3
3
|
description: "Author and verify apps built ON the adia-ui (@adia-ai) light-DOM web-component framework — orient, scaffold, compose, wire, verify, and migrate across SPA and SSR rendering modes. Wires the a2ui MCP for catalog retrieval, UI generation, and validation."
|
|
4
4
|
manifest_version: 1
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Classify a consumer app's theme.css against what the adia-ui framework already ships — restated defaults, re-derived tokens, dead selectors, hand-built component work. Audit and report only; no rewrites."
|
|
3
|
+
argument-hint: "[--theme <file>] [--app <root>]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Audit a theme stylesheet. **$ARGUMENTS**
|
|
7
|
+
|
|
8
|
+
Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/adia-theme-audit.mjs" --theme <file>
|
|
9
|
+
--app <root>` (add `--json`/`--json-out <path>` for a structured report,
|
|
10
|
+
`--strict` to exit non-zero on any `high`-confidence finding, `--class
|
|
11
|
+
1,2,3,4` to narrow scope). No `@adia-ai/web-components` resolvable from
|
|
12
|
+
`<app>` → the tool reports `E_NO_FRAMEWORK` with the install remedy; pass
|
|
13
|
+
`--framework-root <checkout>` to audit against an unreleased framework
|
|
14
|
+
checkout instead. No `--base-url`/Playwright unreachable → class 3's
|
|
15
|
+
rendered census degrades to `UNMEASURED` and the run still exits `0`
|
|
16
|
+
(unless `--strict`); static `last-wins` detection still runs.
|
|
17
|
+
|
|
18
|
+
Return the human-readable summary and per-class tables (theme-audit
|
|
19
|
+
§Reading the report). Findings route back to the owning skill — a
|
|
20
|
+
restated default or re-derived token deletes from the theme file directly;
|
|
21
|
+
a hand-built-component finding routes to `screen-composition` to swap in
|
|
22
|
+
the primitive; a dead selector still needs a human's confirmation before
|
|
23
|
+
removal (the census may not have covered every interaction state). Never
|
|
24
|
+
apply a fix inline from this command — theme-audit and its runner never
|
|
25
|
+
rewrite the file, and neither does this wrapper.
|
package/scripts/adia-probe.mjs
CHANGED
|
@@ -72,7 +72,7 @@ export function isLargeText(fontSizePx, fontWeight) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// samples: [{ text, element, fg:[r,g,b], bg:[r,g,b], fontSizePx, fontWeight }]
|
|
75
|
-
export function scoreContrastSamples(samples, { skipped = 0 } = {}) {
|
|
75
|
+
export function scoreContrastSamples(samples, { skipped = 0, unparsed = 0 } = {}) {
|
|
76
76
|
const pairs = samples.map((s) => {
|
|
77
77
|
const large = isLargeText(s.fontSizePx, s.fontWeight);
|
|
78
78
|
const required = large ? 3.0 : 4.5;
|
|
@@ -85,12 +85,16 @@ export function scoreContrastSamples(samples, { skipped = 0 } = {}) {
|
|
|
85
85
|
standard: 'WCAG AA — 4.5:1 normal / 3.0:1 large text (≥18pt, or ≥14pt bold)',
|
|
86
86
|
checked: pairs.length,
|
|
87
87
|
skippedImageBacked: skipped,
|
|
88
|
+
// Sampler health travels with the row even when some pairs parsed: a
|
|
89
|
+
// page with 1 literal rgb() pair and 200 unreadable token colors is a
|
|
90
|
+
// 1/1 pass only in the narrowest sense. Distinct unreadable strings.
|
|
91
|
+
...(unparsed > 0 ? { unparsedColors: unparsed } : {}),
|
|
88
92
|
failing,
|
|
89
93
|
pairs,
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
export function buildProof({ url, errors, boxes, screenshotPath, navTiming, perfBudgetMs = DEFAULT_PERF_BUDGET_MS, contrastSamples = null, contrastSkipped = 0 }) {
|
|
97
|
+
export function buildProof({ url, errors, boxes, screenshotPath, navTiming, perfBudgetMs = DEFAULT_PERF_BUDGET_MS, contrastSamples = null, contrastSkipped = 0, contrastUnparsed = 0 }) {
|
|
94
98
|
const consolePass = errors.length === 0;
|
|
95
99
|
const boxFailures = Object.entries(boxes)
|
|
96
100
|
.filter(([, b]) => !b || b.width <= 0 || b.height <= 0)
|
|
@@ -112,9 +116,13 @@ export function buildProof({ url, errors, boxes, screenshotPath, navTiming, perf
|
|
|
112
116
|
// captured no samples (selftest fixtures, an all-image page, a sampler
|
|
113
117
|
// crash). This mirrors the perf row's degradation shape but NOT its
|
|
114
118
|
// advisory nature: a measured failing pair fails the verdict.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
// A page whose colors were SEEN but none PARSED is a broken sampler, not
|
|
120
|
+
// an unmeasurable page — fail loud instead of reporting UNMEASURED.
|
|
121
|
+
const contrast = contrastSamples?.length
|
|
122
|
+
? scoreContrastSamples(contrastSamples, { skipped: contrastSkipped, unparsed: contrastUnparsed })
|
|
123
|
+
: contrastUnparsed > 0
|
|
124
|
+
? { pass: false, checked: 0, failing: [], unparsedColors: contrastUnparsed, reason: `sampler parsed 0 pairs but saw ${contrastUnparsed} unreadable color value(s) — color syntax unsupported by the sampler` }
|
|
125
|
+
: { pass: null, reason: 'UNMEASURED — no text foreground/background pairs sampled' };
|
|
118
126
|
return {
|
|
119
127
|
record: 'VerifyProof',
|
|
120
128
|
url,
|
|
@@ -176,6 +184,26 @@ function selftest() {
|
|
|
176
184
|
{ text: 'gray at body size', element: 'p', fg: [148, 148, 148], bg: [255, 255, 255], fontSizePx: 16, fontWeight: 400 },
|
|
177
185
|
{ text: 'gray as a heading', element: 'h1', fg: [148, 148, 148], bg: [255, 255, 255], fontSizePx: 24, fontWeight: 400 },
|
|
178
186
|
]);
|
|
187
|
+
// Sampler saw colors but parsed none — a broken sampler must fail loud,
|
|
188
|
+
// never degrade to UNMEASURED.
|
|
189
|
+
const allUnparsed = buildProof({
|
|
190
|
+
url: 'http://x', errors: [], boxes: { 'my-surface': { width: 640, height: 480 } }, screenshotPath: 'p.png',
|
|
191
|
+
contrastUnparsed: 7,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const emptySamples = buildProof({
|
|
195
|
+
url: 'http://x', errors: [], boxes: { 'my-surface': { width: 640, height: 480 } }, screenshotPath: 'p.png',
|
|
196
|
+
contrastSamples: [],
|
|
197
|
+
});
|
|
198
|
+
// One pair parsed, many colors unreadable — the pass stands (a measured
|
|
199
|
+
// pair is a measured pair) but the sampler-health count must ride on the
|
|
200
|
+
// row, never vanish behind a 1/1.
|
|
201
|
+
const partialUnparsed = buildProof({
|
|
202
|
+
url: 'http://x', errors: [], boxes: { 'my-surface': { width: 640, height: 480 } }, screenshotPath: 'p.png',
|
|
203
|
+
contrastSamples: [{ text: 'ok', element: 'p', fg: [0, 0, 0], bg: [255, 255, 255], fontSizePx: 16, fontWeight: 400 }],
|
|
204
|
+
contrastUnparsed: 200,
|
|
205
|
+
});
|
|
206
|
+
|
|
179
207
|
const fails = [];
|
|
180
208
|
const bw = contrastRatio([0, 0, 0], [255, 255, 255]);
|
|
181
209
|
if (bw < 20.9 || bw > 21.1) fails.push(`black/white ratio ${bw} not ≈21`);
|
|
@@ -198,6 +226,19 @@ function selftest() {
|
|
|
198
226
|
if (good.gates.contrast.pass !== null || !good.gates.contrast.reason.includes('UNMEASURED')) {
|
|
199
227
|
fails.push('missing contrast samples did not degrade to UNMEASURED contrast row');
|
|
200
228
|
}
|
|
229
|
+
if (emptySamples.gates.contrast.pass !== null || emptySamples.verdict !== 'pass-pending-read') {
|
|
230
|
+
fails.push('empty contrast sample array did not degrade to UNMEASURED');
|
|
231
|
+
}
|
|
232
|
+
if (allUnparsed.verdict !== 'fail' || allUnparsed.gates.contrast.pass !== false
|
|
233
|
+
|| !allUnparsed.gates.contrast.reason.includes('unreadable color')) {
|
|
234
|
+
fails.push('all-unparsed sampler did not fail loud');
|
|
235
|
+
}
|
|
236
|
+
if (partialUnparsed.gates.contrast.pass !== true || partialUnparsed.gates.contrast.unparsedColors !== 200) {
|
|
237
|
+
fails.push('partially-parsed sampler dropped the unparsedColors count');
|
|
238
|
+
}
|
|
239
|
+
if (good.gates.contrast.unparsedColors !== undefined) {
|
|
240
|
+
fails.push('unparsedColors leaked onto a row with nothing unparsed');
|
|
241
|
+
}
|
|
201
242
|
if (good.verdict !== 'pass-pending-read') fails.push('good fixture not pass-pending-read');
|
|
202
243
|
if (badBox.verdict !== 'fail' || badBox.gates.boundingBoxes.failing[0] !== 'my-surface') fails.push('0x0 box not failed');
|
|
203
244
|
if (badErr.verdict !== 'fail' || badErr.gates.consoleErrors.pass) fails.push('console error not failed');
|
|
@@ -213,7 +254,7 @@ function selftest() {
|
|
|
213
254
|
console.error('selftest FAIL: ' + fails.join(' | '));
|
|
214
255
|
process.exit(1);
|
|
215
256
|
}
|
|
216
|
-
console.log('selftest OK —
|
|
257
|
+
console.log('selftest OK — 11 fixtures, verdicts + imageRead + advisory perf row + AA contrast gate (positive/negative/large-text) correct');
|
|
217
258
|
process.exit(0);
|
|
218
259
|
}
|
|
219
260
|
|
|
@@ -310,11 +351,38 @@ async function main() {
|
|
|
310
351
|
// guessed. Deduped by (fg, bg, size, weight) and capped, so the report
|
|
311
352
|
// stays bounded on long pages.
|
|
312
353
|
const sampled = await page.evaluate(() => {
|
|
354
|
+
// gh#1259 follow-up: computed styles come back as oklch()/color() in
|
|
355
|
+
// current Chromium for token-driven colors — the rgb() regex alone read
|
|
356
|
+
// checked:0 on every AdiaUI page. Any non-rgb syntax rasterizes through
|
|
357
|
+
// a 1x1 canvas (ground-truth sRGB bytes, not a hand-rolled conversion).
|
|
358
|
+
const rasterCtx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
359
|
+
rasterCtx.canvas.width = rasterCtx.canvas.height = 1;
|
|
360
|
+
// Memoized by the computed string: bgFor re-parses every ancestor
|
|
361
|
+
// background per text node, so without a memo the same token color
|
|
362
|
+
// rasterizes O(textNodes x depth) times and one unreadable body
|
|
363
|
+
// background counts once per visit instead of once. `unparsed` is
|
|
364
|
+
// the number of DISTINCT unreadable strings.
|
|
365
|
+
const memo = new Map();
|
|
366
|
+
let unparsed = 0;
|
|
313
367
|
const parse = (s) => {
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
368
|
+
if (!s) return null;
|
|
369
|
+
if (memo.has(s)) return memo.get(s);
|
|
370
|
+
let out = null;
|
|
371
|
+
const m = /rgba?\(([^)]+)\)/.exec(s);
|
|
372
|
+
if (m) {
|
|
373
|
+
const [r, g, b, a = 1] = m[1].split(',').map(Number);
|
|
374
|
+
out = { rgb: [r, g, b], a };
|
|
375
|
+
} else if (!CSS.supports('color', s)) {
|
|
376
|
+
unparsed += 1;
|
|
377
|
+
} else {
|
|
378
|
+
rasterCtx.fillStyle = s;
|
|
379
|
+
rasterCtx.clearRect(0, 0, 1, 1);
|
|
380
|
+
rasterCtx.fillRect(0, 0, 1, 1);
|
|
381
|
+
const d = rasterCtx.getImageData(0, 0, 1, 1).data;
|
|
382
|
+
out = { rgb: [d[0], d[1], d[2]], a: d[3] / 255 };
|
|
383
|
+
}
|
|
384
|
+
memo.set(s, out);
|
|
385
|
+
return out;
|
|
318
386
|
};
|
|
319
387
|
const over = (fg, a, bg) => fg.map((c, i) => Math.round(c * a + bg[i] * (1 - a)));
|
|
320
388
|
const bgFor = (el) => {
|
|
@@ -322,7 +390,12 @@ async function main() {
|
|
|
322
390
|
for (let n = el; n && n.nodeType === 1; n = n.parentElement || (n.getRootNode && n.getRootNode().host) || null) {
|
|
323
391
|
const cs = getComputedStyle(n);
|
|
324
392
|
if (cs.backgroundImage && cs.backgroundImage !== 'none') return null; // indeterminate — pixels, not computed styles
|
|
325
|
-
const
|
|
393
|
+
const raw = cs.backgroundColor;
|
|
394
|
+
const c = parse(raw);
|
|
395
|
+
// An unreadable layer is indeterminate too, not transparent: falling
|
|
396
|
+
// through to the white default would score white-on-dark as ~1:1
|
|
397
|
+
// (measured, and wrong). Skip and count, same as the image branch.
|
|
398
|
+
if (raw && c === null) return null;
|
|
326
399
|
if (c && c.a > 0) {
|
|
327
400
|
layers.push(c);
|
|
328
401
|
if (c.a >= 1) break;
|
|
@@ -382,7 +455,7 @@ async function main() {
|
|
|
382
455
|
}
|
|
383
456
|
};
|
|
384
457
|
visitRoot(document.body);
|
|
385
|
-
return { samples, skipped };
|
|
458
|
+
return { samples, skipped, unparsed };
|
|
386
459
|
}).catch(() => null);
|
|
387
460
|
await page.screenshot({ path: screenshotPath, scale: 'device', fullPage: true });
|
|
388
461
|
await browser.close();
|
|
@@ -391,6 +464,7 @@ async function main() {
|
|
|
391
464
|
url, errors, boxes, screenshotPath, navTiming, perfBudgetMs,
|
|
392
465
|
contrastSamples: sampled && sampled.samples.length ? sampled.samples : null,
|
|
393
466
|
contrastSkipped: sampled ? sampled.skipped : 0,
|
|
467
|
+
contrastUnparsed: sampled ? sampled.unparsed : 0,
|
|
394
468
|
});
|
|
395
469
|
if (process.argv.includes('--json')) {
|
|
396
470
|
console.log(JSON.stringify(proof, null, 1));
|
|
@@ -401,7 +475,9 @@ async function main() {
|
|
|
401
475
|
const c = proof.gates.contrast;
|
|
402
476
|
const contrastNote = c.pass === null
|
|
403
477
|
? 'contrast:UNMEASURED'
|
|
404
|
-
:
|
|
478
|
+
: c.reason
|
|
479
|
+
? `contrast:FAIL ${c.reason}`
|
|
480
|
+
: `contrast:${c.pass ? 'pass' : 'FAIL'} ${c.checked - c.failing.length}/${c.checked} pairs${c.skippedImageBacked ? ` (+${c.skippedImageBacked} image-backed skipped)` : ''}${c.unparsedColors ? ` (+${c.unparsedColors} unreadable color value(s) — sampler coverage partial)` : ''}`;
|
|
405
481
|
console.log(`[adia-probe] ${proof.verdict} — errors:${errors.length} boxes:${selectors.length - proof.gates.boundingBoxes.failing.length}/${selectors.length} shot:${screenshotPath} ${perfNote} ${contrastNote}`);
|
|
406
482
|
if (proof.verdict === 'fail') {
|
|
407
483
|
for (const e of errors) console.log(` console: ${e}`);
|