styleproof 1.7.1 โ 1.8.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 +33 -1
- package/dist/describe.d.ts +2 -0
- package/dist/describe.js +5 -2
- package/dist/report.d.ts +2 -2
- package/dist/report.js +59 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,39 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [1.
|
|
10
|
+
## [1.8.0]
|
|
11
|
+
|
|
12
|
+
The crop now shows you where to look โ without painting over the UI.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Annotated crop, alongside the clean one.** Each crop shows the clean
|
|
17
|
+
before|after composite by default (the real UI), with an annotated twin one
|
|
18
|
+
click away under a `๐ Highlight what changed` toggle โ a thin magenta outline
|
|
19
|
+
around each changed element on both sides, so the eye lands on exactly what the
|
|
20
|
+
bullet named. Outline only (never filled), and the clean image right there
|
|
21
|
+
proves the box isn't part of the design, so the marker can be confident without
|
|
22
|
+
reading as a change.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Tighter, more focused crops.** Default crop padding drops from 24px to 12px
|
|
27
|
+
(the change fills more of the frame), and up to 8 crop regions per surface
|
|
28
|
+
(was 6) before collapsing โ so distinct changes get their own focused frame
|
|
29
|
+
instead of one wide merged one. Both are still tunable (`pad`, `maxCrops`).
|
|
30
|
+
- The action now commits the annotated crops alongside the composites.
|
|
31
|
+
|
|
32
|
+
## [1.7.2]
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **Responsive variants of one change no longer show as duplicate sections.** A
|
|
37
|
+
grid whose `grid-template-columns/rows` computes to different pixels per width
|
|
38
|
+
(`282px ร2` vs `282px 228px`) was given a different signature per width, so the
|
|
39
|
+
same change rendered once per breakpoint. The signature now keys grid tracks by
|
|
40
|
+
their COUNT, so responsive widths collapse into one grouped section.
|
|
41
|
+
- The `e.g. โฆ` fold line no longer names the `+N more` overflow marker as if it
|
|
42
|
+
were a change.
|
|
11
43
|
|
|
12
44
|
### Fixed
|
|
13
45
|
|
package/dist/describe.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export declare function toHex(v: string): string;
|
|
|
25
25
|
/** Reverse-index a token map (`--red-200` โ `rgb(...)`) to value โ token name,
|
|
26
26
|
* preferring a scale step (`red-200`) over an alias, then the shorter name. */
|
|
27
27
|
export declare function tokenIndex(tokens?: Record<string, string>): Map<string, string>;
|
|
28
|
+
/** Count grid tracks in a `grid-template-*` value, honouring the `Npx รK` form. */
|
|
29
|
+
export declare function trackCount(v: string): number;
|
|
28
30
|
/** Token reverse-indexes for each side, so colour rules can name `red-200`. */
|
|
29
31
|
export type DescribeCtx = {
|
|
30
32
|
tokensBefore?: Map<string, string>;
|
package/dist/describe.js
CHANGED
|
@@ -117,7 +117,7 @@ const shift = (before, after, idxB, idxA) => {
|
|
|
117
117
|
};
|
|
118
118
|
// --- track counting for grid columns/rows ------------------------------------
|
|
119
119
|
/** Count grid tracks in a `grid-template-*` value, honouring the `Npx รK` form. */
|
|
120
|
-
function trackCount(v) {
|
|
120
|
+
export function trackCount(v) {
|
|
121
121
|
if (v === 'none' || !v)
|
|
122
122
|
return 0;
|
|
123
123
|
const rep = v.match(/ร(\d+)/); // summarizeProps collapses "8px 8px 8px" โ "8px ร3"
|
|
@@ -318,10 +318,13 @@ function foldedLine(group, ctx) {
|
|
|
318
318
|
const common = lists[0].filter((p) => lists.every((l) => l.includes(p)));
|
|
319
319
|
if (common.length)
|
|
320
320
|
return `${common.join(', ')} _(details vary)_`;
|
|
321
|
+
// Rank the most common REAL changes โ the `+N more` overflow marker isn't a
|
|
322
|
+
// change and shouldn't be named.
|
|
321
323
|
const freq = new Map();
|
|
322
324
|
for (const l of lists)
|
|
323
325
|
for (const p of new Set(l))
|
|
324
|
-
|
|
326
|
+
if (!/^\+\d+ more$/.test(p))
|
|
327
|
+
freq.set(p, (freq.get(p) ?? 0) + 1);
|
|
325
328
|
const top = [...freq.entries()]
|
|
326
329
|
.sort((a, b) => b[1] - a[1])
|
|
327
330
|
.slice(0, 3)
|
package/dist/report.d.ts
CHANGED
|
@@ -17,14 +17,14 @@ export type ReportOptions = {
|
|
|
17
17
|
outDir: string;
|
|
18
18
|
/** Prefix for image URLs in report.md (default: relative paths). */
|
|
19
19
|
imageBaseUrl?: string;
|
|
20
|
-
/** Padding around the union of changed rects (default
|
|
20
|
+
/** Padding around the union of changed rects (default 12px). */
|
|
21
21
|
pad?: number;
|
|
22
22
|
/** Minimum crop size, for context around tiny changes (default 320ร180). */
|
|
23
23
|
minWidth?: number;
|
|
24
24
|
minHeight?: number;
|
|
25
25
|
/** Crops taller than this are clamped (default 1600px). */
|
|
26
26
|
maxHeight?: number;
|
|
27
|
-
/** Max crop regions per surface before collapsing into one union crop (default
|
|
27
|
+
/** Max crop regions per surface before collapsing into one union crop (default 8). */
|
|
28
28
|
maxCrops?: number;
|
|
29
29
|
/**
|
|
30
30
|
* Row count at which a crop's property tables fold under a `<details>` toggle
|
package/dist/report.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { PNG } from 'pngjs';
|
|
4
4
|
import { loadStyleMap } from './capture.js';
|
|
5
5
|
import { diffStyleMapDirs } from './diff.js';
|
|
6
|
-
import { describeChange, tokenIndex, toHex } from './describe.js';
|
|
6
|
+
import { describeChange, tokenIndex, toHex, trackCount } from './describe.js';
|
|
7
7
|
// Re-export the plain-English summariser so consumers (and tests) reach it
|
|
8
8
|
// through the package's report module rather than a deep path.
|
|
9
9
|
export { describeChange, colorName, tokenIndex, toHex } from './describe.js';
|
|
@@ -80,13 +80,13 @@ function cropPng(src, box, w, h) {
|
|
|
80
80
|
// Center the fixed-size crop on the box, clamped to the image.
|
|
81
81
|
const cx = box.x + box.w / 2;
|
|
82
82
|
const cy = box.y + box.h / 2;
|
|
83
|
-
const
|
|
84
|
-
const
|
|
83
|
+
const ox = Math.max(0, Math.min(Math.round(cx - w / 2), src.width - w));
|
|
84
|
+
const oy = Math.max(0, Math.min(Math.round(cy - h / 2), src.height - h));
|
|
85
85
|
const cw = Math.min(w, src.width);
|
|
86
86
|
const ch = Math.min(h, src.height);
|
|
87
87
|
const out = new PNG({ width: cw, height: ch });
|
|
88
|
-
PNG.bitblt(src, out, Math.max(0,
|
|
89
|
-
return out;
|
|
88
|
+
PNG.bitblt(src, out, Math.max(0, ox), Math.max(0, oy), cw, ch, 0, 0);
|
|
89
|
+
return { png: out, ox, oy };
|
|
90
90
|
}
|
|
91
91
|
// Lossless but lean: drop the alpha channel (every crop/composite is opaque),
|
|
92
92
|
// max deflate, adaptive per-row filtering. ~15% smaller than the default, and
|
|
@@ -108,6 +108,29 @@ function fillRect(png, x, y, w, h, [r, g, b]) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
// The annotation hue: a magenta no real UI palette tends to use, so an outline
|
|
112
|
+
// reads as a marker, not content. Drawn as a hollow rectangle (never filled) so
|
|
113
|
+
// the UI underneath stays visible โ and the clean image alongside proves the box
|
|
114
|
+
// isn't part of the design.
|
|
115
|
+
const HILITE = [255, 0, 200];
|
|
116
|
+
function strokeRect(png, x, y, w, h, t = 2, color = HILITE) {
|
|
117
|
+
fillRect(png, x, y, w, t, color); // top
|
|
118
|
+
fillRect(png, x, y + h - t, w, t, color); // bottom
|
|
119
|
+
fillRect(png, x, y, t, h, color); // left
|
|
120
|
+
fillRect(png, x + w - t, y, t, h, color); // right
|
|
121
|
+
}
|
|
122
|
+
/** Clone a crop and outline each changed element's box (page coords mapped into
|
|
123
|
+
* the crop via its origin), so the eye lands on exactly what the bullet named. */
|
|
124
|
+
function annotateCrop(crop, rects) {
|
|
125
|
+
const out = new PNG({ width: crop.png.width, height: crop.png.height });
|
|
126
|
+
PNG.bitblt(crop.png, out, 0, 0, crop.png.width, crop.png.height, 0, 0);
|
|
127
|
+
for (const [rx, ry, rw, rh] of rects) {
|
|
128
|
+
if (rw <= 0 || rh <= 0)
|
|
129
|
+
continue;
|
|
130
|
+
strokeRect(out, rx - crop.ox, ry - crop.oy, rw, rh);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
111
134
|
/**
|
|
112
135
|
* One before|after image: the two equal-size crops on a dark canvas with a
|
|
113
136
|
* neutral divider between them. Left is always before; before/after is labelled
|
|
@@ -339,6 +362,16 @@ function formatSurfaceList(surfaces) {
|
|
|
339
362
|
})
|
|
340
363
|
.join(' ยท ');
|
|
341
364
|
}
|
|
365
|
+
// Grid-track longhands compute to width-dependent pixels (`282px ร2` at one width,
|
|
366
|
+
// `282px 228px` at another), so the SAME responsive change would otherwise get a
|
|
367
|
+
// different signature per width. Key them by track COUNT โ what actually
|
|
368
|
+
// identifies the change โ so responsive variants group into one section.
|
|
369
|
+
function sigValue(c) {
|
|
370
|
+
if (c.prop === 'grid-template-columns' || c.prop === 'grid-template-rows') {
|
|
371
|
+
return `${c.prop}=${trackCount(c.before)}t>${trackCount(c.after)}t`;
|
|
372
|
+
}
|
|
373
|
+
return `${c.prop}=${c.before}>${c.after}`;
|
|
374
|
+
}
|
|
342
375
|
/** Canonical signature of a surface's findings: surfaces that changed in the
|
|
343
376
|
* same way collapse into one section + one image (the rects differ per width;
|
|
344
377
|
* the change itself does not). */
|
|
@@ -348,11 +381,7 @@ function signatureOf(findings) {
|
|
|
348
381
|
p: f.path,
|
|
349
382
|
k: f.kind,
|
|
350
383
|
t: f.kind === 'dom' ? f.change : f.kind === 'state' ? f.state : (f.pseudo ?? ''),
|
|
351
|
-
v: f.kind === 'dom'
|
|
352
|
-
? ''
|
|
353
|
-
: summarizeProps(f.props)
|
|
354
|
-
.map((c) => `${c.prop}=${c.before}>${c.after}`)
|
|
355
|
-
.join('|'),
|
|
384
|
+
v: f.kind === 'dom' ? '' : summarizeProps(f.props).map(sigValue).join('|'),
|
|
356
385
|
}))
|
|
357
386
|
.sort((a, b) => `${a.p}|${a.k}|${a.t}`.localeCompare(`${b.p}|${b.k}|${b.t}`)));
|
|
358
387
|
}
|
|
@@ -585,7 +614,13 @@ function buildElementChanges(findings) {
|
|
|
585
614
|
return els;
|
|
586
615
|
}
|
|
587
616
|
export function generateStyleMapReport(opts) {
|
|
588
|
-
const { beforeDir, afterDir, outDir, imageBaseUrl = '',
|
|
617
|
+
const { beforeDir, afterDir, outDir, imageBaseUrl = '',
|
|
618
|
+
// Tighter than before (was 24) so the change fills the frame โ the annotation
|
|
619
|
+
// box keeps enough context legible.
|
|
620
|
+
pad: padBy = 12, minWidth = 320, minHeight = 180, maxHeight = 1600,
|
|
621
|
+
// More, smaller crops before collapsing (was 6), so distinct changes get their
|
|
622
|
+
// own focused frame rather than one wide merged one.
|
|
623
|
+
maxCrops = 8, foldDetailsAt = 0, } = opts;
|
|
589
624
|
const includeNoise = opts.includeLayoutNoise ?? false;
|
|
590
625
|
const { surfaces, volatile: volatileCount } = diffStyleMapDirs(beforeDir, afterDir);
|
|
591
626
|
fs.mkdirSync(path.join(outDir, 'crops'), { recursive: true });
|
|
@@ -711,15 +746,19 @@ export function generateStyleMapReport(opts) {
|
|
|
711
746
|
const stem = `crops/${sd.surface.replace(/[^a-z0-9-]/gi, '-')}-${cropSeq}`;
|
|
712
747
|
const before = cropPng(pngA, cropBox, w, h);
|
|
713
748
|
const after = cropPng(pngB, cropBox, w, h);
|
|
714
|
-
const composite = compositePair(before, after);
|
|
715
|
-
writePng(path.join(outDir, `${stem}-before.png`), before);
|
|
716
|
-
writePng(path.join(outDir, `${stem}-after.png`), after);
|
|
749
|
+
const composite = compositePair(before.png, after.png);
|
|
717
750
|
writePng(path.join(outDir, `${stem}-composite.png`), composite);
|
|
718
|
-
|
|
719
|
-
//
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
751
|
+
// Annotated twin: outline each changed element on both sides, so the eye
|
|
752
|
+
// lands on what the bullet named. The clean image stays the default.
|
|
753
|
+
const rectsA = g.paths.map((p) => mapA.elements[p]?.rect).filter((r) => !!r);
|
|
754
|
+
const rectsB = g.paths.map((p) => mapB.elements[p]?.rect).filter((r) => !!r);
|
|
755
|
+
const annotated = compositePair(annotateCrop(before, rectsA), annotateCrop(after, rectsB));
|
|
756
|
+
writePng(path.join(outDir, `${stem}-annotated.png`), annotated);
|
|
757
|
+
images = { composite: `${stem}-composite.png`, annotated: `${stem}-annotated.png` };
|
|
758
|
+
// Clean before|after shown by default (the real UI); the annotated twin โ
|
|
759
|
+
// boxes marking each change โ sits one click away under a toggle. Plain
|
|
760
|
+
// images (no link wrap) so a click opens the full-resolution file.
|
|
761
|
+
md.push('', `})`, '', `<sub>โ before ยท after โถ โ ${sd.surface}</sub>`, '', '<details>', '<summary>๐ Highlight what changed</summary>', '', `})`, '', `<sub>magenta boxes mark each change โ ${sd.surface}</sub>`, '', '</details>');
|
|
723
762
|
}
|
|
724
763
|
else if (!region) {
|
|
725
764
|
md.push('', '_Changed element is not visible in this state (zero-size box) โ see the property list._');
|
|
@@ -747,7 +786,7 @@ export function generateStyleMapReport(opts) {
|
|
|
747
786
|
if (png) {
|
|
748
787
|
cropSeq++;
|
|
749
788
|
const h = Math.min(maxHeight, png.height);
|
|
750
|
-
const crop = cropPng(png, { x: 0, y: 0, w: png.width, h }, png.width, h);
|
|
789
|
+
const crop = cropPng(png, { x: 0, y: 0, w: png.width, h }, png.width, h).png;
|
|
751
790
|
const stem = `crops/${p.sd.surface.replace(/[^a-z0-9-]/gi, '-')}-${cropSeq}-new`;
|
|
752
791
|
writePng(path.join(outDir, `${stem}.png`), crop);
|
|
753
792
|
md.push('', `})`, '', `<sub>${side} ยท ${p.sd.surface}${png.height > h ? ' (top of page)' : ''}</sub>`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "styleproof",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Catch every CSS change before it ships โ review PRs and certify refactors by the browser's computed styles, not pixels. Works with any styling system.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|