pixel-perfect-kit 0.1.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/CLONE-ALOYOGA-HEADER.md +76 -0
- package/CLONE-ANY-HEADER.md +106 -0
- package/CLONE-ANY-SITE.md +120 -0
- package/DEVELOP.md +144 -0
- package/LAUNCH-PROMPT.md +66 -0
- package/LEARNINGS.md +398 -0
- package/LICENSE +21 -0
- package/PLAYBOOK.md +260 -0
- package/README.md +219 -0
- package/WORKFLOW.md +257 -0
- package/bin/ppk +4 -0
- package/harness/adopt.js +52 -0
- package/harness/agent-setup.js +55 -0
- package/harness/behavior-selftest.js +243 -0
- package/harness/benchmarks/README.md +34 -0
- package/harness/benchmarks/battery.js +86 -0
- package/harness/benchmarks/detection-power.js +75 -0
- package/harness/capture-build-selftest.js +134 -0
- package/harness/capture-build.js +323 -0
- package/harness/doctor-selftest.js +58 -0
- package/harness/doctor.js +85 -0
- package/harness/fixtures/01-backdrop-color.js +51 -0
- package/harness/fixtures/02-line-strut.js +53 -0
- package/harness/fixtures/03-compat-mode.js +44 -0
- package/harness/fixtures/README.md +31 -0
- package/harness/human-qa-selftest.js +201 -0
- package/harness/human-qa.js +406 -0
- package/harness/merge-snapshot-selftest.js +56 -0
- package/harness/new-target.js +101 -0
- package/harness/regression.js +50 -0
- package/harness/score.js +58 -0
- package/harness/serve.js +149 -0
- package/harness/setup-selftest.js +93 -0
- package/harness/setup.js +158 -0
- package/harness/tunnel-selftest.js +76 -0
- package/harness/tunnel.js +241 -0
- package/harness/workflow-selftest.js +211 -0
- package/harness/workflow.js +739 -0
- package/package.json +40 -0
- package/skill/ditto-finish/SKILL.md +52 -0
- package/skill/pixel-perfect-clone/SKILL.md +49 -0
- package/tools/RUNBOOK.md +228 -0
- package/tools/behavior-capture.js +307 -0
- package/tools/behavior-worksheet.js +82 -0
- package/tools/browser-capture.js +240 -0
- package/tools/cli-selftest.js +136 -0
- package/tools/extract-fonts.js +133 -0
- package/tools/extract-icons.js +255 -0
- package/tools/merge-snapshot.js +56 -0
- package/tools/pixel-diff.js +845 -0
- package/tools/reassemble.js +63 -0
- package/tools/selftest.js +67 -0
- package/tools/sink.js +80 -0
package/PLAYBOOK.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# PLAYBOOK — clone a site pixel-perfect, and prove it with numbers
|
|
2
|
+
|
|
3
|
+
A repeatable method for recreating a website's visual identity and verifying the
|
|
4
|
+
match objectively. Every phase has a **Do**, a **Verify** (an objective check), and
|
|
5
|
+
an **Evaluate** (the pass bar). No step is "done" because it looks right — it's done
|
|
6
|
+
because a command exits 0.
|
|
7
|
+
|
|
8
|
+
**The one principle** (everything below is a corollary):
|
|
9
|
+
|
|
10
|
+
> Measure the **painted mark**, not the wrapper — and measure it **completely**.
|
|
11
|
+
> Text → the glyph box via `Range` + all font metrics. Icons/logos → the drawn
|
|
12
|
+
> pixels (SVG bbox or background element + `background-position`). Colour, underline,
|
|
13
|
+
> shadow are painted marks too. A wrapper box, or a hand-picked subset of properties,
|
|
14
|
+
> will pass while the pixels are wrong.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Phase 0 — Decide the target and the viewport
|
|
19
|
+
**Do:** Pick the exact page/section to clone and a **fixed measurement width** (e.g.
|
|
20
|
+
1728px). Everything is measured at that width; the diff refuses to trust x-positions
|
|
21
|
+
if the two snapshots differ in width.
|
|
22
|
+
**Verify:** you can state the width and section in one sentence.
|
|
23
|
+
**Evaluate:** ✅ target + width written down.
|
|
24
|
+
|
|
25
|
+
## Phase 1 — Extract the real assets (never redraw / retype)
|
|
26
|
+
**Do:** On the live site's DevTools console:
|
|
27
|
+
- `extractFonts(/family/i)` → `<host>-fonts.zip` (real `.woff2` + a `fonts.css`).
|
|
28
|
+
- `extractIcons('header')` → `<host>-icons.zip` (clean SVGs + `icons.css` with the
|
|
29
|
+
**exact** original data-URIs + a `preview.html` + `report.json`).
|
|
30
|
+
- Capture any non-icon vectors (logo/wordmark `<svg>`) and rasters by their real
|
|
31
|
+
`src`/`srcset` URLs.
|
|
32
|
+
**Verify:** every `.woff2` begins with magic bytes `wOF2`; each SVG starts with
|
|
33
|
+
`<svg` and matches the live path data; each raster downloads at the same URL/size.
|
|
34
|
+
**Evaluate:** ✅ every shipped asset is a real captured file, none hand-drawn/typed.
|
|
35
|
+
|
|
36
|
+
## Phase 2 — Measure the design spec (no guessing)
|
|
37
|
+
**Do:** From the live DOM, capture a deduped inventory of every rendered text style
|
|
38
|
+
— `weight / size / line-height / letter-spacing / text-transform / color` — plus the
|
|
39
|
+
logo/icon intrinsic sizes and the section's box geometry. Record in a `spec.md`.
|
|
40
|
+
**Verify:** the token table covers every distinct text role (hero, heading, nav,
|
|
41
|
+
button, label, body, caption) and each icon's intrinsic size.
|
|
42
|
+
**Evaluate:** ✅ the spec is measured, not inferred. **`line-height` and
|
|
43
|
+
`letter-spacing` are mandatory columns** — they are the two everyone skips, and they
|
|
44
|
+
decide where glyphs land.
|
|
45
|
+
|
|
46
|
+
## Phase 3 — Capture the breakpoints
|
|
47
|
+
**Do:** Sweep viewport widths and read the live stylesheet's `@media` rules to record
|
|
48
|
+
the exact px breakpoints and how each token shifts. Set your clone's `screens` to match.
|
|
49
|
+
**Verify:** every width where a measured token changes is recorded with its exact px.
|
|
50
|
+
**Evaluate:** ✅ breakpoints come from the live `@media`, not from eyeballing the sweep.
|
|
51
|
+
|
|
52
|
+
## Phase 4 — Build: capture by default, rebuild only to integrate
|
|
53
|
+
**Do (default — build by capture):** don't reconstruct the markup; capture it. The gate's
|
|
54
|
+
chronic blind spot is *technique* mismatches — same numbers, different construction,
|
|
55
|
+
different rasterisation (LEARNINGS #12/#14/#15/#17/#18) — and every one of them is
|
|
56
|
+
self-inflicted by hand-rebuilding. A clone built from the captured post-hydration DOM
|
|
57
|
+
inherits live's doctype, authored line-heights, `font-feature-settings`, and drawing
|
|
58
|
+
primitives **by construction** (LEARNINGS #19). Capture the settled DOM with
|
|
59
|
+
`pxSendDom('http://localhost:7799/dom.html')`, then:
|
|
60
|
+
```sh
|
|
61
|
+
ppk capture-build <name> # self-hosts CSS+fonts, strips scripts/CSP, PRESERVES the doctype
|
|
62
|
+
```
|
|
63
|
+
See RUNBOOK "Build by capture". JS-driven behavior and animated/generative content can't
|
|
64
|
+
be captured statically — reproduce those separately.
|
|
65
|
+
**Do (rebuild — only when the deliverable is a component in your own stack):** recreate
|
|
66
|
+
the section driven strictly by the captured spec: self-host the woff2s, use the real icon
|
|
67
|
+
data-URIs, apply the measured type tokens. Reproduce quirks faithfully (e.g. a faux-bold
|
|
68
|
+
button that renders `font-weight:900` over a 700-max face — replicate the synthesis, don't
|
|
69
|
+
substitute a heavier font). Expect the technique-mismatch class to be live — every
|
|
70
|
+
LEARNINGS rule below was paid for on this path.
|
|
71
|
+
**Verify:** the page renders standalone in the self-hosted faces (no external font
|
|
72
|
+
request); capture-build exited 0 (a failed asset download is loud, never silent).
|
|
73
|
+
**Evaluate:** ✅ builds clean; assets resolve locally.
|
|
74
|
+
|
|
75
|
+
## Phase 5 — Verify with the numeric diff (the core)
|
|
76
|
+
**Do:** Capture both pages at the same width and diff. See `tools/RUNBOOK.md` for the
|
|
77
|
+
exact fast sequence. In short:
|
|
78
|
+
- Clone (localhost, no CSP): `pxSend("http://localhost:7799/clone.json")` — one call.
|
|
79
|
+
- Live (strict CSP): inject `pixel-diff.js` **source directly** (never
|
|
80
|
+
`fetch(...).then(eval)` — that hangs to a 45s timeout), then `pxStash()` +
|
|
81
|
+
batched `pxRead(0..n)` → reassemble as `live.json`.
|
|
82
|
+
```sh
|
|
83
|
+
node tools/pixel-diff.js live.json clone.json --visual # "does it look identical?" exit 0 = pass
|
|
84
|
+
node tools/pixel-diff.js live.json clone.json # strict: also flags structure
|
|
85
|
+
```
|
|
86
|
+
Measure **all** of these per element (skipping any one is how a defect hides):
|
|
87
|
+
geometry (`x,y,w,h,top,right,bottom,fromRight`); the **text-glyph box** via `Range`
|
|
88
|
+
(not the element box — padding moves the box but not the glyphs); the **painted
|
|
89
|
+
glyph** for icons/logos (SVG bbox or bg element + `background-position`, not the
|
|
90
|
+
clickable wrapper); the full box-model; **font incl. `line-height`,
|
|
91
|
+
`letter-spacing`, `color`, `-webkit-font-smoothing`, and `underline`**; layout + the
|
|
92
|
+
parent's `gap`.
|
|
93
|
+
|
|
94
|
+
**Every painted mark is a box — and the gate now measures the ones that used to slip a
|
|
95
|
+
green sweep, so trust the diff instead of re-deriving them:** `font.smoothing`
|
|
96
|
+
(`-webkit-font-smoothing` — changes *perceived weight* while `font-weight` matches), the
|
|
97
|
+
**underline as a box** (`underline.{thickness,x,right,w,top,bottom}`, measured on
|
|
98
|
+
whichever element — often an **ancestor** — draws it, not a boolean), and the **painted
|
|
99
|
+
backdrop** (`bg` — the nearest opaque `background-color` behind a mark, a
|
|
100
|
+
transparent chain resolving to the white canvas; catches a wrong announcement-bar /
|
|
101
|
+
button / badge colour that lives on a container the text leaf never measured — #16).
|
|
102
|
+
`--visual` compares all three — plus the **line-box strut** (`font.strut`: the nearest
|
|
103
|
+
line-box *container's* line-height, often an ancestor td/div you never targeted; it
|
|
104
|
+
positions the glyphs vertically, and `normal` vs a number is a technique mismatch that
|
|
105
|
+
drifts across platforms even when the same-machine delta is sub-tolerance — #17).
|
|
106
|
+
Generalise the principle to any mark the tool doesn't special-case (a
|
|
107
|
+
`box-shadow`, an `outline`): measure the drawing element's box with `--inspect`. Set the
|
|
108
|
+
clone root to `antialiased` / `grayscale` by default. What the gate can *not* do —
|
|
109
|
+
reproducing the technique so it also rasterises identically — is Phase 6.
|
|
110
|
+
|
|
111
|
+
**`--visual` vs strict.** `--visual` compares only pixel-determining props (geometry,
|
|
112
|
+
text-glyph box, `font weight/size/line/spacing/transform/color/decoration/underline`,
|
|
113
|
+
glyph center + `background-position`) — use it to answer "does it look identical?".
|
|
114
|
+
Strict (default) additionally compares structure (`display`, `position`, parent
|
|
115
|
+
`gap`, padding, the `font-family` alias); two valid implementations legitimately
|
|
116
|
+
differ there — fix or **document** each structural delta, don't fail the pixel check
|
|
117
|
+
on it.
|
|
118
|
+
|
|
119
|
+
**Verify:** `--visual` exits 0 at every breakpoint.
|
|
120
|
+
**Evaluate:** ✅ only when every pixel-determining property is within tolerance
|
|
121
|
+
(0.5px default) at every breakpoint.
|
|
122
|
+
|
|
123
|
+
## Phase 5b — Close coverage (don't skip)
|
|
124
|
+
**Do:** After `--visual` is green, **auto-enumerate** every painted leaf in the
|
|
125
|
+
region (each element with own text, a background-image, or an `<svg>`) and require a
|
|
126
|
+
`pxTargets` entry for each. Add the missing ones and re-diff.
|
|
127
|
+
**Verify:** the count of painted elements in the region equals the count of targets;
|
|
128
|
+
off-region/hidden nodes (x<0, display:none, flyouts below the band) are excluded. A
|
|
129
|
+
**solid-colour container** (an announcement bar, a coloured button) is a painted mark
|
|
130
|
+
too: either target it, or rely on its text/icon child — whose `bg` now carries that
|
|
131
|
+
backdrop colour (#16) — but don't leave a coloured bar with no covered mark on it.
|
|
132
|
+
**Evaluate:** ✅ coverage list empty. *A green table only proves the elements you
|
|
133
|
+
measured; an uncovered element is unverified, not matched.*
|
|
134
|
+
|
|
135
|
+
## Phase 5c — Reproduce JS-driven dynamics (the `behavior` phase)
|
|
136
|
+
Statics are proven by Phases 5/5b before this one runs. A static capture strips `<script>`,
|
|
137
|
+
so anything driven by JS — animations, rotations, reveals, marquees, counters, hover-mounted
|
|
138
|
+
content — is frozen at its captured moment. This phase reproduces it, and the workflow's
|
|
139
|
+
`behavior` gate proves the reproduction by MEASURED number, never by eye. Method ported
|
|
140
|
+
verbatim from `lovable_dupe_html/CLONE_PLAYBOOK.md` §8/§8a — this is a port, not a
|
|
141
|
+
reinvention:
|
|
142
|
+
|
|
143
|
+
1. **Static pass (candidates, necessary but noisy):** grep the captured CSS for every
|
|
144
|
+
`@keyframes` name; grep the DOM for `opacity-0`, `translate-x/y-*`, `blur-`,
|
|
145
|
+
`will-change`, `data-[starting-style]`, `animate-*`, `data-state` markers.
|
|
146
|
+
`tools/behavior-capture.js`'s `pxBehaviorDiscover()` does this scan for you.
|
|
147
|
+
2. **Dynamic differential pass on LIVE (authoritative):** attach a `MutationObserver`
|
|
148
|
+
(watching `class`/`style`/`data-*`) across the region, snapshot each candidate's computed
|
|
149
|
+
`opacity`/`transform`/`filter` before, then **scripted-scroll** the page in increments
|
|
150
|
+
(dwelling at each stop) and dispatch synthetic hover events on each trigger. Record what
|
|
151
|
+
changed, from what to what, and by what trigger (load/scroll/hover/mutation). A candidate
|
|
152
|
+
frozen in its start state after the sweep is presentational noise, not a behavior.
|
|
153
|
+
`pxBehaviorDiscover()` runs this pass and writes the result — save it as
|
|
154
|
+
`targets/<name>/behaviors-live.json`.
|
|
155
|
+
3. **Measure, never eyeball.** A marquee's speed is a real px/sec sampled over a real
|
|
156
|
+
1-second window (`pxBehaviorDiscover`'s `marqueeSelectors` option), not "looks about
|
|
157
|
+
right." A reveal's end-state is its settled computed style, not a screenshot glance.
|
|
158
|
+
4. **Reproduce in ONE vanilla `clone/fixes.js`** (no framework, each behavior in its own
|
|
159
|
+
guarded `try`). Common patterns and how to do them right (full detail + the reference
|
|
160
|
+
implementation live in `lovable_dupe_html/CLONE_PLAYBOOK.md` §8 and
|
|
161
|
+
`lovable_dupe_html/snapshot/fixes.js`):
|
|
162
|
+
- **Marquee/logo belt:** find the actual moving wrapper (often holds *multiple* track
|
|
163
|
+
copies inside an `overflow-clip` viewport) and animate that WHOLE wrapper as one unit —
|
|
164
|
+
animating a single inner track gives "static + moving at the same time."
|
|
165
|
+
- **Scroll reveals / gradient sweeps:** the element is usually already in the DOM, stuck
|
|
166
|
+
in its start state. Re-trigger via `IntersectionObserver` on scroll-in.
|
|
167
|
+
- **Hover-mounted content (mega-menus, portals):** not in the static DOM at all — capture
|
|
168
|
+
the live markup on hover (dispatch `pointerover`/`pointerenter`, wait for it to settle,
|
|
169
|
+
POST the panel's `outerHTML`), sanitize it, and toggle it on hover positioned by the
|
|
170
|
+
MEASURED relationship to its anchor, never a guessed offset.
|
|
171
|
+
- **Typewriter / rotating placeholder:** sample the live text over several seconds to
|
|
172
|
+
collect the real phrases before typing/erasing them.
|
|
173
|
+
`harness/capture-build.js --fixes` wires `<script src="fixes.js" defer>` before `</body>`
|
|
174
|
+
and scaffolds a starter file the first time (never overwrites one you've written).
|
|
175
|
+
5. **Discover on the CLONE the same way**, with `fixes.js` loaded and captured AFTER the
|
|
176
|
+
same settle procedure as live (so end-states genuinely match, not just at t=0) →
|
|
177
|
+
`targets/<name>/behaviors-clone.json`.
|
|
178
|
+
6. **Gate:** `node harness/workflow.js gate <name> behavior` compares every live behavior to
|
|
179
|
+
the clone's by key and measured value within a documented tolerance (WORKFLOW.md). A
|
|
180
|
+
behavior that's genuinely irreproducible statically (WebGL/canvas generative content) is
|
|
181
|
+
documented in `targets/<name>/behavior-deviations.json` with a reason — never silently
|
|
182
|
+
dropped. An empty/absent live inventory does not pass unless the discovery pass's own
|
|
183
|
+
metadata proves it actually ran (scroll sweep range, observer duration, elements scanned).
|
|
184
|
+
|
|
185
|
+
## Phase 6 — The human-flagged fix loop (detection → measure → fix → verify)
|
|
186
|
+
Detection stays with a human (or a vision reviewer) — they're the best perceptual
|
|
187
|
+
detector. **The workflow enforces this as the `human` phase**: `node harness/human-qa.js
|
|
188
|
+
file <name> --draft <public-url>` files a scope-pinned PingHumans side-by-side (per-leaf
|
|
189
|
+
steps from coverage.json; JS behavior marked informational), and the gate passes only on
|
|
190
|
+
a fetched approving verdict — a rejection's notes are your flag list for the loop below,
|
|
191
|
+
then refile. But once a human points, **stop guessing the property.** Measure the whole
|
|
192
|
+
element:
|
|
193
|
+
|
|
194
|
+
1. **Localize the element that *paints* the mark — not the one you labelled.** A
|
|
195
|
+
colour lives on the text, but an underline may be a `border-bottom` on an
|
|
196
|
+
**ancestor group**, a shadow on a wrapper, a rule on a sibling. Resolve *that*
|
|
197
|
+
element on **both** pages:
|
|
198
|
+
```js
|
|
199
|
+
copy(pxInspect({ text: "sign in" })) // also {aria:/cart/}, {sel:".x"}, {at:[x,y]}
|
|
200
|
+
```
|
|
201
|
+
Save `el_live.json` / `el_clone.json`. (Live CSP → `pxStashInspect(resolver)` +
|
|
202
|
+
batched `pxRead`.) If the paint bucket comes back empty but the human still sees it,
|
|
203
|
+
you inspected the wrong element — walk up to the ancestor whose box actually draws
|
|
204
|
+
the mark and measure *its* box (top/height/border), because that box positions it.
|
|
205
|
+
2. **Measure** — diff the element's **entire computed style**, paint-first:
|
|
206
|
+
```sh
|
|
207
|
+
node tools/pixel-diff.js --inspect el_live.json el_clone.json
|
|
208
|
+
```
|
|
209
|
+
- **PAINT** bucket = every visible difference with exact values (colour, border,
|
|
210
|
+
decoration/underline, background, transform, opacity, geometry…). *This is the
|
|
211
|
+
fix list.*
|
|
212
|
+
- **STRUCTURAL** = layout-technique differences, demoted (usually fine).
|
|
213
|
+
- Irrelevant props (`transition`, `cursor`, …) hidden.
|
|
214
|
+
3. **Fix by reproducing the box model + technique, not by offsetting.** If the mark's
|
|
215
|
+
position falls out of a box (a `border-bottom` sits at a group's content-bottom;
|
|
216
|
+
the group's `top`+`height`+`box-sizing` decide where), build **that same box** so
|
|
217
|
+
the mark *emerges* in place. A hand-tuned `top:-3.6px` that merely lands the number
|
|
218
|
+
is fragile — it drifts when neighbours change and **rasterises differently** (a
|
|
219
|
+
positioned `<div>` ≠ a `border`), so it fails a flicker/overlay comparison even at
|
|
220
|
+
0.01px. Match the drawing element's type too.
|
|
221
|
+
4. **Fix the *whole* mark in one shot, not the one facet named.** A decoration has
|
|
222
|
+
thickness + width + offset + colour + technique; fixing only "it's too thin" leaves
|
|
223
|
+
width and Y wrong and the human iterates again. Measure and set all of them together.
|
|
224
|
+
5. **Verify** — re-run step 2. **Done = the PAINT bucket is empty (exit 0)** — a
|
|
225
|
+
measured fact, never a screenshot.
|
|
226
|
+
|
|
227
|
+
Why this beats patching the tool per bug: the measure step compares the **whole**
|
|
228
|
+
computed style, so a novel property (a shadow, an outline) is caught the **first**
|
|
229
|
+
time — you never grow an allowlist one embarrassing miss at a time.
|
|
230
|
+
|
|
231
|
+
## Phase 7 — Final evaluation
|
|
232
|
+
**Do:** Produce one match table straight from `pixel-diff.js` output (element →
|
|
233
|
+
property → live vs ours → Δ → ✅/❌), noting intentional non-matches and why.
|
|
234
|
+
**Verify:** `--visual` exits 0 at every breakpoint; coverage list empty; every strict
|
|
235
|
+
structural delta explained; every human-flagged element's `--inspect` paint bucket empty.
|
|
236
|
+
**Evaluate:** overall pass = real assets + all pixel-determining properties of all
|
|
237
|
+
covered elements within tolerance + no uncovered element + every structural
|
|
238
|
+
difference documented. A green diff that skipped elements or properties is not a pass.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Ground rules (the non-negotiables)
|
|
243
|
+
1. **No asset is hand-drawn or hand-typed** — fonts/icons/logos/vectors/images come
|
|
244
|
+
only from the extractors or a direct live-site capture (real files, real URLs).
|
|
245
|
+
2. **No eyeballing.** Every "match" is a `pixel-diff.js` run. Screenshots decide only
|
|
246
|
+
*what to measure*, never *whether it matches*.
|
|
247
|
+
3. **No silent gaps.** Every painted element in the region has a target; enumerate
|
|
248
|
+
and close coverage.
|
|
249
|
+
4. **Match across breakpoints** — run the diff at each captured breakpoint.
|
|
250
|
+
5. **Document intentional deviations** (faux-bold, structural technique choices,
|
|
251
|
+
accepted sub-pixel deltas) with their cause — never silent.
|
|
252
|
+
6. **A colour / visibility / underline delta is never "structural."** If strict shows
|
|
253
|
+
`font.color` or `font.underline`, fix it — don't wave it off as noise.
|
|
254
|
+
7. **Reproduce the technique of a painted mark, not just its number.** A border,
|
|
255
|
+
decoration, or shadow must be drawn the way the live site draws it (right element,
|
|
256
|
+
right property), so it rasterises identically under a flicker/overlay compare. A
|
|
257
|
+
magic offset that lands the coordinate but uses a different primitive is a defect.
|
|
258
|
+
8. **A mark is a box.** Decorations (underline/strike/border) and glyphs have
|
|
259
|
+
thickness, width, and position — measure and match all of them, and capture
|
|
260
|
+
`-webkit-font-smoothing`. "Present: true" or a matched colour is not a matched mark.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# pixel-perfect-kit
|
|
2
|
+
|
|
3
|
+
A small, framework-agnostic toolkit for **cloning a website's visual identity and
|
|
4
|
+
proving the clone is pixel-perfect with numbers, never by eye.** Point it at any
|
|
5
|
+
live site, lift the real fonts/icons/vectors, then diff your recreation against the
|
|
6
|
+
live DOM property-by-property until every pixel-determining value matches.
|
|
7
|
+
|
|
8
|
+
The guiding rule behind everything here:
|
|
9
|
+
|
|
10
|
+
> **Measure the *painted mark*, not the wrapper — and measure it *completely*.**
|
|
11
|
+
> A green check is a command that exits 0, not a screenshot that "looks close."
|
|
12
|
+
|
|
13
|
+
## Start here (one line, then just talk to your agent)
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx pixel-perfect-kit setup
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
One interactive pass: installs `ppk`, offers `cloudflared`, runs the PingHumans login
|
|
20
|
+
(skippable — local review mode needs no account), checks for the optional `ditto` fast
|
|
21
|
+
builder, and teaches your AI agent both skills. Idempotent — re-run anytime. (Prefer the
|
|
22
|
+
pieces? `npm i -g pixel-perfect-kit`, then `ppk doctor` + `ppk agent-setup`.)
|
|
23
|
+
|
|
24
|
+
Then, in your AI agent (e.g. Claude Code with browser access):
|
|
25
|
+
|
|
26
|
+
> **"Clone https://example.com pixel-perfect."**
|
|
27
|
+
|
|
28
|
+
The agent drives the whole pipeline — capture, numeric gates, behavior reproduction,
|
|
29
|
+
review rounds — and you do exactly one job:
|
|
30
|
+
|
|
31
|
+
### You are the reviewer
|
|
32
|
+
Review pings arrive with a **side-by-side compare UI** (original vs clone). Steer by
|
|
33
|
+
**pinning comments** on anything that looks wrong, in your own words — "this text sits a
|
|
34
|
+
bit high", "these cards should animate in". **Always pick a verdict button**: comment-only
|
|
35
|
+
reviews stall the pipeline (approval is never inferred from prose). Your browser's
|
|
36
|
+
rendering of the original is the ground truth — even when it differs from what the
|
|
37
|
+
agent's browser sees (LEARNINGS #20). The clone is *done* only when every machine gate
|
|
38
|
+
is green **and** you've pressed approve.
|
|
39
|
+
|
|
40
|
+
Two review modes, same contract:
|
|
41
|
+
- **Marketplace (default)** — rounds go to PingHumans reviewers over a public tunnel and
|
|
42
|
+
bill small credits (`npx cpyany setup` creates the login). Independent verdicts.
|
|
43
|
+
- **Local (no account, no tunnel, free)** — `ppk human <name> file --local`, then open
|
|
44
|
+
`http://localhost:8080/__review`: the clone with click-to-pin, the same step list, the
|
|
45
|
+
same mandatory verdict buttons. Recorded as `provider:"local"` in every receipt —
|
|
46
|
+
operator-trusted by definition, so it's never mistaken for independent review.
|
|
47
|
+
|
|
48
|
+
Prerequisites doctor checks for you: Node ≥ 18 (required), plus `cloudflared` and the
|
|
49
|
+
PingHumans login (marketplace mode only — local mode needs neither) — and your agent
|
|
50
|
+
needs browser automation.
|
|
51
|
+
|
|
52
|
+
### Bring your own builder (ditto, Lovable, anything)
|
|
53
|
+
|
|
54
|
+
Generators like [ditto](https://github.com/ion-design/ditto.site) compile a URL into a
|
|
55
|
+
runnable codebase — by their own framing, a **starting point (~90%)**, not a finished
|
|
56
|
+
replica. The kit's human-review loop is the last 9%: it works with ANY builder's output,
|
|
57
|
+
no pixel pipeline required.
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
ppk adopt acme https://acme.com # register the external build for the loop
|
|
61
|
+
npm run dev # your builder's dev server (ditto: next/vite)
|
|
62
|
+
ppk tunnel acme --url http://localhost:3000 # public, reachability-verified
|
|
63
|
+
ppk human acme file # round 1: a human compares clone vs original
|
|
64
|
+
# fix what they pin → refile with --changelog "…" → repeat until the verdict approves
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Agents get this flow automatically: `ppk agent-setup` installs the **ditto-finish**
|
|
68
|
+
skill alongside the main one — "clone https://acme.com with ditto and polish it" runs
|
|
69
|
+
build → adopt → tunnel → human rounds until approval, unattended between your reviews.
|
|
70
|
+
|
|
71
|
+
### Bring your own builder (ditto, Lovable, hand-built…)
|
|
72
|
+
Generators like [ditto](https://github.com/ion-design/ditto.site) compile a URL into a
|
|
73
|
+
runnable codebase and, by their own framing, produce **starting points — roughly 90%**.
|
|
74
|
+
The human-review loop is the last 9: publish the build, put it side-by-side with the
|
|
75
|
+
original in front of a real human, fix what they pin, refile with a changelog, repeat
|
|
76
|
+
until they approve. The loop is builder-agnostic:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
ppk adopt mysite https://example.com # register the external build (no pixel gates)
|
|
80
|
+
ppk tunnel mysite --url http://localhost:3000 # publish its dev server (reachability-verified)
|
|
81
|
+
ppk human mysite file # round 1 — then fix pins, refile with --changelog
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Agents get this as the **ditto-finish** skill (installed by `ppk agent-setup`): "clone
|
|
85
|
+
https://example.com with ditto and polish it" runs the whole ditto → human-loop flow.
|
|
86
|
+
|
|
87
|
+
<details>
|
|
88
|
+
<summary>Driving it by hand / developing the kit (the manual quickstart)</summary>
|
|
89
|
+
|
|
90
|
+
Run `ppk` in any project — your clone `targets/` are created in the current directory;
|
|
91
|
+
the kit's tools/docs come from the installed package (`ppk where` prints their location).
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
ppk new aloyoga https://www.aloyoga.com/ 1728 # scaffold a target + seed the workflow
|
|
95
|
+
ppk sink & # snapshot receiver (:7799)
|
|
96
|
+
# on the live tab: await pxSendDom('http://localhost:7799/dom.html') ← capture the DOM
|
|
97
|
+
ppk capture-build aloyoga # DEFAULT build: clone FROM the capture (LEARNINGS #19)
|
|
98
|
+
ppk serve aloyoga # serve the clone + the kit's /tools (:8080)
|
|
99
|
+
# …measure live, capture the clone… (see PLAYBOOK.md + tools/RUNBOOK.md)
|
|
100
|
+
ppk score aloyoga # is this iteration better? (a number, not a vibe)
|
|
101
|
+
ppk status aloyoga # the enforced workflow: what's done / next / gated
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The workflow is **enforced** (see `WORKFLOW.md`): `ppk advance <name> <phase>` refuses to mark a
|
|
105
|
+
phase done unless its gate exits 0, and refuses out of order — so nothing claims "pixel-perfect"
|
|
106
|
+
without the receipts. `ppk help` lists every command. Handing the run to an agent yourself?
|
|
107
|
+
`LAUNCH-PROMPT.md` is the canonical wrapper (the skill uses the same path).
|
|
108
|
+
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
## What's in the box
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
pixel-perfect-kit/
|
|
115
|
+
├── README.md ← you are here — what it is, quickstart, tool index
|
|
116
|
+
├── PLAYBOOK.md ← the method: how to clone + verify, step by step
|
|
117
|
+
├── WORKFLOW.md ← the ENFORCED pipeline: hard-gated phases (the kit's `gjc`)
|
|
118
|
+
├── LEARNINGS.md ← the failure catalog — read before trusting any "match"
|
|
119
|
+
├── DEVELOP.md ← the META-LOOP: improve the kit by cloning real sites
|
|
120
|
+
├── CLONE-ANY-SITE.md ← the ONE-SHOT template: whole page, capture-built, gated,
|
|
121
|
+
│ human-approved — hand it to an agent and wait
|
|
122
|
+
├── CLONE-ANY-HEADER.md ← ready-to-run prompt template ({{URL}}/{{WIDTH}}/{{OUTPUT_PATH}})
|
|
123
|
+
├── CLONE-ALOYOGA-HEADER.md ← the same prompt, pre-filled for aloyoga.com (example)
|
|
124
|
+
├── tools/
|
|
125
|
+
│ ├── extract-fonts.js paste in DevTools → one zip of the real @font-face woff2s
|
|
126
|
+
│ ├── extract-icons.js paste in DevTools → one zip of the real SVG/data-URI icons
|
|
127
|
+
│ ├── pixel-diff.js the measure + numeric-diff engine (browser capture + Node diff)
|
|
128
|
+
│ ├── browser-capture.js the browser half of pixel-diff.js, split out to inject as
|
|
129
|
+
│ │ PLAIN SOURCE on a strict-CSP live site (no base64/gzip)
|
|
130
|
+
│ ├── behavior-capture.js the browser half of the `behavior` phase: discovers + MEASURES
|
|
131
|
+
│ │ JS-driven dynamics (reveals, marquees, rotations, hover menus)
|
|
132
|
+
│ │ on live and clone, so reproduction is judged by number — and
|
|
133
|
+
│ │ keeps declared-but-unfired candidates as inventory (no-js /
|
|
134
|
+
│ │ bot-gated sites where nothing fires but everything should)
|
|
135
|
+
│ ├── behavior-worksheet.js one row per supposed-to-move behavior with its disposition;
|
|
136
|
+
│ │ unresolved rows print ready-to-send questions for the human
|
|
137
|
+
│ ├── sink.js tiny POST→file receiver for delivering the clone snapshot
|
|
138
|
+
│ ├── merge-snapshot.js fold a PARTIAL re-capture into a full snapshot (fast fix-loop
|
|
139
|
+
│ │ iteration; the done gate demands one final full capture)
|
|
140
|
+
│ ├── selftest.js guards the diff engine — asserts it still compares the
|
|
141
|
+
│ │ underline box + font-smoothing (run: node tools/selftest.js)
|
|
142
|
+
│ └── RUNBOOK.md the fast, verified command sequence for a live-vs-clone diff
|
|
143
|
+
├── bin/ppk shim for the enforced workflow (→ harness/workflow.js)
|
|
144
|
+
├── harness/ the dev framework (see DEVELOP.md)
|
|
145
|
+
│ ├── new-target.js scaffold a disposable clone workspace for a URL (+ seed the workflow)
|
|
146
|
+
│ ├── capture-build.js the DEFAULT build step: clone FROM the captured live DOM —
|
|
147
|
+
│ │ self-hosts CSS/fonts, strips scripts/CSP, preserves the doctype
|
|
148
|
+
│ │ byte-for-byte (kills the technique-mismatch class, LEARNINGS #19)
|
|
149
|
+
│ ├── human-qa.js the HUMAN phase: files a scope-pinned PingHumans side-by-side and
|
|
150
|
+
│ │ gates on the fetched verdict — human iteration rounds, unattended
|
|
151
|
+
│ ├── tunnel.js public HTTPS tunnel for the clone, byte-VERIFIED to serve it before
|
|
152
|
+
│ │ it's recorded (a dead tunnel burns a human round — hn round 1)
|
|
153
|
+
│ ├── workflow.js the ENFORCED phase state machine — gates block until a check exits 0
|
|
154
|
+
│ ├── serve.js static server for a target's clone (+ /tools)
|
|
155
|
+
│ ├── score.js score live-vs-clone, compare to the previous run
|
|
156
|
+
│ ├── regression.js selftest + workflow-selftest + every fixture + detection battery — the guardrail
|
|
157
|
+
│ ├── fixtures/ one file per class-of-miss, so it can never recur
|
|
158
|
+
│ └── benchmarks/ detection-power battery — score a gate change old-vs-new
|
|
159
|
+
│ (node harness/benchmarks/detection-power.js --vs HEAD)
|
|
160
|
+
└── targets/ ← DISPOSABLE per-site instances; the kit is the product
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Developing the kit itself** (cloning sites to make the tools + instructions better) —
|
|
164
|
+
see **`DEVELOP.md`**. In short: `node harness/new-target.js <name> <url> <width>`, build
|
|
165
|
+
the clone, `node harness/score.js <name>` to loop, and turn every miss the gate didn't
|
|
166
|
+
catch into a tool check + a `harness/fixtures/` regression + a generalized lesson.
|
|
167
|
+
|
|
168
|
+
Zero dependencies. The extractors and the browser half of `pixel-diff.js` run by
|
|
169
|
+
pasting into a DevTools console (or via a browser-automation `javascript_tool`);
|
|
170
|
+
the diff half runs in Node (`node tools/pixel-diff.js …`), so pass/fail is
|
|
171
|
+
reproducible and CI-gateable.
|
|
172
|
+
|
|
173
|
+
**What the numeric gate measures** (so a green `--visual` is trustworthy): geometry,
|
|
174
|
+
the text-glyph box via `Range`, the painted glyph + `background-position` for
|
|
175
|
+
icons/logos, the full box-model, font metrics **including `line-height`,
|
|
176
|
+
`letter-spacing`, `color`, `-webkit-font-smoothing`, the underline as a *box***
|
|
177
|
+
(thickness / width / offset, measured on whichever element — often an ancestor —
|
|
178
|
+
draws it), **and the painted *backdrop*** (`bg` — the bar/button/badge colour behind
|
|
179
|
+
a mark, so a wrong announcement-bar colour can't pass a green sweep). Perceived-weight and underline-geometry misses that used to slip a green
|
|
180
|
+
sweep now fail on the first run; `node tools/selftest.js` locks that in. What the gate
|
|
181
|
+
can't do — reproduce the *drawing technique* so a mark also rasterises identically —
|
|
182
|
+
stays with you (see `PLAYBOOK.md` Phase 6 and `LEARNINGS.md` "the gate vs your eyes").
|
|
183
|
+
|
|
184
|
+
## The two verification modes (this is the whole point)
|
|
185
|
+
|
|
186
|
+
1. **Full-page regression sweep** — a curated `pxTargets` list (logo, each nav item,
|
|
187
|
+
each icon…). Capture both pages, diff, require an all-green `--visual` table, then
|
|
188
|
+
**close coverage**: every painted element in the region must have a target.
|
|
189
|
+
```sh
|
|
190
|
+
node tools/pixel-diff.js live.json clone.json --visual # exit 0 = looks identical
|
|
191
|
+
node tools/pixel-diff.js live.json clone.json # strict: also flags structure
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
2. **Human-flagged drill-down** — someone says *"this looks wrong here."* Resolve
|
|
195
|
+
that one element and diff its **entire computed style** live-vs-clone, so **every**
|
|
196
|
+
real difference surfaces (a border, a shadow, a `text-decoration`, a background)
|
|
197
|
+
without adding a per-property rule. Paint differences first, structure demoted.
|
|
198
|
+
```sh
|
|
199
|
+
node tools/pixel-diff.js --inspect el_live.json el_clone.json # exit 0 = 0 paint diffs
|
|
200
|
+
```
|
|
201
|
+
This is the detection→**measure**→fix→verify loop. Detection is the human;
|
|
202
|
+
everything after is measured, and "fixed" means the paint bucket is empty.
|
|
203
|
+
|
|
204
|
+
## Quickstart
|
|
205
|
+
|
|
206
|
+
1. **Extract assets.** On the live site's DevTools console:
|
|
207
|
+
`extractFonts(/yourfont/i)` and `extractIcons('header')` → two zips → unpack into
|
|
208
|
+
your project's `assets/`. (See tool sections in `PLAYBOOK.md`.)
|
|
209
|
+
2. **Build your recreation** in whatever stack you like, driven by measurements — not
|
|
210
|
+
guesses. Load the real woff2s; use the real icon data-URIs; match the measured
|
|
211
|
+
type tokens.
|
|
212
|
+
3. **Verify.** Start your clone dev server + `node tools/sink.js`. Follow
|
|
213
|
+
`tools/RUNBOOK.md` to capture both pages at the **same viewport width** and diff.
|
|
214
|
+
Loop `--visual` until it exits 0 **and** coverage is empty.
|
|
215
|
+
4. **When a human spots something off**, run the `--inspect` loop on that element
|
|
216
|
+
until its paint bucket is empty.
|
|
217
|
+
|
|
218
|
+
Start with **PLAYBOOK.md** for the full method, and read **LEARNINGS.md** first —
|
|
219
|
+
every rule in it was paid for by a real miss.
|