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 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 24px). */
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 6). */
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 x = Math.max(0, Math.min(Math.round(cx - w / 2), src.width - w));
84
- const y = Math.max(0, Math.min(Math.round(cy - h / 2), src.height - h));
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, x), Math.max(0, y), cw, ch, 0, 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 = '', pad: padBy = 24, minWidth = 320, minHeight = 180, maxHeight = 1600, maxCrops = 6, foldDetailsAt = 0, } = opts;
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
- images = { before: `${stem}-before.png`, after: `${stem}-after.png`, composite: `${stem}-composite.png` };
725
- // One side-by-side image per crop, left as a PLAIN image (not wrapped in a
726
- // link) so GitHub's built-in lightbox opens it in a zoomable popup on click —
727
- // a link wrapper would navigate to the file instead.
728
- md.push('', `![before ▶ after](${img(images.composite)})`, '', `<sub>◀ before · after ▶ — ${sd.surface} · click to zoom</sub>`);
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('', `![before ◀ │ ▶ after](${img(images.composite)})`, '', `<sub>◀ before · after ▶ — ${sd.surface}</sub>`, '', '<details>', '<summary>🔍 Highlight what changed</summary>', '', `![annotated before ◀ │ ▶ after](${img(images.annotated)})`, '', `<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('', `![new surface — ${side}](${img(`${stem}.png`)})`, '', `<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.7.2",
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",