styleproof 1.7.2 → 1.8.1
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 +31 -0
- package/dist/report.d.ts +2 -2
- package/dist/report.js +58 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.8.1]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- The crop annotation now boxes the **innermost** changed elements (the added
|
|
15
|
+
avatars, the restyled cards) instead of the container the crop anchors on —
|
|
16
|
+
whose box just traced the whole frame and told you nothing. An element present
|
|
17
|
+
on only one side (added/removed) is boxed only there.
|
|
18
|
+
|
|
19
|
+
## [1.8.0]
|
|
20
|
+
|
|
21
|
+
The crop now shows you where to look — without painting over the UI.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Annotated crop, alongside the clean one.** Each crop shows the clean
|
|
26
|
+
before|after composite by default (the real UI), with an annotated twin one
|
|
27
|
+
click away under a `🔍 Highlight what changed` toggle — a thin magenta outline
|
|
28
|
+
around each changed element on both sides, so the eye lands on exactly what the
|
|
29
|
+
bullet named. Outline only (never filled), and the clean image right there
|
|
30
|
+
proves the box isn't part of the design, so the marker can be confident without
|
|
31
|
+
reading as a change.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Tighter, more focused crops.** Default crop padding drops from 24px to 12px
|
|
36
|
+
(the change fills more of the frame), and up to 8 crop regions per surface
|
|
37
|
+
(was 6) before collapsing — so distinct changes get their own focused frame
|
|
38
|
+
instead of one wide merged one. Both are still tunable (`pad`, `maxCrops`).
|
|
39
|
+
- The action now commits the annotated crops alongside the composites.
|
|
40
|
+
|
|
10
41
|
## [1.7.2]
|
|
11
42
|
|
|
12
43
|
### Fixed
|
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
|
@@ -20,10 +20,17 @@ const union = (a, b) => {
|
|
|
20
20
|
};
|
|
21
21
|
const intersects = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
22
22
|
const visible = (b) => !!b && b.w > 0 && b.h > 0;
|
|
23
|
-
/** Outermost changed paths: drop any path that has a changed strict ancestor.
|
|
23
|
+
/** Outermost changed paths: drop any path that has a changed strict ancestor.
|
|
24
|
+
* Used to ANCHOR a crop (zoom to the whole changed region, not a leaf). */
|
|
24
25
|
function outermost(paths) {
|
|
25
26
|
return paths.filter((p) => !paths.some((q) => q !== p && p.startsWith(q + ' > ')));
|
|
26
27
|
}
|
|
28
|
+
/** Innermost changed paths: drop any path that has a changed strict descendant.
|
|
29
|
+
* Used to ANNOTATE — box the leaf elements that actually changed (the added
|
|
30
|
+
* avatars, the restyled cards), not their container, whose box ≈ the whole crop. */
|
|
31
|
+
function innermost(paths) {
|
|
32
|
+
return paths.filter((p) => !paths.some((q) => q !== p && q.startsWith(p + ' > ')));
|
|
33
|
+
}
|
|
27
34
|
/** Headline counts with the zeros dropped — `0 state-delta difference(s)` is noise. */
|
|
28
35
|
function changeCountLabel(shown) {
|
|
29
36
|
const parts = [];
|
|
@@ -80,13 +87,13 @@ function cropPng(src, box, w, h) {
|
|
|
80
87
|
// Center the fixed-size crop on the box, clamped to the image.
|
|
81
88
|
const cx = box.x + box.w / 2;
|
|
82
89
|
const cy = box.y + box.h / 2;
|
|
83
|
-
const
|
|
84
|
-
const
|
|
90
|
+
const ox = Math.max(0, Math.min(Math.round(cx - w / 2), src.width - w));
|
|
91
|
+
const oy = Math.max(0, Math.min(Math.round(cy - h / 2), src.height - h));
|
|
85
92
|
const cw = Math.min(w, src.width);
|
|
86
93
|
const ch = Math.min(h, src.height);
|
|
87
94
|
const out = new PNG({ width: cw, height: ch });
|
|
88
|
-
PNG.bitblt(src, out, Math.max(0,
|
|
89
|
-
return out;
|
|
95
|
+
PNG.bitblt(src, out, Math.max(0, ox), Math.max(0, oy), cw, ch, 0, 0);
|
|
96
|
+
return { png: out, ox, oy };
|
|
90
97
|
}
|
|
91
98
|
// Lossless but lean: drop the alpha channel (every crop/composite is opaque),
|
|
92
99
|
// max deflate, adaptive per-row filtering. ~15% smaller than the default, and
|
|
@@ -108,6 +115,29 @@ function fillRect(png, x, y, w, h, [r, g, b]) {
|
|
|
108
115
|
}
|
|
109
116
|
}
|
|
110
117
|
}
|
|
118
|
+
// The annotation hue: a magenta no real UI palette tends to use, so an outline
|
|
119
|
+
// reads as a marker, not content. Drawn as a hollow rectangle (never filled) so
|
|
120
|
+
// the UI underneath stays visible — and the clean image alongside proves the box
|
|
121
|
+
// isn't part of the design.
|
|
122
|
+
const HILITE = [255, 0, 200];
|
|
123
|
+
function strokeRect(png, x, y, w, h, t = 2, color = HILITE) {
|
|
124
|
+
fillRect(png, x, y, w, t, color); // top
|
|
125
|
+
fillRect(png, x, y + h - t, w, t, color); // bottom
|
|
126
|
+
fillRect(png, x, y, t, h, color); // left
|
|
127
|
+
fillRect(png, x + w - t, y, t, h, color); // right
|
|
128
|
+
}
|
|
129
|
+
/** Clone a crop and outline each changed element's box (page coords mapped into
|
|
130
|
+
* the crop via its origin), so the eye lands on exactly what the bullet named. */
|
|
131
|
+
function annotateCrop(crop, rects) {
|
|
132
|
+
const out = new PNG({ width: crop.png.width, height: crop.png.height });
|
|
133
|
+
PNG.bitblt(crop.png, out, 0, 0, crop.png.width, crop.png.height, 0, 0);
|
|
134
|
+
for (const [rx, ry, rw, rh] of rects) {
|
|
135
|
+
if (rw <= 0 || rh <= 0)
|
|
136
|
+
continue;
|
|
137
|
+
strokeRect(out, rx - crop.ox, ry - crop.oy, rw, rh);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
111
141
|
/**
|
|
112
142
|
* One before|after image: the two equal-size crops on a dark canvas with a
|
|
113
143
|
* neutral divider between them. Left is always before; before/after is labelled
|
|
@@ -591,7 +621,13 @@ function buildElementChanges(findings) {
|
|
|
591
621
|
return els;
|
|
592
622
|
}
|
|
593
623
|
export function generateStyleMapReport(opts) {
|
|
594
|
-
const { beforeDir, afterDir, outDir, imageBaseUrl = '',
|
|
624
|
+
const { beforeDir, afterDir, outDir, imageBaseUrl = '',
|
|
625
|
+
// Tighter than before (was 24) so the change fills the frame — the annotation
|
|
626
|
+
// box keeps enough context legible.
|
|
627
|
+
pad: padBy = 12, minWidth = 320, minHeight = 180, maxHeight = 1600,
|
|
628
|
+
// More, smaller crops before collapsing (was 6), so distinct changes get their
|
|
629
|
+
// own focused frame rather than one wide merged one.
|
|
630
|
+
maxCrops = 8, foldDetailsAt = 0, } = opts;
|
|
595
631
|
const includeNoise = opts.includeLayoutNoise ?? false;
|
|
596
632
|
const { surfaces, volatile: volatileCount } = diffStyleMapDirs(beforeDir, afterDir);
|
|
597
633
|
fs.mkdirSync(path.join(outDir, 'crops'), { recursive: true });
|
|
@@ -717,15 +753,22 @@ export function generateStyleMapReport(opts) {
|
|
|
717
753
|
const stem = `crops/${sd.surface.replace(/[^a-z0-9-]/gi, '-')}-${cropSeq}`;
|
|
718
754
|
const before = cropPng(pngA, cropBox, w, h);
|
|
719
755
|
const after = cropPng(pngB, cropBox, w, h);
|
|
720
|
-
const composite = compositePair(before, after);
|
|
721
|
-
writePng(path.join(outDir, `${stem}-before.png`), before);
|
|
722
|
-
writePng(path.join(outDir, `${stem}-after.png`), after);
|
|
756
|
+
const composite = compositePair(before.png, after.png);
|
|
723
757
|
writePng(path.join(outDir, `${stem}-composite.png`), composite);
|
|
724
|
-
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
758
|
+
// Annotated twin: outline the LEAF changed elements (the added avatars, the
|
|
759
|
+
// restyled cards) on each side — not the merged container the crop anchors
|
|
760
|
+
// on, whose box would just trace the whole frame. An element present on only
|
|
761
|
+
// one side (added/removed) is boxed only there.
|
|
762
|
+
const markPaths = innermost([...new Set(regionFindings.map((f) => f.path))]);
|
|
763
|
+
const rectsA = markPaths.map((p) => mapA.elements[p]?.rect).filter((r) => !!r);
|
|
764
|
+
const rectsB = markPaths.map((p) => mapB.elements[p]?.rect).filter((r) => !!r);
|
|
765
|
+
const annotated = compositePair(annotateCrop(before, rectsA), annotateCrop(after, rectsB));
|
|
766
|
+
writePng(path.join(outDir, `${stem}-annotated.png`), annotated);
|
|
767
|
+
images = { composite: `${stem}-composite.png`, annotated: `${stem}-annotated.png` };
|
|
768
|
+
// Clean before|after shown by default (the real UI); the annotated twin —
|
|
769
|
+
// boxes marking each change — sits one click away under a toggle. Plain
|
|
770
|
+
// images (no link wrap) so a click opens the full-resolution file.
|
|
771
|
+
md.push('', `})`, '', `<sub>◀ before · after ▶ — ${sd.surface}</sub>`, '', '<details>', '<summary>🔍 Highlight what changed</summary>', '', `})`, '', `<sub>magenta boxes mark each change — ${sd.surface}</sub>`, '', '</details>');
|
|
729
772
|
}
|
|
730
773
|
else if (!region) {
|
|
731
774
|
md.push('', '_Changed element is not visible in this state (zero-size box) — see the property list._');
|
|
@@ -753,7 +796,7 @@ export function generateStyleMapReport(opts) {
|
|
|
753
796
|
if (png) {
|
|
754
797
|
cropSeq++;
|
|
755
798
|
const h = Math.min(maxHeight, png.height);
|
|
756
|
-
const crop = cropPng(png, { x: 0, y: 0, w: png.width, h }, png.width, h);
|
|
799
|
+
const crop = cropPng(png, { x: 0, y: 0, w: png.width, h }, png.width, h).png;
|
|
757
800
|
const stem = `crops/${p.sd.surface.replace(/[^a-z0-9-]/gi, '-')}-${cropSeq}-new`;
|
|
758
801
|
writePng(path.join(outDir, `${stem}.png`), crop);
|
|
759
802
|
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.1",
|
|
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",
|