luckiest-co 1.0.13 → 1.0.15

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.
Files changed (132) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/skills/luckiest-ab-testing/SKILL.md +1 -1
  4. package/skills/luckiest-ad-creative/SKILL.md +1 -1
  5. package/skills/luckiest-ads/SKILL.md +1 -1
  6. package/skills/luckiest-advisors/SKILL.md +1 -1
  7. package/skills/luckiest-aeo-grader/SKILL.md +1 -1
  8. package/skills/luckiest-ai-seo/SKILL.md +1 -1
  9. package/skills/luckiest-analytics/SKILL.md +1 -1
  10. package/skills/luckiest-aso/SKILL.md +1 -1
  11. package/skills/luckiest-churn-prevention/SKILL.md +1 -1
  12. package/skills/luckiest-co-marketing/SKILL.md +1 -1
  13. package/skills/luckiest-coder/SKILL.md +1 -1
  14. package/skills/luckiest-coder-brainstorming/SKILL.md +1 -1
  15. package/skills/luckiest-coder-consistency/SKILL.md +1 -1
  16. package/skills/luckiest-coder-constitution/SKILL.md +1 -1
  17. package/skills/luckiest-coder-debugging/SKILL.md +1 -1
  18. package/skills/luckiest-coder-dispatching-parallel-agents/SKILL.md +1 -1
  19. package/skills/luckiest-coder-executing-plans/SKILL.md +1 -1
  20. package/skills/luckiest-coder-finishing-a-branch/SKILL.md +1 -1
  21. package/skills/luckiest-coder-git-worktrees/SKILL.md +1 -1
  22. package/skills/luckiest-coder-guard/SKILL.md +1 -1
  23. package/skills/luckiest-coder-qa/SKILL.md +1 -1
  24. package/skills/luckiest-coder-receiving-code-review/SKILL.md +1 -1
  25. package/skills/luckiest-coder-requesting-code-review/SKILL.md +1 -1
  26. package/skills/luckiest-coder-shipping/SKILL.md +1 -1
  27. package/skills/luckiest-coder-subagent-driven-development/SKILL.md +1 -1
  28. package/skills/luckiest-coder-tdd/SKILL.md +1 -1
  29. package/skills/luckiest-coder-verification/SKILL.md +1 -1
  30. package/skills/luckiest-coder-writing-plans/SKILL.md +1 -1
  31. package/skills/luckiest-coder-writing-skills/SKILL.md +1 -1
  32. package/skills/luckiest-cold-email/SKILL.md +1 -1
  33. package/skills/luckiest-community-marketing/SKILL.md +1 -1
  34. package/skills/luckiest-competitor-profiling/SKILL.md +1 -1
  35. package/skills/luckiest-competitors/SKILL.md +1 -1
  36. package/skills/luckiest-content-strategy/SKILL.md +1 -1
  37. package/skills/luckiest-copy-editing/SKILL.md +1 -1
  38. package/skills/luckiest-copywriting/SKILL.md +1 -1
  39. package/skills/luckiest-cro/SKILL.md +1 -1
  40. package/skills/luckiest-customer-research/SKILL.md +1 -1
  41. package/skills/luckiest-directory-submissions/SKILL.md +1 -1
  42. package/skills/luckiest-emails/SKILL.md +1 -1
  43. package/skills/luckiest-extract-design-system/SKILL.md +1 -1
  44. package/skills/luckiest-free-tools/SKILL.md +1 -1
  45. package/skills/luckiest-image/SKILL.md +1 -1
  46. package/skills/luckiest-launch/SKILL.md +1 -1
  47. package/skills/luckiest-lead-magnets/SKILL.md +1 -1
  48. package/skills/luckiest-marketing-ideas/SKILL.md +1 -1
  49. package/skills/luckiest-marketing-plan/SKILL.md +1 -1
  50. package/skills/luckiest-marketing-psychology/SKILL.md +1 -1
  51. package/skills/luckiest-model-router/SKILL.md +1 -1
  52. package/skills/luckiest-offers/SKILL.md +1 -1
  53. package/skills/luckiest-onboarding/SKILL.md +1 -1
  54. package/skills/luckiest-paywalls/SKILL.md +1 -1
  55. package/skills/luckiest-popups/SKILL.md +1 -1
  56. package/skills/luckiest-pricing/SKILL.md +1 -1
  57. package/skills/luckiest-product-marketing/SKILL.md +1 -1
  58. package/skills/luckiest-programmatic-seo/SKILL.md +1 -1
  59. package/skills/luckiest-prompt-rewrite/SKILL.md +1 -1
  60. package/skills/luckiest-prospecting/SKILL.md +1 -1
  61. package/skills/luckiest-public-relations/SKILL.md +1 -1
  62. package/skills/luckiest-referrals/SKILL.md +1 -1
  63. package/skills/luckiest-revops/SKILL.md +1 -1
  64. package/skills/luckiest-sales-enablement/SKILL.md +1 -1
  65. package/skills/luckiest-schema/SKILL.md +1 -1
  66. package/skills/luckiest-seo-audit/SKILL.md +1 -1
  67. package/skills/luckiest-session-handoff/SKILL.md +1 -1
  68. package/skills/luckiest-signup/SKILL.md +1 -1
  69. package/skills/luckiest-site-architecture/SKILL.md +1 -1
  70. package/skills/luckiest-sms/SKILL.md +1 -1
  71. package/skills/luckiest-social/SKILL.md +1 -1
  72. package/skills/luckiest-thinker/SKILL.md +1 -1
  73. package/skills/luckiest-trends/SKILL.md +1 -1
  74. package/skills/luckiest-video/SKILL.md +1 -1
  75. package/skills/luckiest-website-cloner/ATTRIBUTION.md +29 -0
  76. package/skills/luckiest-website-cloner/CHANGELOG.md +8 -0
  77. package/skills/luckiest-website-cloner/LICENSE +21 -0
  78. package/skills/luckiest-website-cloner/SKILL.md +289 -0
  79. package/skills/luckiest-website-cloner/references/builtin-browser-recipes.md +74 -0
  80. package/skills/luckiest-website-cloner/references/compositing.md +65 -0
  81. package/skills/luckiest-website-cloner/references/fidelity-fast-path.md +92 -0
  82. package/skills/luckiest-website-cloner/references/playwright-cli-recipes.md +68 -0
  83. package/skills/luckiest-website-cloner/references/teardown.md +142 -0
  84. package/skills/luckiest-website-cloner/references/visual-qa.md +102 -0
  85. package/skills/luckiest-website-cloner/scripts/build-bundle.cjs +12 -0
  86. package/skills/luckiest-website-cloner/scripts/motion-probe.js +423 -0
  87. package/skills/luckiest-website-cloner/scripts/surface-map.js +392 -0
  88. package/skills/luckiest-website-cloner/scripts/tokens-probe.js +178 -0
  89. package/skills/luckiest-website-cloner-dom/ATTRIBUTION.md +4 -0
  90. package/skills/luckiest-website-cloner-dom/CHANGELOG.md +8 -0
  91. package/skills/luckiest-website-cloner-dom/LICENSE +21 -0
  92. package/skills/luckiest-website-cloner-dom/SKILL.md +170 -0
  93. package/skills/luckiest-website-cloner-dom/references/asset-resolution.md +93 -0
  94. package/skills/luckiest-website-cloner-dom/references/interaction-fingerprints.md +85 -0
  95. package/skills/luckiest-website-cloner-remix/ATTRIBUTION.md +4 -0
  96. package/skills/luckiest-website-cloner-remix/CHANGELOG.md +8 -0
  97. package/skills/luckiest-website-cloner-remix/LICENSE +21 -0
  98. package/skills/luckiest-website-cloner-remix/SKILL.md +202 -0
  99. package/skills/luckiest-website-cloner-remix/references/directions.md +82 -0
  100. package/skills/luckiest-website-cloner-remix/references/reskin.md +84 -0
  101. package/skills/luckiest-website-cloner-remix/references/tweak-panel.md +72 -0
  102. package/skills/luckiest-website-cloner-remix/scripts/apply-overrides.cjs +105 -0
  103. package/skills/luckiest-website-cloner-remix/scripts/gallery.cjs +71 -0
  104. package/skills/luckiest-website-cloner-remix/scripts/tokenize-css.cjs +127 -0
  105. package/skills/luckiest-website-cloner-remix/scripts/tweak-panel.js +169 -0
  106. package/skills/luckiest-website-cloner-shaders/ATTRIBUTION.md +11 -0
  107. package/skills/luckiest-website-cloner-shaders/CHANGELOG.md +8 -0
  108. package/skills/luckiest-website-cloner-shaders/LICENSE +21 -0
  109. package/skills/luckiest-website-cloner-shaders/SKILL.md +129 -0
  110. package/skills/luckiest-website-cloner-shaders/references/capture-backends.md +89 -0
  111. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/SKILL.md +123 -0
  112. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/capture-backends.md +201 -0
  113. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/evidence-policy.md +93 -0
  114. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/operating-contract.md +82 -0
  115. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/qa-failure-policy.md +100 -0
  116. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/recon-kernel.md +206 -0
  117. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/replay-policy.md +145 -0
  118. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/shaders-com.md +212 -0
  119. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/source-analysis.md +112 -0
  120. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/surface-discovery.md +113 -0
  121. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/target-lock.md +205 -0
  122. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/three-shader-reconstruction.md +155 -0
  123. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/tool-capability-matrix.md +57 -0
  124. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/unicorn-studio.md +387 -0
  125. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/scripts/fetch-rendered-dom.mjs +178 -0
  126. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/scripts/scan-bundle.sh +80 -0
  127. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/extraction-report.md +41 -0
  128. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/known-gaps.md +14 -0
  129. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/qa-report.md +74 -0
  130. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/replay-manifest.json +147 -0
  131. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/run-state.json +75 -0
  132. package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/scout-card.json +106 -0
@@ -0,0 +1,74 @@
1
+ # Built-in browser recipes (no playwright-cli)
2
+
3
+ Use these when the only browser you can drive is the in-app browser pane
4
+ (`navigate`, `javascript_tool`, `computer`, `get_page_text`, `read_page`). This
5
+ path has **no preload**, so `instrumentGetContext()` and `instrumentMotion()`
6
+ cannot run before the page's own scripts. Everything here is post-hoc. That is
7
+ still reliable for the common case; it is weaker for canvases that request their
8
+ context once at startup and for motion that only fires on first paint.
9
+
10
+ ## Inject the probe bundle
11
+
12
+ Build it once, then paste it in. `javascript_tool` cannot read local files, so
13
+ inline the bundle's text rather than pointing at a path.
14
+
15
+ ```bash
16
+ node scripts/build-bundle.cjs /tmp/probes.bundle.js
17
+ ```
18
+
19
+ Then evaluate the file's contents in the page, followed by the probe call. Do it
20
+ in two steps so a syntax error in the bundle is easy to tell apart from a probe
21
+ error.
22
+
23
+ ## Settle the page before probing
24
+
25
+ ```js
26
+ (async () => {
27
+ const h = document.documentElement.scrollHeight;
28
+ for (let y = 0; y < h; y += 700) { window.scrollTo(0, y); await new Promise(r => setTimeout(r, 120)); }
29
+ window.scrollTo(0, 0);
30
+ await new Promise(r => setTimeout(r, 800));
31
+ return h;
32
+ })()
33
+ ```
34
+
35
+ ## Probe and write out
36
+
37
+ `javascript_tool` returns its last expression as JSON, and large probe output
38
+ will blow up the conversation. Take `motionSummary()` inline for a first look,
39
+ and for the full `motionProbe()` / `tokensProbe()` output return it in chunks
40
+ and write each chunk to the output directory from the shell, or keep only the
41
+ fields you need:
42
+
43
+ ```js
44
+ JSON.stringify(surfaceMap()) // small, safe inline
45
+ JSON.stringify(motionSummary()) // small, safe inline
46
+ JSON.stringify(tokensProbe()).length // check size BEFORE returning the body
47
+ ```
48
+
49
+ ## Expect `instrumented: false`
50
+
51
+ `motionProbe().instrumented` will be false on this path. That is expected, not a
52
+ failure. Record it in the surface map so the teardown does not claim ground
53
+ truth it does not have, and treat canvas routing as a hypothesis to confirm with
54
+ a screenshot.
55
+
56
+ ## Screenshots and viewports
57
+
58
+ Use `resize_window` for the 1440 / 768 / 390 passes and `computer` with
59
+ `action: "screenshot"` for section references. Reload after a viewport change so
60
+ load-time breakpoints re-run.
61
+
62
+ ## Assets
63
+
64
+ ```js
65
+ JSON.stringify(performance.getEntriesByType('resource').map(e => e.name))
66
+ ```
67
+
68
+ ## Viewport reads can come back zero
69
+
70
+ In a hidden or background pane, `window.innerWidth` / `innerHeight` can report
71
+ `0` even though the page laid out correctly and every `getBoundingClientRect()`
72
+ is real. Do not treat a zero viewport as a failed probe, and do not record it as
73
+ the breakpoint you measured at. Front the tab, or take the width you asked
74
+ `resize_window` for as the truth.
@@ -0,0 +1,65 @@
1
+ # Compositing GPU effects into the DOM shell
2
+
3
+ The DOM track leaves a **placeholder mount** wherever a GPU surface belonged.
4
+ The GPU track produces a **self-contained effect module**. Compositing is the
5
+ contract that joins them. Most "the clone looks off" bugs are composite bugs, not
6
+ extraction bugs: the effect renders perfectly in its own baseline, then sits at
7
+ the wrong depth, swallows clicks, or renders blurry once it's in the page.
8
+
9
+ ## The mount contract
10
+
11
+ Each effect module should expose a tiny, framework-agnostic lifecycle so any
12
+ shell (Vite, Next, plain HTML) can drive it the same way:
13
+
14
+ ```js
15
+ // effect module (per GPU surface)
16
+ export function mount(canvasEl, opts) { /* start renderer + rAF loop */ return handle }
17
+ export function unmount(handle) { /* cancel rAF, lose context, free GPU */ }
18
+ export function resize(handle, w, h, dpr) { /* update drawing buffer + viewport */ }
19
+ ```
20
+
21
+ The DOM shell mounts it into the placeholder container from the surface map:
22
+
23
+ ```js
24
+ const el = document.querySelector(MOUNT_SELECTOR);
25
+ const canvas = document.createElement('canvas');
26
+ el.appendChild(canvas);
27
+ const handle = mount(canvas, { /* uniforms captured by luckiest-website-cloner-shaders */ });
28
+ ```
29
+
30
+ ## Get these five things right (they are the usual failure points)
31
+
32
+ 1. **Sizing + DPR.** The drawing buffer must be `cssWidth * dpr` by
33
+ `cssHeight * dpr`, with the canvas CSS-sized to the container. Skipping DPR is
34
+ the #1 "why is the clone blurry / aliased" cause on retina displays. Re-run
35
+ `resize()` on container resize (ResizeObserver), not just window resize.
36
+ 2. **z-index + stacking.** Copy `zIndex` and `position` from `surface-map.json`.
37
+ A hero shader usually sits at a low z-index BEHIND the text, not on top. If it
38
+ covers the copy, the composite is wrong even though the effect is right.
39
+ 3. **pointer-events.** Copy the original's `pointerEvents`. A full-bleed
40
+ background canvas almost always needs `pointer-events: none` so buttons and
41
+ links under/over it stay clickable. An *interactive* effect (cursor-reactive
42
+ blob) needs pointer events ON — the surface map records which.
43
+ 4. **Teardown.** On route change / unmount, call `unmount()` and let the WebGL
44
+ context go (`loseContext()`). Leaking contexts hits the browser's ~16-context
45
+ limit fast on multi-effect pages and the later canvases silently go black.
46
+ 5. **Reduced motion + offscreen pause.** Respect
47
+ `prefers-reduced-motion: reduce` (render a static first frame or the captured
48
+ poster). Pause the rAF loop when the canvas is scrolled out of view
49
+ (IntersectionObserver) — the original may not, but it's cheap fidelity-neutral
50
+ insurance against jank, and matches how good sites ship these.
51
+
52
+ ## Multiple effects on one page
53
+
54
+ Mount each into its own container from the surface map. Share a single renderer
55
+ only if all effects came from the same driver and baseline; otherwise keep them
56
+ independent so one failing effect can't black out the others. Watch the context
57
+ budget (point 4).
58
+
59
+ ## When the effect can't be reconstructed
60
+
61
+ If luckiest-website-cloner-shaders returned only an approximate or poster-only result (e.g.
62
+ cross-origin readback was blocked, or the surface was `CANVAS_UNKNOWN`), mount
63
+ its fallback (captured poster image or a low-fidelity approximation) and record
64
+ the gap in the completion report. Do not silently ship a dead container — a
65
+ poster still reads as intentional; an empty box reads as broken.
@@ -0,0 +1,92 @@
1
+ # The fidelity fast path — reuse the original's CSS instead of rebuilding it
2
+
3
+ Validated on a real clone run (Nuxt marketing site): this path reached a
4
+ **0.02–0.06% pixel diff** on the DOM track in one pass, where rebuild-from-spec
5
+ typically lands at 1–3% after several fix rounds. Prefer it whenever it applies.
6
+
7
+ ## When it applies
8
+
9
+ The target ships **scope-attributed CSS** — Vue scoped styles (`data-v-*`),
10
+ CSS Modules (hashed class names), styled-components (hashed classes), Angular
11
+ (`_ngcontent-*`). The attributes/classes are baked into the rendered DOM, so:
12
+
13
+ > post-hydration HTML + the site's own stylesheets + rewritten asset paths
14
+ > = pixel-identical DOM layer, byte for byte.
15
+
16
+ It does NOT apply when the goal is a *maintainable* codebase to keep developing
17
+ (then use luckiest-website-cloner-dom's component rebuild), or when the user asked for a specific
18
+ stack. Say which path you chose and why in the report.
19
+
20
+ ## The recipe
21
+
22
+ 1. **Capture post-hydration HTML** after a full settle scroll:
23
+ `document.documentElement.outerHTML` via Playwright. Not view-source — the
24
+ served HTML lacks client-rendered state.
25
+ 2. **Download every stylesheet** in document order (`<link rel=stylesheet>`
26
+ hrefs). Concatenate in that order — cascade order is load order.
27
+ 3. **Rewrite paths** in both HTML and CSS: absolute site URLs → local, CDN image
28
+ URLs (strip query params, keep a deterministic filename), `/fonts/`,
29
+ `url(...)` in CSS. Grep the result for `https?://` afterward — remaining
30
+ hits should only be outbound links, or you missed an asset.
31
+ 4. **Strip the runtime**: remove `<script>` tags and preload/prefetch links.
32
+ Keep everything else, including framework scope attributes and inline
33
+ `style=""` — they're part of the captured state.
34
+ 5. **Capture state you can't see at rest.** SPA frameworks render only the
35
+ ACTIVE state into the DOM — inactive tab panels, closed dropdowns, unfired
36
+ dialogs simply don't exist in the capture. For each stateful widget, drive
37
+ the live site (click each tab/accordion/menu) and save each state's
38
+ innerHTML; replay those snapshots from your own JS. The interaction sweep's
39
+ click step is where you find these.
40
+ 6. **Beware capture-time inline state**: elements the site's JS was mid-
41
+ animating carry inline `opacity`/`transform` from the moment of capture
42
+ (e.g. a bar captured at `opacity: 0`). Your rewritten behavior JS must own
43
+ those elements' initial state explicitly, not trust the captured inline value.
44
+ 7. **Rebuild only behaviors** (one small JS entry): smooth scroll, canvas/GPU
45
+ mounts, state machines, toggles — from `motion.json` params. CSS-driven
46
+ effects (scroll-timelines, keyframes, transitions) came along with the
47
+ stylesheets for free — do not reimplement them.
48
+
49
+ ## Trigger-position drift (bit us; will bite again)
50
+
51
+ Scroll-triggered behaviors computed at load go stale when lazy images/fonts
52
+ shift layout seconds later — symptom: effects fire at the wrong scroll position
53
+ only on some breakpoints. Standard fix, include it by default:
54
+
55
+ ```js
56
+ let lastH = 0, t = 0;
57
+ new ResizeObserver(() => {
58
+ const h = document.documentElement.scrollHeight;
59
+ if (h === lastH) return; lastH = h;
60
+ clearTimeout(t); t = setTimeout(() => ScrollTrigger.refresh(), 120);
61
+ }).observe(document.body);
62
+ ```
63
+
64
+ ## Matching state machines: measure thresholds, don't guess them
65
+
66
+ For each observed state flip (header theme, content swap, element fade), sample
67
+ the original at a FINE grain around the transition (±50px steps) and record the
68
+ exact trigger geometry before implementing. Two traps proven real:
69
+ - a flip can be **binary** where you'd assume scrubbed (and vice versa) — the
70
+ fine-grained samples tell you which;
71
+ - the trigger element is often NOT the section that changes (a header flips on
72
+ the NEXT section's top edge, at an offset unrelated to the header's height).
73
+ Also check for CSS-variable drivers before assuming classes: if a computed style
74
+ uses `color-mix(... var(--x-progress) ...)`, the JS writes that var — toggling
75
+ the class alone changes nothing visible.
76
+
77
+ ## When the GPU surface is a known library, reuse it (SOURCE fidelity)
78
+
79
+ Before routing a canvas to luckiest-website-cloner-shaders, grep the bundles for library
80
+ fingerprints: `data-paper-shader` / `@paper-design` (Paper Shaders),
81
+ `unicorn.studio`, `spline-viewer`, `rive`, `lottie`, `r3f`/`@react-three`,
82
+ `ogl`. If the effect is an off-the-shelf component, the bundle usually carries
83
+ its **props verbatim** (Next.js app-router chunks are barely minified — read the
84
+ page chunk, it IS the source). Install the same package and pass the same props:
85
+ that's SOURCE fidelity for free, no capture/replay needed. Also copy the site's
86
+ **render gate** (reduced-motion / pointer / width / cores / GPU-renderer
87
+ checks) — it decides when the fallback shows, and the fallback is part of the
88
+ look. Add a `?shaders=1|0` override so QA can exercise both paths.
89
+
90
+ Mount-point rule: when the original renders the effect wrapper *conditionally*,
91
+ your mount element must be visually inert (no background, no filter) — put the
92
+ wrapper styling on the mounted component, or the fallback state shows a blank box.
@@ -0,0 +1,68 @@
1
+ # playwright-cli recipes for this skill
2
+
3
+ `playwright-cli` is a CLI over a persistent browser session. Each invocation is
4
+ a separate process, which shapes how you must use it here.
5
+
6
+ ## Running the probe scripts
7
+
8
+ ```bash
9
+ # inject the probe bundle into the LIVE page, then evaluate
10
+ playwright-cli -s=<name> run-code "async page => { await page.addScriptTag({ path: '<abs>/probes.bundle.js' }); return page.evaluate(() => motionSummary()); }"
11
+ ```
12
+ Build `probes.bundle.js` with `node scripts/build-bundle.cjs [dest]` — it concatenates
13
+ the three probe files AND exports them onto `window`. That export is mandatory:
14
+ Playwright wraps `addInitScript`/`addScriptTag` code in a function scope, so
15
+ bare `function` declarations are invisible to later `page.evaluate` calls
16
+ (symptom: `motionSummary is not defined` even though the script ran). `run-code` has **no `fs`** — return data and redirect
17
+ stdout, or use `eval` and parse the `### Result` block:
18
+
19
+ ```bash
20
+ playwright-cli -s=<name> eval "JSON.stringify(tokensProbe())" > raw.txt
21
+ # then unwrap: the JSON is the quoted string after the '### Result' line
22
+ ```
23
+ `eval` treats an expression starting with `(` as a function — wrap IIFEs in
24
+ `run-code` + `page.evaluate` instead.
25
+
26
+ ## Instrumented (preload) mode — do it in ONE run-code call
27
+
28
+ `page.addInitScript()` registered in one `run-code` invocation does NOT
29
+ survive into the next (the CLI's `page` binding is re-created). Do init +
30
+ navigate + settle + probe in a single call:
31
+
32
+ ```bash
33
+ playwright-cli -s=<name> run-code "async page => {
34
+ await page.addInitScript({ path: '<abs>/probes.bundle.js' });
35
+ await page.addInitScript(() => { instrumentGetContext(); instrumentMotion(); });
36
+ await page.goto('<url>', { waitUntil: 'domcontentloaded' });
37
+ await page.waitForTimeout(2500);
38
+ const h = await page.evaluate(() => document.documentElement.scrollHeight);
39
+ for (let y = 0; y < h; y += 700) { await page.mouse.wheel(0, 700); await page.waitForTimeout(120); }
40
+ await page.evaluate(() => window.scrollTo(0, 0)); await page.waitForTimeout(800);
41
+ return page.evaluate(() => ({ sm: surfaceMap(), mo: motionProbe() }));
42
+ }" > raw-instrumented.txt
43
+ ```
44
+ Verify `mo.instrumented === true`; if false the preload didn't take.
45
+
46
+ ## Capturing hidden SPA state
47
+
48
+ ```bash
49
+ playwright-cli -s=<name> run-code "async page => { const tabs = page.locator('.tabs .tab'); const out = []; for (let i = 0; i < await tabs.count(); i++) { await tabs.nth(i).click(); await page.waitForTimeout(800); out.push(await page.evaluate(() => document.querySelector('.tabs-content').innerHTML)); } return out; }"
50
+ ```
51
+
52
+ ## Asset list
53
+
54
+ `playwright-cli network` only logs from the moment it's first called. For the
55
+ full list use the page's own record:
56
+ `page.evaluate(() => performance.getEntriesByType('resource').map(e => e.name))`.
57
+
58
+ ## Screenshots
59
+
60
+ `page.screenshot({ path })` inside `run-code` works and is the reliable way to
61
+ get section references regardless of which browser tool is fronted.
62
+
63
+ ## Headless GPU note
64
+
65
+ Headless Chromium on a desktop with a real GPU often exposes a real GPU renderer string, so sites
66
+ that gate shaders on `WEBGL_debug_renderer_info` DO render them in QA — mask
67
+ `[data-paper-shader]`, `canvas`, and any wrapper the library injects. Don't
68
+ assume headless = software GL.
@@ -0,0 +1,142 @@
1
+ # TEARDOWN.md — the human-readable blueprint
2
+
3
+ `surface-map.json`, `motion.json`, and `tokens-*.json` are machine evidence.
4
+ `TEARDOWN.md` is the same evidence written for a person: what the site is made
5
+ of, how every effect actually works, and what it would take to rebuild it. It is
6
+ the deliverable of `--analyze-only` runs, the briefing the builders work from on
7
+ full runs, and — because the reveals are usually "that impressive thing is 54
8
+ images swapped on mousemove" — the raw material for a video.
9
+
10
+ Write it at the end of Phase 1, from the probe outputs + your scroll/hover sweep.
11
+ Every claim carries an evidence tag:
12
+
13
+ - **CONFIRMED** — read from a live runtime object or computed style
14
+ (`ScrollTrigger.getAll()`, `getComputedStyle`, `document.getAnimations()`).
15
+ - **OBSERVED** — seen during the sweep (screenshots at scroll offsets, hover
16
+ before/after) but the mechanism wasn't exposed by a probe.
17
+ - **INFERRED** — guessed from class names / markup / general patterns. The
18
+ builder should verify before relying on it.
19
+
20
+ Never upgrade a tag. A clone built on INFERRED values needs a diff pass to prove
21
+ them; one built on CONFIRMED values mostly needs a diff pass to catch typos.
22
+
23
+ ## Template
24
+
25
+ ```markdown
26
+ # Teardown: {site name}
27
+
28
+ **URL:** {url} **Analyzed:** {YYYY-MM-DD} @ {1440/768/390}
29
+ **Platform:** {Next.js / Webflow / Framer / WordPress / custom} — {evidence}
30
+ **Built by:** {agency/dev, if a footer/meta credit exists; else omit}
31
+ **Surfaces:** {N} DOM sections, {N} GPU surfaces ({drivers}), {N} video
32
+
33
+ ## Stack (from runtime)
34
+
35
+ | Layer | What | Evidence |
36
+ |---|---|---|
37
+ | Framework | {…} | `__NEXT_DATA__` present / `data-framer-name` / … |
38
+ | Animation | GSAP {ver} + ScrollTrigger ({n} triggers, {n} pinned, scrub on {n}) | `motion.json › gsap, scrollTrigger` |
39
+ | Scroll | Lenis lerp {x} duration {y} | `motion.json › lenis.options` |
40
+ | 3D / GPU | {three.js r{ver} / Unicorn Studio / none} | `surface-map.json` |
41
+ | Sliders | {Swiper: loop, autoplay 4000, effect fade} | `motion.json › sliders` |
42
+ | Fonts | {Family A (display), Family B (text)} — self-hosted woff2 / Google | `tokens.fontFaces` |
43
+
44
+ ## Design system (measured)
45
+
46
+ **Palette** — top values by usage, role-labeled:
47
+ | Role | Value | Uses |
48
+ |---|---|---|
49
+ | Page bg | {#…} | {n} |
50
+ | Text | {#…} | {n} |
51
+ | Accent | {#…} | {n} |
52
+
53
+ **Type scale @1440** (size / line-height / weight — family):
54
+ | Role | Value | Example |
55
+ |---|---|---|
56
+ | h1 | {…} | `.hero__title` |
57
+ | h2 | {…} | |
58
+ | body | {…} | |
59
+ | label/eyebrow | {…, letter-spacing, uppercase} | |
60
+ {Note what changes @768 and @390 — usually 2–4 lines.}
61
+
62
+ **Spacing rhythm** — {base unit + the 5–8 values that carry the layout}.
63
+ **Radii** {…} · **Shadows** {…} · **Max-width** {…} · **Breakpoints** {…}
64
+ **Custom props** — {if the site exposes its own tokens, list the important ones;
65
+ reuse their names in the clone}.
66
+
67
+ ## Effects (one row per distinct effect, top-to-bottom)
68
+
69
+ | # | Section | Effect | Mechanism | Tag | Complexity |
70
+ |---|---|---|---|---|---|
71
+ | 1 | Hero | Title chars rise + fade in on load | SplitText → stagger 0.03, y 100%→0, power3.out, 1.2s | CONFIRMED | Low |
72
+ | 2 | Hero | Background liquid gradient | WebGL2 fragment shader, three.js, 2 uniforms (time, mouse) | CONFIRMED (surface) / PARTIAL (replay) | High |
73
+ | 3 | Features | Sticky mock swaps as copy scrolls | `position:sticky` + IntersectionObserver threshold 0.5 | CONFIRMED (IO instrumented) | Med |
74
+ | … | | | | | |
75
+
76
+ ## Reveals — how the impressive ones actually work
77
+
78
+ ### {Effect #} — {name}
79
+ **What it looks like:** {one sentence}
80
+ **What it is:** {the mechanism in plain words — the "oh, that's all it is"}
81
+ **Exact params (CONFIRMED):**
82
+ ```
83
+ {paste the relevant motion.json entry: trigger, start/end, scrub, vars…}
84
+ ```
85
+ **Rebuild note:** {the one thing that makes it feel right — easing, the lerp
86
+ value, the DPR handling, the fact that it's 3 layers not 1}
87
+
88
+ {Repeat for every effect rated Med/High, and any Low that's load-bearing.}
89
+
90
+ ## Assets
91
+
92
+ | Asset | Role | Tier | Notes |
93
+ |---|---|---|---|
94
+ | hero-bg.mp4 | decorative bg | REAL | 1920×1080, 4.2 MB |
95
+ | logo.svg | identity | PLACEHOLDER | don't copy brand |
96
+ | noise.png | texture | RECONSTRUCT | generate with canvas |
97
+ {Tiers from `luckiest-website-cloner-dom/references/asset-resolution.md`.}
98
+
99
+ ## Build plan
100
+
101
+ **Substrate:** {Vite+TS / Next+Tailwind} — {why, per Phase 2 rules}
102
+ **Packages:** `npm i {gsap lenis three …}`
103
+ **Order:** {section list top-to-bottom, each with: interaction model, which
104
+ effects from the table, expected difficulty}
105
+ **Known gaps:** {cross-origin stylesheets that hid @font-face, un-instrumented
106
+ IO, surfaces flagged CANVAS_UNKNOWN, premium plugins (SplitText/ScrollSmoother
107
+ need Club GSAP → use free alternatives or the now-free GSAP 3.13+)}
108
+ ```
109
+
110
+ ## Common reveals (look for these first)
111
+
112
+ Most "how did they do that" effects on award sites are one of these. When the
113
+ probe shows the signature, name it:
114
+
115
+ - **Image sequence on scroll/mouse** — N preloaded frames, one visible. Signature:
116
+ many sibling `<img>`/canvas draws + a scrub ScrollTrigger or mousemove listener.
117
+ - **SplitText reveal** — `.char/.word/.line` wrappers + staggered tween.
118
+ - **Parallax layers** — several tweens on siblings with different `y`/`yPercent`
119
+ and the same trigger, or mousemove → `transform` with different multipliers.
120
+ - **Scrub animation** — `ScrollTrigger.scrub: true|number`. Number = lerp delay.
121
+ - **Pinned section** — `pin: true` with a long `end` (`+=200%`); inside it,
122
+ content swaps by progress.
123
+ - **CSS-var driven** — JS writes `--progress`; CSS `calc()`s off it. Look for
124
+ custom props that change as you scroll.
125
+ - **Smooth scroll** — Lenis on `<html class="lenis">`. Feel comes from `lerp`.
126
+ - **Page transitions** — barba / Swup global + an overlay element with z-index
127
+ 9999.
128
+ - **Grain** — a fixed full-viewport div, noise PNG/SVG `feTurbulence`,
129
+ `mix-blend-mode: overlay|soft-light`, low opacity, sometimes animated
130
+ `background-position`.
131
+ - **Custom cursor** — fixed small element + mousemove + `gsap.quickTo` /
132
+ `quickSetter`; often two elements (dot + ring) with different lerps.
133
+ - **Marquee** — `@keyframes` translateX −50% on a duplicated track, or GSAP
134
+ `repeat:-1` with `xPercent`.
135
+ - **Magnetic button** — mousemove inside a hit area → `x/y` tween toward cursor,
136
+ `elastic.out` on leave.
137
+ - **Reveal on scroll** — IO (threshold 0.1–0.3) toggles a class; CSS transition
138
+ does the motion. Or ScrollTrigger `toggleActions: "play none none reverse"`.
139
+ - **Theme flip by section** — IO or ScrollTrigger toggling a class on `<body>`;
140
+ CSS vars swap; 0.4–0.8s transition on background/color.
141
+ - **Text scramble / counter** — rAF loop writing `textContent`; not an animation
142
+ the probe can see — OBSERVED, note the duration.
@@ -0,0 +1,102 @@
1
+ # Automated visual QA
2
+
3
+ Replace "look at a screenshot and call it done" with a real pixel diff. The catch
4
+ is that a cloned page is two materials with opposite QA needs: DOM must match
5
+ closely and deterministically; GPU effects animate and will never match pixel-
6
+ for-pixel. So diff DOM, and **verify** GPU — don't diff GPU.
7
+
8
+ ## Harness
9
+
10
+ Use Playwright's built-in visual comparison (`toHaveScreenshot()` / `expect(page)`
11
+ compares via the pixelmatch library — pixel-by-pixel, local, no external
12
+ service). Capture original and clone at the same viewport, DPR, and scroll state.
13
+
14
+ ```js
15
+ // qa/clone.spec.ts — run per breakpoint
16
+ import { test, expect } from '@playwright/test';
17
+
18
+ const BREAKPOINTS = [1440, 768, 390];
19
+ // GPU surface selectors come straight from surface-map.json.routingSummary.sendToShaderExtract
20
+ const GPU_MASKS = ['#hero-canvas', '.bg-shader'];
21
+
22
+ for (const width of BREAKPOINTS) {
23
+ test(`clone matches original @ ${width}`, async ({ page }) => {
24
+ await page.setViewportSize({ width, height: 900 });
25
+ await page.goto(CLONE_URL);
26
+ await page.waitForLoadState('networkidle');
27
+ await expect(page).toHaveScreenshot(`clone-${width}.png`, {
28
+ fullPage: true,
29
+ mask: GPU_MASKS.map((s) => page.locator(s)), // exclude animated GPU regions
30
+ maxDiffPixelRatio: 0.02, // ~2% tolerance absorbs anti-aliasing noise
31
+ threshold: 0.2, // per-pixel color sensitivity (default; lower = stricter)
32
+ animations: 'disabled', // freeze CSS animations/transitions for a stable frame
33
+ });
34
+ });
35
+ }
36
+ ```
37
+
38
+ Generate the baseline from the **original** site (first run writes the reference
39
+ screenshot), then run the **clone** against it. Or capture both to files and diff
40
+ with a standalone pixelmatch script if you want the diff image saved as an
41
+ artifact for the completion report.
42
+
43
+ ## Why the knobs, not zero-diff
44
+
45
+ Pixel diffing is literal: it can't tell a real layout regression from a harmless
46
+ anti-aliasing or subpixel-font difference, which is the main source of flaky
47
+ "failures." So:
48
+
49
+ - **`threshold`** (0–1, default 0.2) — per-pixel color sensitivity. Leave near
50
+ default; drop only when chasing a genuine color mismatch.
51
+ - **`maxDiffPixelRatio`** — fraction of pixels allowed to differ. This is your
52
+ main dial. Start ~0.02 and tighten as the clone converges. A whole section
53
+ shifted will blow way past it; font AA won't.
54
+ - **`mask`** — the GPU regions. Non-negotiable: without it, a single animating
55
+ shader frame makes every run "fail" and hides the DOM regressions you care about.
56
+ - **`animations: 'disabled'`** — freezes CSS transitions so the DOM frame is
57
+ deterministic. (It does not stop a canvas rAF loop — that's why GPU regions are
58
+ masked, not just frozen.)
59
+
60
+ ## Verifying the GPU regions (separately)
61
+
62
+ Masked out of the diff, so check them by behavior, not by pixel equality:
63
+
64
+ 1. **Renders at all** — the canvas is non-blank after load (sample a few pixels;
65
+ if cross-origin blocks readback, verify visually via screenshot).
66
+ 2. **Animates** — frames change over ~500ms (`isCanvasAnimating()` in
67
+ `scripts/surface-map.js`).
68
+ 3. **Responds** — if the original reacts to pointer/scroll, the clone does too.
69
+ 4. **Fidelity label** — carry luckiest-website-cloner-shaders's SOURCE / PARTIAL / approximate
70
+ verdict into the report. A masked region that "passes" the DOM diff has NOT
71
+ been proven faithful — only excluded. Say so honestly.
72
+
73
+ ## Reporting
74
+
75
+ For each breakpoint: diff pass/fail, `maxDiffPixelRatio` achieved, and a link to
76
+ the saved diff image. For each GPU surface: renders / animates / responds + the
77
+ fidelity label. This is the evidence the clone is done — not a vibe.
78
+
79
+ ## Harness gotchas (learned the hard way)
80
+
81
+ - **Don't set `reducedMotion: 'reduce'`** in the QA context. It changes the
82
+ ORIGINAL's JS behavior (sites branch on it), so heights and content diverge
83
+ and you chase phantom diffs. Freeze motion with injected CSS instead
84
+ (`animation-play-state: paused; transition: none`).
85
+ - **`waitUntil: 'networkidle'` hangs against a Vite dev server** (HMR socket).
86
+ Use `domcontentloaded` + a fixed settle, then a full scroll-through so lazy
87
+ content loads before you measure.
88
+ - **Measure both pages the same way.** Section heights differ between a fresh
89
+ load and a session that already scrolled (lazy images, fonts). Always scroll
90
+ top→bottom→top on both before comparing offsets, and compare fresh-load to
91
+ fresh-load.
92
+ - **First check heights, then pixels.** Dump `[selector, top, height]` for every
93
+ section on both pages; a 0 px delta down the whole list means the DOM track is
94
+ right and any remaining diff is behavior/timing. A delta localizes the bug to
95
+ one section instantly.
96
+ - **Per-viewport slices beat one fullPage shot.** Screenshot every 900 px of
97
+ scroll and diff slice-by-slice; the report then says *where* it's wrong
98
+ (`y=19800 16%`) instead of one useless global number. Fixed chrome (header)
99
+ shows in every slice — a header bug looks like "every slice over section X
100
+ fails", which is itself the diagnosis.
101
+ - Mask GPU canvases **and** anything time-driven (marquees, morphing counters)
102
+ — they're verified separately, not diffed.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ // Concatenate the probes into one injectable file and EXPORT them onto window.
3
+ // Playwright's addInitScript / addScriptTag wrap code in a function scope, so
4
+ // bare `function` declarations are NOT globals — the explicit export is required.
5
+ const fs = require('fs'), path = require('path');
6
+ const strip = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '').replace(/^\s*[\r\n]/gm, '');
7
+ const files = ['surface-map.js', 'motion-probe.js', 'tokens-probe.js'];
8
+ let out = files.map((f) => strip(fs.readFileSync(path.join(__dirname, f), 'utf8'))).join('\n');
9
+ out += '\n;(function(){ const g = typeof window !== "undefined" ? window : globalThis; Object.assign(g, { instrumentGetContext, surfaceMap, isCanvasAnimating, instrumentMotion, motionProbe, motionSummary, tokensProbe }); })();\n';
10
+ const dest = process.argv[2] || path.join(__dirname, 'probes.bundle.js');
11
+ fs.writeFileSync(dest, out);
12
+ console.log(dest, out.length, 'bytes');