styleproof 3.1.4 → 3.2.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,71 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.2.0] - 2026-06-30
11
+
12
+ ### Added
13
+
14
+ - Added optional `styleproof-map --crawl-base-url ... --crawl-route ...` pre-map
15
+ variant crawling, so automation can refresh the generated variant manifest
16
+ immediately before map capture.
17
+
18
+ ### Changed
19
+
20
+ - `styleproof-init` now writes a dedicated `playwright.styleproof.config.ts`,
21
+ scopes discovery to the StyleProof spec, detects Vite/Next production preview
22
+ commands, and respects pnpm/Corepack pins in generated commands.
23
+ - Map upload dirty-tree detection now ignores Next's generated `next-env.d.ts`
24
+ shim, while normal source edits still block upload.
25
+
26
+ ## [3.1.5] - 2026-06-29
27
+
28
+ ### Added
29
+
30
+ - Added `harvestStyleVariants` and the `styleproof-variants` CLI to discover
31
+ one-step UI state variants from a running app. The crawler tries semantic
32
+ controls, keeps only actions that change computed styles, dedupes equivalent
33
+ outcomes, and reports live-state candidates that need fixtures or opt-outs.
34
+ - **Captured maps now include semantic overlay proof metadata.** Visible dialog,
35
+ menu, listbox, modal, popover, tooltip, and toast roots that are present in the
36
+ captured DOM are recorded under `overlays`, filtered to paths that are actually
37
+ in the computed-style map. Downstream suites can now assert that a popup map
38
+ captured `role="dialog"`, `aria-modal`, `role="menu"`, `role="listbox"`, or
39
+ hot-toast text instead of only asserting that a popup file exists.
40
+ - **Component inventory coverage is now available through
41
+ `discoverComponentFiles`.** It scans explicit component roots across common
42
+ framework file types and returns stable `component-*` keys, so teams can wire a
43
+ Storybook/Ladle/custom catalog into `expected` and fail CI when a component file
44
+ has no rendered StyleProof surface or reviewed exclusion. `componentCatalogSurfaces`
45
+ turns the same inventory into catalog URL capture surfaces so the surface list
46
+ and expected list cannot drift apart.
47
+
48
+ ### Changed
49
+
50
+ - **`styleproof-init` now points modal, popover, menu, tab, and form-error
51
+ captures at `variants`.** The generated generic spec includes commented
52
+ `dialog-open` and `popover-open` examples under the base surface, so app-owned
53
+ UI states compare state-to-state instead of becoming orphan root surfaces.
54
+ - **The coverage guard now checks expanded variant keys.** Projects can put
55
+ required states such as `dashboard-user-menu-open`, `dashboard-toast-visible`,
56
+ or component catalog keys in `expected`; StyleProof fails unless those exact
57
+ surfaces are captured or explicitly excluded.
58
+ - **Non-live variants now keep the base capture.** Dialog, menu, popover, toast,
59
+ and overlay captures augment the owning surface instead of replacing its normal
60
+ page capture. `liveStates` continue to model explicit pinned live states.
61
+ - **Automatic popup capture now treats modal attributes, dropdown roles, and
62
+ toast roots as default overlay candidates.** The default selector includes
63
+ `[aria-modal="true"]`, `role="menu"`, `role="listbox"`, hot-toast/Sonner-style
64
+ toast markers, and status/alert toast roots. Popup matching also compares a
65
+ semantic/text signature, so a reused mount or toast host can still be captured
66
+ when its visible state changes.
67
+
68
+ ### Fixed
69
+
70
+ - Computed-style diffs now ignore sub-pixel `transform-origin` and
71
+ `perspective-origin` jitter while still reporting meaningful origin changes.
72
+
73
+ ## [3.1.4] - 2026-06-29
74
+
10
75
  ### Fixed
11
76
 
12
77
  - **Popup capture no longer collapses distinct triggers that reuse the same
@@ -1009,7 +1074,10 @@ number)`), so each viewport band can capture at its own height. Default remains
1009
1074
  - `styleproof-diff` CLI: certifies a refactor (exit 0) or names the exact element,
1010
1075
  property, and state that drifted (exit 1).
1011
1076
 
1012
- [Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.3...HEAD
1077
+ [Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.2.0...HEAD
1078
+ [3.2.0]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.5...v3.2.0
1079
+ [3.1.5]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.4...v3.1.5
1080
+ [3.1.4]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.3...v3.1.4
1013
1081
  [3.1.3]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.2...v3.1.3
1014
1082
  [3.1.2]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.1...v3.1.2
1015
1083
  [3.1.1]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.0...v3.1.1
package/README.md CHANGED
@@ -1,28 +1,138 @@
1
1
  # StyleProof
2
2
 
3
- **Know exactly what every PR changes visually, and sign it off.** StyleProof captures the browser's _computed_ styles (not pixels), diffs your PR's HEAD against its base branch, and posts a per-change report on the PR, so a styling change never ships without someone confirming it was intended.
3
+ **StyleProof is a PR gate for visual CSS changes.** You tell it which app states
4
+ matter. It opens those states in a real browser, records the browser's computed
5
+ styles, compares the PR head against the base branch, and posts a reviewable PR
6
+ report. Intentional visual changes get approved; unexpected ones block or fail.
4
7
 
5
8
  [![npm version](https://img.shields.io/npm/v/styleproof.svg)](https://www.npmjs.com/package/styleproof)
6
9
  [![CI](https://github.com/BenSheridanEdwards/StyleProof/actions/workflows/ci.yml/badge.svg)](https://github.com/BenSheridanEdwards/StyleProof/actions)
7
10
  [![license](https://img.shields.io/npm/l/styleproof.svg)](https://github.com/BenSheridanEdwards/StyleProof/blob/main/LICENSE)
8
11
 
9
- ## Why
10
-
11
- Pixel-snapshot tools miss most CSS regressions: they can't force `:hover` / `:focus` / `:active`, can't see hidden or off-screen elements, can't reach between-breakpoint rules, and blur away sub-pixel drift. StyleProof reads the **computed style** of every element instead — every resolved longhand, every pseudo-element, the deltas `:hover` / `:focus` / `:active` apply (forced via CDP, no mouse), swept across each `@media` breakpoint.
12
-
13
- ## What the gate does
14
-
15
- On every PR, StyleProof diffs the PR head's computed-style map against the base branch's, and posts a Markdown comment:
12
+ ## Contents
13
+
14
+ - [Why](#why)
15
+ - [The short version](#the-short-version)
16
+ - [What it catches](#what-it-catches)
17
+ - [What you own](#what-you-own)
18
+ - [What the PR gets](#what-the-pr-gets)
19
+ - [Auto-discovery](#auto-discovery)
20
+ - [What a report looks like](#what-a-report-looks-like)
21
+ - [Works with any styling system](#works-with-any-styling-system)
22
+ - [Breakpoints, detected automatically](#breakpoints-detected-automatically)
23
+ - [Certify a refactor](#certify-a-refactor)
24
+ - [Install](#install)
25
+ - [Quickstart](#quickstart)
26
+ - [Forks and Dependabot](#forks-and-dependabot)
27
+ - [Optional: content layer](#optional-content-layer-advisory)
28
+ - [Optional: React component layer](#optional-react-component-layer-advisory)
29
+ - [Newly-added elements show their full style](#newly-added-elements-show-their-full-style)
30
+ - [Reference](#reference)
31
+ - [Blocking without branch protection](#blocking-without-branch-protection)
32
+ - [License](#license)
16
33
 
17
- - A **lean summary comment** linking to a committed side-by-side report — the report is the complete source of truth (**one section per distinct change**, with a before/after cropped screenshot cropped from the same rectangle so the two sides line up exactly, **plain-English bullets that tell you what to look for** — `columns: 2 → 3`, `recoloured cyan → amber` — and the exact property changes). The comment never duplicates the report, so the two can't drift, and it renders identically on public and private repos.
18
- - A single **Approve all changes** checkbox in the comment, driving a `StyleProof` commit status: red until one tick signs off every change, green when there are none. The reviewer who ticks it is recorded inline (_approved by @them_), sourced from the commit status so it survives a report re-run.
19
- - **Clean runs leave a receipt too.** When no visual changes are detected, StyleProof still creates or updates the PR comment with `✓ No visual changes detected.`, so a green status has a visible record instead of only a silent check.
20
- - **New surfaces don't block.** A surface that exists only on the PR head (no baseline to diff — e.g. the bootstrap PR that first adds the capture spec, or a brand-new page) is shown in the report under a `🆕 new surface` heading but never holds the status red and needs no sign-off. It becomes part of the baseline once merged.
21
- - The diff is always **head-vs-base**, so the report is _exactly what this PR changes_ — whether the maps are restored from the `styleproof-maps` cache or recaptured in CI on a cache miss. See [Quickstart](#quickstart).
22
-
23
- ## Don't let a new page ship uncaptured
34
+ ## Why
24
35
 
25
- StyleProof diffs the surfaces your spec lists — so a page nobody added to the list is invisible to the gate. Its change has no base capture _and_ no head capture, so it never appears in any diff, and the status goes green having never looked at it. This is the one thing the captures can't catch on their own: a capture that was never taken.
36
+ Use StyleProof when a PR can change CSS, design tokens, component classes,
37
+ layout, or hidden/open UI states and you want CI to say whether the browser's
38
+ rendered styles actually changed. Unit and e2e tests prove behavior; StyleProof
39
+ proves the visual contract for the states you declared.
40
+
41
+ It is useful for:
42
+
43
+ - certifying a "no visual change" refactor such as CSS Modules to Tailwind;
44
+ - reviewing intentional visual changes with exact before/after style evidence;
45
+ - catching accidental changes that only appear at one breakpoint;
46
+ - catching open-state regressions in dialogs, dropdowns, listboxes, popovers, and
47
+ toasts;
48
+ - failing CI when a required route, component, or UI state exists but has no
49
+ capture.
50
+
51
+ The important boundary: StyleProof only certifies states it can reach. If a state
52
+ matters, list it as a surface, variant, popup, live state, or component-catalog
53
+ surface. `expected` turns that inventory into a guard, so an uncaptured new page,
54
+ component, modal, dropdown, or toast fails as missing coverage instead of
55
+ silently passing.
56
+
57
+ ## The short version
58
+
59
+ 1. A **surface** is one UI state to certify: a route, tab, modal-open state,
60
+ dropdown-open state, toast-visible state, loading state, etc.
61
+ 2. You list or auto-discover surfaces in a Playwright-style spec.
62
+ 3. StyleProof opens each surface at real breakpoint widths and records computed
63
+ styles for every captured element.
64
+ 4. On a PR, it compares base vs head and reports exactly which rendered styles
65
+ changed.
66
+ 5. The PR gets a `StyleProof` status: green when nothing changed, red until
67
+ someone approves intentional changes, or failing when certification mode is
68
+ configured.
69
+
70
+ StyleProof is not a screenshot diff. Screenshots appear in the report so humans
71
+ can see the change, but the gate compares browser-computed CSS: resolved
72
+ longhands, pseudo-elements, layout boxes, motion longhands, and forced
73
+ `:hover`/`:focus`/`:active` deltas.
74
+
75
+ ## What it catches
76
+
77
+ - A button recoloured by a token, utility class, CSS module, inline style, or
78
+ design-system change.
79
+ - A layout shift at one breakpoint but not another.
80
+ - A dropped `:hover`, `:focus`, or `:active` style.
81
+ - A modal, menu, listbox, popover, sheet, or toast whose open state changed.
82
+ - A supposedly no-op refactor, such as CSS-to-Tailwind, that changed rendered
83
+ output.
84
+
85
+ ## What you own
86
+
87
+ StyleProof can only certify states it reaches. You own the app-specific list of
88
+ states that matter:
89
+
90
+ - routes and views belong in `surfaces`;
91
+ - open states belong in `variants` or `popups`;
92
+ - loading/loaded/empty/error states belong in `liveStates`;
93
+ - component catalogs can be wired through `discoverComponentFiles`;
94
+ - required-but-not-yet-captured states belong in `expected`, where the coverage
95
+ guard fails until they are captured or explicitly excluded with a reason.
96
+
97
+ Auto-discovery is a key feature, not a side path: StyleProof can discover
98
+ Next.js routes, crawl real nav links, inventory component files, harvest
99
+ one-step variants, click safe popups, detect breakpoints, and flag live or
100
+ volatile regions. You review the app-specific leftovers; StyleProof keeps the
101
+ mechanical inventory from drifting.
102
+
103
+ That boundary is deliberate. StyleProof should not guess destructive flows,
104
+ auth-only fixtures, or which product state your component needs. It should make
105
+ missing coverage loud.
106
+
107
+ ## What the PR gets
108
+
109
+ On every PR, StyleProof posts a small summary comment that links to the committed
110
+ full report. The report groups each distinct visual change with:
111
+
112
+ - before/after crops from the same page rectangle;
113
+ - highlighted crops that box the changed element;
114
+ - a plain-English summary such as `columns: 2 -> 3` or
115
+ `background brand-cyan -> brand-amber`;
116
+ - the exact computed CSS properties that changed.
117
+
118
+ In review-gate mode, one **Approve all changes** checkbox turns the `StyleProof`
119
+ status green for that commit. Clean runs still leave a receipt: `No visual
120
+ changes detected.` New surfaces are shown as new baselines; coverage gaps are
121
+ handled by `expected`.
122
+
123
+ ## Auto-discovery
124
+
125
+ StyleProof's auto-discovery keeps the boring inventory out of your hands where it
126
+ can be inferred safely: Next.js routes, crawlable links, component files,
127
+ semantic popups, one-step variants, breakpoints, and volatile/live candidates.
128
+ The key loop is simple: discover what the app exposes, capture what is safe, and
129
+ fail loudly for anything that still needs an owner.
130
+
131
+ StyleProof diffs the surfaces your spec lists or discovers — so a page nobody
132
+ added to either set is invisible to the gate. Its change has no base capture
133
+ _and_ no head capture, so it never appears in any diff, and the status goes
134
+ green having never looked at it. This is the one thing the captures can't catch
135
+ on their own: a capture that was never taken.
26
136
 
27
137
  Declare your app's route/view universe in `expected` and StyleProof emits a coverage-guard test in your **normal** suite (it runs even without `STYLEMAP_DIR` — it's a static check, no browser). It fails the moment a route exists with no surface, so a new page can't ship uncaptured:
28
138
 
@@ -80,6 +190,144 @@ defineCrawlCapture({
80
190
 
81
191
  Each discovered link becomes a surface keyed by its URL (`/?tab=overview` → `overview`; pass `key` for a different scheme). The app only has to render its nav as real `<a href>` links — a button-only nav (`<button onClick>`) exposes nothing to crawl. Replay, self-check and clock-freeze behave exactly as for explicit surfaces; one Playwright test runs the whole sweep (the link set isn't known until the page renders).
82
192
 
193
+ **Component inventory: fail when the catalog misses a component.** StyleProof
194
+ cannot render arbitrary component files by itself across frameworks; props,
195
+ providers, loaders, portals, and app shell context are app-owned. What it can do
196
+ reliably is inventory component files and make your catalog/story route prove it
197
+ has a capture for each one:
198
+
199
+ ```ts
200
+ import { componentCatalogSurfaces, defineStyleMapCapture, discoverComponentFiles } from 'styleproof';
201
+
202
+ const COMPONENTS = discoverComponentFiles({
203
+ roots: ['src/components'],
204
+ ignore: [/\/icons\//],
205
+ });
206
+
207
+ defineStyleMapCapture({
208
+ surfaces: componentCatalogSurfaces(COMPONENTS, {
209
+ url: (component) => `/styleproof/components/${component.key}`,
210
+ widths: [390, 1024],
211
+ }),
212
+ expected: COMPONENTS.map((component) => component.key),
213
+ exclude: {
214
+ 'component-payment-card': 'needs a billing provider fixture',
215
+ },
216
+ dir: process.env.STYLEMAP_DIR,
217
+ });
218
+ ```
219
+
220
+ Use Storybook, Ladle, a framework route, or a tiny app-specific catalog for
221
+ `/styleproof/components/:key`. The inventory feeds both `surfaces` and
222
+ `expected`, so a new component file appears immediately and CI fails until it has
223
+ a rendered surface or an explicit exclusion.
224
+
225
+ **Dialogs, popovers and menus: capture the open state as a variant.** StyleProof
226
+ cannot guess which app-specific button opens a modal, but once you tell it the
227
+ interaction, it compares matching states on base and head (`home-dialog-open` to
228
+ `home-dialog-open`). Keep these under the route/view that owns them:
229
+
230
+ ```ts
231
+ const SURFACES: Surface[] = [
232
+ {
233
+ key: 'home',
234
+ go: (page) => page.goto('/'),
235
+ variants: [
236
+ {
237
+ key: 'dialog-open',
238
+ go: async (page) => {
239
+ await page.getByRole('button', { name: /open settings/i }).click();
240
+ await page.getByRole('dialog').waitFor();
241
+ },
242
+ },
243
+ {
244
+ key: 'popover-open',
245
+ go: async (page) => {
246
+ await page.getByRole('button', { name: /more/i }).click();
247
+ await page.locator('[popover], [role="menu"]').first().waitFor();
248
+ },
249
+ },
250
+ ],
251
+ },
252
+ ];
253
+ ```
254
+
255
+ Non-live `variants` add captures; the owning surface still captures too. Use
256
+ `liveStates` instead when the default live state is too fuzzy and only pinned
257
+ states such as `loading`, `loaded`, `empty`, or `error` should be compared.
258
+
259
+ When `popups: true` is enabled, StyleProof also tries visible safe triggers and
260
+ captures opened dialogs, menus, listboxes, modal roots, popovers, tooltips, and
261
+ toast/status roots. Each saved map includes `overlays` proof metadata for
262
+ semantic roots that were actually present in the computed-style map, so tests can
263
+ assert a capture reached `role="dialog"`, `aria-modal`, `role="menu"`,
264
+ `role="listbox"`, or hot-toast text.
265
+
266
+ **Harvest one-step variants.** Routes are not the whole UI: drawers, tabs,
267
+ dialogs, empty form errors, selects, and other one-step states need their own
268
+ captures. `styleproof-variants` opens a running app, tries semantic controls
269
+ (`[aria-expanded]`, tabs, summaries, selects, required forms, etc.), captures a
270
+ baseline and post-action StyleMap, and keeps only actions that change computed
271
+ styles. It also reports live-state candidates that need fixtures or opt-outs.
272
+
273
+ ```bash
274
+ styleproof-variants --base-url http://localhost:3000 --route / --route settings=/settings
275
+ ```
276
+
277
+ Use it as a manifest generator, not a replacement for review. To refresh that
278
+ manifest as part of the map loop, pass the same crawl inputs to `styleproof-map`;
279
+ it runs the crawler before Playwright captures the maps:
280
+
281
+ ```bash
282
+ styleproof-map --crawl-base-url http://localhost:3000 --crawl-route / --crawl-route settings=/settings
283
+ ```
284
+
285
+ The app must already be reachable at `--crawl-base-url`. If Playwright's
286
+ `webServer` is the thing starting the app, keep route-link crawling inside the
287
+ capture run with `defineCrawlCapture`.
288
+
289
+ ```json
290
+ {
291
+ "routes": [
292
+ {
293
+ "key": "settings",
294
+ "url": "/settings",
295
+ "variants": [
296
+ {
297
+ "key": "plan-selected",
298
+ "action": "select-option",
299
+ "selector": "select[aria-label=\"Plan\"]",
300
+ "value": "pro"
301
+ }
302
+ ],
303
+ "liveStates": [{ "key": "status", "fixtureRequired": true }],
304
+ "skipped": []
305
+ }
306
+ ]
307
+ }
308
+ ```
309
+
310
+ ```ts
311
+ defineStyleMapCapture({
312
+ surfaces: [
313
+ {
314
+ key: 'settings',
315
+ go: (page) => page.goto('/settings'),
316
+ variants: [
317
+ {
318
+ key: 'plan-selected',
319
+ go: (page) => page.locator('select[aria-label="Plan"]').selectOption('pro'),
320
+ },
321
+ ],
322
+ },
323
+ ],
324
+ });
325
+ ```
326
+
327
+ Destructive labels are skipped, duplicate computed-style outcomes are deduped,
328
+ and `--strict` exits non-zero when live-state fixtures or skipped candidates
329
+ remain unresolved.
330
+
83
331
  **Live UI states: capture each state, not an average.** StyleProof automatically
84
332
  detects semantic live-state candidates (`aria-live`, `role=status`, `role=alert`,
85
333
  `aria-busy=true`) and keeps stable ones in the normal diff. If a stream, poll, or
@@ -169,7 +417,7 @@ npx styleproof-init
169
417
  It scaffolds:
170
418
 
171
419
  - a **capture spec** (`e2e/styleproof.spec.ts`) describing your surfaces (a Next.js app gets its routes _and_ the coverage guard wired automatically — see below);
172
- - a **`playwright.config.ts`** that builds and serves a **production build** (never a flaky dev server) and captures surfaces **in parallel** (`fullyParallel`);
420
+ - a dedicated **`playwright.styleproof.config.ts`** that builds and serves a **production build** (never a flaky dev server), scopes discovery to the StyleProof spec, and captures surfaces **in parallel** (`fullyParallel`) without disturbing your app's existing Playwright config;
173
421
  - `.gitignore` entries for `.styleproof/`, `test-results/`, and `playwright-report/`;
174
422
  - a **cache-first CI workflow** that restores reusable maps from the `styleproof-maps` branch and generates the report without a browser when both maps are already built.
175
423
 
@@ -312,16 +560,16 @@ Anything still moving on its own after that is detected as a volatile region and
312
560
  | Live / volatile regions (tickers, third-party embeds) | auto-detected as still-moving and excluded from direct element comparison |
313
561
  | Non-deterministic capture (replay gap, unseeded randomness) | self-check flags it _while recording_, with a named error |
314
562
 
315
- | You set this — only because it's app-specific | Why it exists |
316
- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
317
- | `STYLEPROOF_REPLAY_FROM` (record / replay) | Base and head capture at different times against a live backend; replaying the base's recorded data pins the head to the same inputs, so the diff is **your code, not data drift**. The one piece of real setup. |
318
- | `replayUrl` / `STYLEPROOF_REPLAY_URL` | Your data endpoints aren't under `**/api/**`. |
319
- | `ignore: ['.selector']` | You want a region gone **explicitly** — auto-exclude already handles most live regions, but a known-noisy element reads clearer named. |
320
- | `liveStates: [{ key, setup, go }]` | A live feature has real states to certify. Capture each state on base and head (`surface-loading`, `surface-loaded`) instead of relying on a single moving page state. |
321
- | `variants: [{ key, setup, go }]` | Non-live deterministic variants, such as nav-open or modal-open states. |
322
- | `popups: true` | Visible click-triggered overlays should be discovered automatically. Captures each matching trigger's persistent dialogs, popovers, menus, listboxes, and open data-state overlays as `surface-popup-XX`; keep hover-only or destructive states as explicit `variants`. |
323
- | `clockTime` | Your styling keys off a **specific** date, not just "now". |
324
- | `stabilize: { quietFor, timeout }` | An unusually slow surface needs a longer quiet window before the map is read. |
563
+ | You set this — only because it's app-specific | Why it exists |
564
+ | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
565
+ | `STYLEPROOF_REPLAY_FROM` (record / replay) | Base and head capture at different times against a live backend; replaying the base's recorded data pins the head to the same inputs, so the diff is **your code, not data drift**. The one piece of real setup. |
566
+ | `replayUrl` / `STYLEPROOF_REPLAY_URL` | Your data endpoints aren't under `**/api/**`. |
567
+ | `ignore: ['.selector']` | You want a region gone **explicitly** — auto-exclude already handles most live regions, but a known-noisy element reads clearer named. |
568
+ | `liveStates: [{ key, setup, go }]` | A live feature has real states to certify. Capture each state on base and head (`surface-loading`, `surface-loaded`) instead of relying on a single moving page state. |
569
+ | `variants: [{ key, setup, go }]` | Non-live deterministic variants, such as nav-open, modal-open, toast-visible, or overlay-expanded states. |
570
+ | `popups: true` | Visible click-triggered overlays should be discovered automatically. Captures each matching trigger's persistent dialogs, modal roots, popovers, menus, listboxes, toast/status roots, and open data-state overlays as `surface-popup-XX`; keep hover-only or destructive states as explicit `variants`. |
571
+ | `clockTime` | Your styling keys off a **specific** date, not just "now". |
572
+ | `stabilize: { quietFor, timeout }` | An unusually slow surface needs a longer quiet window before the map is read. |
325
573
 
326
574
  ## Optional: content layer (advisory)
327
575
 
@@ -392,44 +640,48 @@ It's **asynchronous by design**: approval is a checkbox tick handled by a separa
392
640
 
393
641
  **Capture spec `defineStyleMapCapture({ surfaces, … })`** — determinism is on by default; you rarely set more than `surfaces` and `dir`:
394
642
 
395
- | Option | Default | Purpose |
396
- | ------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
397
- | `surfaces` | _required_ | Page states to certify — each `{ key, go, widths?, ignore?, height?, liveStates?, variants?, popups? }`. `go(page)` drives to a settled state. Omit `widths` to auto-detect the app's `@media` breakpoints and sweep one width per band. |
398
- | `liveStates` | _none_ | Optional pinned live product states. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<state>` and is labeled as a live state in reports. |
399
- | `variants` | _none_ | Optional non-live deterministic states under a surface. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<variant>` so base/head compare matching states. |
400
- | `popups` | `false` | Optional automatic popup capture. Set `true` or `{ max, triggers, overlays, timeoutMs }` to click visible safe triggers and save each opened overlay state as `<surface>-popup-XX`. |
401
- | `expected` | _none_ | Your route/view universe. Emits a coverage-guard test (runs without a capture dir) that fails when a route has no surface and isn't excluded — so a new page can't ship uncaptured. |
402
- | `exclude` | `{}` | `key → reason` for routes deliberately not captured. Keeps the guard green for known gaps; a key absent from `expected` fails the guard, so the ledger can't go stale. |
403
- | `dir` | `STYLEMAP_DIR` | Output label (`base`/`head`); the spec is **inert until set**, so it sits safely beside your other specs. |
404
- | `replayFrom` | `STYLEPROOF_REPLAY_FROM` | Baseline dir whose recorded responses to replay. Unset → this run **records** its HAR for the comparison to use. |
405
- | `replayUrl` | `**/api/**` (`…REPLAY_URL`) | URL glob for the data boundary to record/replay; everything else (JS/CSS/fonts) loads live so the code runs. |
406
- | `freezeClock` | `true` | Pin `Date.now()`/`new Date()` so time-derived styling can't drift; timers keep running so settling still works. |
407
- | `clockTime` | `2025-01-01T00:00:00Z` | The frozen instant. |
408
- | `selfCheck` | on while recording | Capture each surface twice and fail on any difference — proves the capture is deterministic. Off on the replay run; `STYLEPROOF_SELFCHECK=1` forces both. |
409
- | `screenshots` | `true` | Save full-page screenshots for the report's before/after crops. |
410
- | `baseDir` | `__stylemaps__` | Output root directory. |
643
+ | Option | Default | Purpose |
644
+ | ------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
645
+ | `surfaces` | _required_ | Page states to certify — each `{ key, go, widths?, ignore?, height?, liveStates?, variants?, popups? }`. `go(page)` drives to a settled state. Omit `widths` to auto-detect the app's `@media` breakpoints and sweep one width per band. |
646
+ | `liveStates` | _none_ | Optional pinned live product states. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<state>` and is labeled as a live state in reports. |
647
+ | `variants` | _none_ | Optional non-live deterministic states under a surface. The base surface still captures; each variant becomes `<surface>-<variant>` so base/head compare matching states. |
648
+ | `popups` | `false` | Optional automatic popup capture. Set `true` or `{ max, triggers, overlays, timeoutMs }` to click visible safe triggers and save each opened overlay state as `<surface>-popup-XX`; maps include `overlays` proof metadata for captured semantic roots. |
649
+ | `expected` | _none_ | Your route/view/state/component universe. Emits a coverage-guard test (runs without a capture dir) that fails when a required key has no surface and isn't excluded. |
650
+ | `exclude` | `{}` | `key → reason` for routes deliberately not captured. Keeps the guard green for known gaps; a key absent from `expected` fails the guard, so the ledger can't go stale. |
651
+ | `dir` | `STYLEMAP_DIR` | Output label (`base`/`head`); the spec is **inert until set**, so it sits safely beside your other specs. |
652
+ | `replayFrom` | `STYLEPROOF_REPLAY_FROM` | Baseline dir whose recorded responses to replay. Unset → this run **records** its HAR for the comparison to use. |
653
+ | `replayUrl` | `**/api/**` (`…REPLAY_URL`) | URL glob for the data boundary to record/replay; everything else (JS/CSS/fonts) loads live so the code runs. |
654
+ | `freezeClock` | `true` | Pin `Date.now()`/`new Date()` so time-derived styling can't drift; timers keep running so settling still works. |
655
+ | `clockTime` | `2025-01-01T00:00:00Z` | The frozen instant. |
656
+ | `selfCheck` | on while recording | Capture each surface twice and fail on any difference — proves the capture is deterministic. Off on the replay run; `STYLEPROOF_SELFCHECK=1` forces both. |
657
+ | `screenshots` | `true` | Save full-page screenshots for the report's before/after crops. |
658
+ | `baseDir` | `__stylemaps__` | Output root directory. |
411
659
 
412
660
  Non-visual and framework-injected elements (`<meta>`/`<title>`/`<script>`/`<style>`/… and `next-route-announcer`) are skipped automatically; a surface's `ignore` adds to that default, it doesn't replace it.
413
661
 
414
662
  **Capture env vars** (wire CI without editing the spec):
415
663
 
416
- | Env | Purpose |
417
- | ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
418
- | `STYLEMAP_DIR` | Output label; the capture is skipped entirely when unset. |
419
- | `STYLEPROOF_BASEDIR` | Output root dir (runner default `__stylemaps__`; `styleproof-map` CLI default `.styleproof/maps`). |
420
- | `STYLEPROOF_SCREENSHOTS` | `0` to skip full-page screenshots. The CLI keeps screenshots by default so reports can crop maps restored from cache. |
421
- | `STYLEPROOF_REPLAY_FROM` | Baseline dir to replay recorded data from — set this on the **head** capture. |
422
- | `STYLEPROOF_REPLAY_URL` | Override the `**/api/**` data-boundary glob. |
423
- | `STYLEPROOF_SELFCHECK` | `1` to capture each surface twice and fail if the two differ. |
424
- | `STYLEPROOF_UPLOAD` | `1` to require map-store upload; `0` to capture locally only. |
425
- | `STYLEPROOF_CACHE_BRANCH` | Map store branch (default `styleproof-maps`). |
664
+ | Env | Purpose |
665
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
666
+ | `STYLEMAP_DIR` | Output label; the capture is skipped entirely when unset. |
667
+ | `STYLEPROOF_BASEDIR` | Output root dir (runner default `__stylemaps__`; `styleproof-map` CLI default `.styleproof/maps`). |
668
+ | `STYLEPROOF_SCREENSHOTS` | `0` to skip full-page screenshots. The CLI keeps screenshots by default so reports can crop maps restored from cache. |
669
+ | `STYLEPROOF_REPLAY_FROM` | Baseline dir to replay recorded data from — set this on the **head** capture. |
670
+ | `STYLEPROOF_REPLAY_URL` | Override the `**/api/**` data-boundary glob. |
671
+ | `STYLEPROOF_SELFCHECK` | `1` to capture each surface twice and fail if the two differ. |
672
+ | `STYLEPROOF_UPLOAD` | `1` to require map-store upload; `0` to capture locally only. |
673
+ | `STYLEPROOF_CACHE_BRANCH` | Map store branch (default `styleproof-maps`). |
674
+ | `STYLEPROOF_CRAWL_BASE_URL` | App URL for the optional pre-map `styleproof-variants` crawl. |
675
+ | `STYLEPROOF_CRAWL_ROUTES` | Comma-separated routes for the optional pre-map crawl, e.g. `/,settings=/settings`. |
676
+ | `STYLEPROOF_CRAWL_STRICT` | `1` to fail the optional pre-map crawl on live-state fixtures or skipped candidates. |
426
677
 
427
678
  **CLIs** (every flag accepts `--flag value` and `--flag=value`; `--help` lists all):
428
679
 
429
- - `styleproof-init` — scaffold the gate: the capture spec, a `playwright.config.ts` (production-build `webServer`, parallel capture), `.gitignore` cache entries, and the cache-first report workflow. One command. Generated commands follow the repo's lockfile (`bun.lock`/`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, or npm by default) instead of assuming npm.
430
- - `styleproof-map` — capture the current commit's computed-style map through Playwright. By default it writes `.styleproof/maps/current`, keeps screenshots for reports, writes a manifest, and uploads to `styleproof-maps` outside CI when the working tree was clean and a git remote exists. Pass `--no-upload`, `--restore --sha <commit>`, `--spec`, `--dir`, `--base-dir`, or `--no-screenshots` for custom flows.
680
+ - `styleproof-init` — scaffold the gate: the capture spec, a dedicated `playwright.styleproof.config.ts` (production-build `webServer`, parallel capture), `.gitignore` cache entries, and the cache-first report workflow. One command. Generated commands follow the repo's lockfile (`bun.lock`/`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, or npm by default), respect pnpm/Corepack version pins, and detect Vite/Next production preview commands instead of assuming every repo has `start`.
681
+ - `styleproof-map` — capture the current commit's computed-style map through Playwright. By default it writes `.styleproof/maps/current`, keeps screenshots for reports, writes a manifest, and uploads to `styleproof-maps` outside CI when the working tree was clean and a git remote exists. Pass `--crawl-base-url` plus repeated `--crawl-route` to run `styleproof-variants` before capture, `--no-upload`, `--restore --sha <commit>`, `--spec`, `--dir`, `--base-dir`, or `--no-screenshots` for custom flows.
431
682
  - `styleproof-diff` — the certify gate. With no args, it restores cached maps for the current commit and inferred base (`GITHUB_BASE_REF`, `branch.<name>.gh-merge-base`, `gh pr view`, then main/master fallbacks); `styleproof-diff main` / `styleproof-diff master` pins the base; `styleproof-diff <beforeDir> <afterDir>` keeps the manual two-directory form for CI fallback captures. Exits `0` certified (identical), `1` on a diff, `2` on a usage/capture error, `3` when only new surfaces are present (no baseline to diff against). A clean run prints `0 changed surfaces across N captured surface(s)`, and `--json` includes `compared`.
432
683
  - `styleproof-report` — render the diff to a Markdown report with before/after crops. With no args, it reports cached maps for the current commit against the inferred base; `styleproof-report main` / `styleproof-report master` pins the base; `styleproof-report <beforeDir> <afterDir> --out <dir>` keeps the manual two-directory form. Add `--include-content` for the opt-in, advisory content section (see above).
684
+ - `styleproof-variants` — crawl a running app for one-step state variants and write `styleproof.variants.generated.json`. Pass `--base-url`, repeat `--route`, and use `--strict` when unresolved skipped/live candidates should fail automation.
433
685
 
434
686
  A programmatic API is also exported — `captureStyleMap`, `diffStyleMaps`, `generateStyleMapReport`, and the breakpoint helpers `detectViewportWidths` / `widthsFromBoundaries`, among others. For the capture internals, the approve-workflow trust model, and how to contribute, see [CONTRIBUTING](https://github.com/BenSheridanEdwards/StyleProof/blob/main/CONTRIBUTING.md) and the [`example/`](https://github.com/BenSheridanEdwards/StyleProof/tree/main/example) workflows.
435
687