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,392 @@
1
+ /*
2
+ * surface-map.js — the recon router for /luckiest-website-cloner.
3
+ *
4
+ * Purpose: decide, for one loaded page, WHICH regions are plain DOM/CSS and
5
+ * which are GPU/canvas-rendered, so the coordinator can send each region to the
6
+ * right extractor. A DOM extractor reading getComputedStyle() is blind to
7
+ * anything a <canvas> paints — those pixels live on the GPU, not in the DOM.
8
+ * Misrouting a WebGL hero to the DOM track produces an empty div where the
9
+ * effect should be. This script is the seam that prevents that.
10
+ *
11
+ * Two ways to run it, best first:
12
+ *
13
+ * 1. INSTRUMENTED (most reliable). Inject instrumentGetContext() as a preload
14
+ * script BEFORE the page's own scripts run (browser MCP "evaluate on new
15
+ * document", Playwright page.addInitScript, or a Chrome MCP preload). It
16
+ * records the context type each canvas actually requested. Then, after the
17
+ * page settles, run surfaceMap() to read those tags. This is the ground
18
+ * truth because it sees the real getContext() call the site made.
19
+ *
20
+ * 2. POST-HOC (no preload available). Just run surfaceMap() on the loaded
21
+ * page. It probes getContext() live. This is reliable for the common case
22
+ * (see note in classifyCanvas) but can mutate an as-yet-uninitialized
23
+ * canvas, so prefer mode 1 when the tooling supports preload.
24
+ *
25
+ * Both return the same JSON shape. Keep the output in the run's output dir, not
26
+ * in the conversation — it can be large on canvas-heavy pages.
27
+ */
28
+
29
+ /* ---- Mode 1 helper: inject this BEFORE page scripts run ---- */
30
+ function instrumentGetContext() {
31
+ const proto = HTMLCanvasElement.prototype;
32
+ const orig = proto.getContext;
33
+ proto.getContext = function (type, ...args) {
34
+ try {
35
+ // First requested type wins as the canvas's "real" type; record it once.
36
+ if (!this.__requestedContextType) this.__requestedContextType = type;
37
+ } catch (_) {}
38
+ return orig.call(this, type, ...args);
39
+ };
40
+ // Also flag OffscreenCanvas transfers, which hide the real drawing surface.
41
+ try {
42
+ const to = proto.transferControlToOffscreen;
43
+ if (to) {
44
+ proto.transferControlToOffscreen = function (...a) {
45
+ this.__offscreen = true;
46
+ return to.apply(this, a);
47
+ };
48
+ }
49
+ } catch (_) {}
50
+ }
51
+
52
+ /* ---- The map builder: run AFTER the page has settled ---- */
53
+ function surfaceMap() {
54
+ const W = window, D = document;
55
+ const vw = W.innerWidth, vh = W.innerHeight;
56
+
57
+ // Framework + platform globals. Presence is a hypothesis about HOW a canvas
58
+ // is driven — it does not by itself prove a given canvas is GPU-backed, so it
59
+ // annotates surfaces rather than deciding them.
60
+ const globals = {
61
+ three: !!W.THREE,
62
+ babylon: !!W.BABYLON,
63
+ regl: !!W.regl,
64
+ pixi: !!W.PIXI,
65
+ ogl: !!W.OGL || !!W.ogl,
66
+ p5: !!W.p5,
67
+ gsap: !!W.gsap || !!W.TweenMax,
68
+ // Platform effect tools that ship shader backgrounds:
69
+ unicornStudio:
70
+ !!W.UnicornStudio ||
71
+ !!D.querySelector('[data-us-project],[data-us-project-src],script[src*="unicorn" i]'),
72
+ spline: !!D.querySelector('spline-viewer,script[src*="spline" i]'),
73
+ rive: !!W.rive || !!D.querySelector('canvas[data-rive],script[src*="rive" i]'),
74
+ shadertoy: !!D.querySelector('script[src*="shadertoy" i]'),
75
+ };
76
+
77
+ // Scroll / interaction library fingerprints. These decide whether an
78
+ // interaction is scroll-driven vs click/hover — the single most expensive
79
+ // thing to get wrong when rebuilding, because it changes the component's
80
+ // whole control model, not just its CSS. (Signatures per vendor docs; treat
81
+ // as strong hints and confirm by scrolling during recon.)
82
+ const html = D.documentElement;
83
+ const scroll = {
84
+ lenis:
85
+ !!W.Lenis ||
86
+ html.classList.contains('lenis') ||
87
+ !!D.querySelector('[data-lenis],[data-lenis-prevent],[data-lenis-prevent-wheel]'),
88
+ locomotive: !!D.querySelector('[data-scroll],[data-scroll-container],[data-scroll-section]'),
89
+ scrollTimelineCSS: detectScrollTimelineCSS(),
90
+ scrollSnap: detectScrollSnap(),
91
+ };
92
+
93
+ const surfaces = [];
94
+
95
+ // 1) Canvas surfaces — the ones the DOM track cannot see.
96
+ [...D.querySelectorAll('canvas')].forEach((c, i) => {
97
+ const r = c.getBoundingClientRect();
98
+ const kind = classifyCanvas(c);
99
+ surfaces.push({
100
+ id: `canvas-${i}`,
101
+ surface: kind, // WEBGPU | WEBGL2 | WEBGL1 | CANVAS2D | CANVAS_UNKNOWN
102
+ route: kind === 'CANVAS2D' || kind === 'CANVAS_UNKNOWN' ? 'luckiest-website-cloner-shaders?' : 'luckiest-website-cloner-shaders',
103
+ selector: cssPath(c),
104
+ rect: round(r),
105
+ coveragePct: coverage(r, vw, vh),
106
+ fullscreenHero: coverage(r, vw, vh) > 70 && r.top < vh * 0.5,
107
+ zIndex: getComputedStyle(c).zIndex,
108
+ position: getComputedStyle(c).position,
109
+ pointerEvents: getComputedStyle(c).pointerEvents,
110
+ offscreen: !!c.__offscreen,
111
+ requestedType: c.__requestedContextType || null, // present only in instrumented mode
112
+ driver: guessDriver(c, globals),
113
+ animated: null, // fill via the rAF check below if you run it
114
+ });
115
+ });
116
+
117
+ // 2) <video> surfaces — often mistaken for shader/canvas effects. A hero that
118
+ // looks generative may just be a looping muted video; cloning it as a canvas
119
+ // is wasted work. Flag them so the coordinator can just re-embed the source.
120
+ [...D.querySelectorAll('video')].forEach((v, i) => {
121
+ const r = v.getBoundingClientRect();
122
+ surfaces.push({
123
+ id: `video-${i}`,
124
+ surface: 'VIDEO',
125
+ route: 'luckiest-website-cloner-dom', // re-embed the <video>; no reconstruction needed
126
+ selector: cssPath(v),
127
+ rect: round(r),
128
+ coveragePct: coverage(r, vw, vh),
129
+ src: v.currentSrc || v.src || (v.querySelector('source') || {}).src || null,
130
+ autoplay: v.autoplay, loop: v.loop, muted: v.muted, poster: v.poster || null,
131
+ });
132
+ });
133
+
134
+ // 2b) Canvases inside <iframe>. A top-document canvas scan misses effects that
135
+ // sites host in an iframe (common — e.g. an embedded demo used as a hero). For
136
+ // SAME-ORIGIN iframes, recurse and offset the child rect into page coordinates.
137
+ // For CROSS-ORIGIN iframes, the browser blocks DOM access, so we can't classify
138
+ // the inner canvas — flag the frame as opaque so the coordinator handles it
139
+ // deliberately (re-embed the iframe src, or treat as an external effect) rather
140
+ // than silently producing an empty box.
141
+ [...D.querySelectorAll('iframe')].forEach((f, i) => {
142
+ const r = f.getBoundingClientRect();
143
+ let doc = null;
144
+ try { doc = f.contentDocument; } catch (_) { doc = null; } // cross-origin throws
145
+ if (!doc) {
146
+ surfaces.push({
147
+ id: `iframe-${i}`,
148
+ surface: 'IFRAME_OPAQUE',
149
+ route: 'coordinator-decide',
150
+ selector: cssPath(f),
151
+ rect: round(r),
152
+ coveragePct: coverage(r, vw, vh),
153
+ fullscreenHero: coverage(r, vw, vh) > 70 && r.top < vh * 0.5,
154
+ src: f.src || null,
155
+ note: 'Cross-origin iframe — inner surfaces not inspectable. Re-embed src or route to luckiest-website-cloner-shaders via a direct load of the frame URL.',
156
+ });
157
+ return;
158
+ }
159
+ [...doc.querySelectorAll('canvas')].forEach((c, j) => {
160
+ const cr = c.getBoundingClientRect(); // relative to iframe viewport
161
+ const kind = classifyCanvas(c);
162
+ surfaces.push({
163
+ id: `iframe-${i}-canvas-${j}`,
164
+ surface: kind,
165
+ route: kind === 'CANVAS2D' || kind === 'CANVAS_UNKNOWN' ? 'luckiest-website-cloner-shaders?' : 'luckiest-website-cloner-shaders',
166
+ selector: `${cssPath(f)} >> ${cssPath(c)}`, // frame-piercing hint
167
+ inIframe: f.src || true,
168
+ rect: { x: Math.round(r.left + cr.left), y: Math.round(r.top + cr.top), w: Math.round(cr.width), h: Math.round(cr.height) },
169
+ coveragePct: coverage(r, vw, vh),
170
+ fullscreenHero: coverage(r, vw, vh) > 70 && r.top < vh * 0.5,
171
+ requestedType: c.__requestedContextType || null,
172
+ driver: 'unknown-iframe',
173
+ });
174
+ });
175
+ });
176
+
177
+ // 3) Inline SVG animation surfaces — vector, not raster; DOM track handles
178
+ // them, but flag animated ones so they aren't frozen to a static snapshot.
179
+ [...D.querySelectorAll('svg')].forEach((s, i) => {
180
+ if (!s.querySelector('animate,animateTransform,animateMotion,set')) return;
181
+ const r = s.getBoundingClientRect();
182
+ surfaces.push({
183
+ id: `svg-anim-${i}`,
184
+ surface: 'SVG_ANIMATED',
185
+ route: 'luckiest-website-cloner-dom',
186
+ selector: cssPath(s),
187
+ rect: round(r),
188
+ });
189
+ });
190
+
191
+ return {
192
+ url: location.href,
193
+ title: D.title,
194
+ viewport: { w: vw, h: vh, dpr: W.devicePixelRatio || 1 },
195
+ globals,
196
+ scroll,
197
+ counts: {
198
+ canvas: D.querySelectorAll('canvas').length,
199
+ gpuCanvas: surfaces.filter((s) => /WEBG/.test(s.surface)).length,
200
+ video: D.querySelectorAll('video').length,
201
+ iframe: D.querySelectorAll('iframe').length,
202
+ opaqueIframe: surfaces.filter((s) => s.surface === 'IFRAME_OPAQUE').length,
203
+ },
204
+ surfaces,
205
+ routingSummary: routingSummary(surfaces),
206
+ };
207
+
208
+ /* ---------- helpers ---------- */
209
+
210
+ // Classify a single canvas by probing getContext.
211
+ //
212
+ // Key fact that makes this reliable post-hoc: getContext(type) returns the
213
+ // EXISTING context when `type` matches how the canvas was initialized, and
214
+ // returns null when it does not match. So probing webgl2 -> webgl -> webgpu ->
215
+ // 2d in order identifies an already-initialized canvas without clobbering it:
216
+ // the matching type hands back the live context, the others fail fast.
217
+ // WebGL2 must be probed before WebGL because a WebGL2RenderingContext is NOT
218
+ // an instanceof WebGLRenderingContext, so a webgl-first probe would mislabel
219
+ // WebGL2 canvases. Prefer instrumented mode (requestedType) when available.
220
+ function classifyCanvas(c) {
221
+ if (c.__requestedContextType) {
222
+ const t = c.__requestedContextType;
223
+ if (t === 'webgpu') return 'WEBGPU';
224
+ if (t === 'webgl2') return 'WEBGL2';
225
+ if (t === 'webgl' || t === 'experimental-webgl') return 'WEBGL1';
226
+ if (t === '2d') return 'CANVAS2D';
227
+ }
228
+ // Use the canvas's OWN realm globals for instanceof. A canvas inside an
229
+ // iframe yields a context that is an instance of the IFRAME's
230
+ // WebGL*RenderingContext, not the parent frame's, so a parent-realm
231
+ // instanceof silently fails cross-realm (misclassifying as UNKNOWN). Fall
232
+ // back to constructor-name matching when the realm globals aren't reachable.
233
+ const win = (c.ownerDocument && c.ownerDocument.defaultView) || window;
234
+ const isType = (ctx, ctorName) => {
235
+ if (!ctx) return false;
236
+ const Ctor = win[ctorName];
237
+ if (Ctor && ctx instanceof Ctor) return true;
238
+ let proto = ctx && Object.getPrototypeOf(ctx);
239
+ return !!(proto && proto.constructor && proto.constructor.name === ctorName);
240
+ };
241
+ try {
242
+ const g2 = c.getContext('webgl2');
243
+ if (isType(g2, 'WebGL2RenderingContext')) return 'WEBGL2';
244
+ } catch (_) {}
245
+ try {
246
+ const g1 = c.getContext('webgl') || c.getContext('experimental-webgl');
247
+ if (isType(g1, 'WebGLRenderingContext')) return 'WEBGL1';
248
+ } catch (_) {}
249
+ try {
250
+ const gpu = c.getContext('webgpu');
251
+ if (gpu) return 'WEBGPU';
252
+ } catch (_) {}
253
+ try {
254
+ const two = c.getContext('2d');
255
+ if (two) return 'CANVAS2D';
256
+ } catch (_) {}
257
+ return 'CANVAS_UNKNOWN';
258
+ }
259
+
260
+ function guessDriver(c, g) {
261
+ const cls = (c.className || '').toString().toLowerCase();
262
+ const id = (c.id || '').toLowerCase();
263
+ if (g.unicornStudio || /unicorn|us-/.test(cls + id)) return 'unicorn-studio';
264
+ if (g.spline || c.closest('spline-viewer')) return 'spline';
265
+ if (g.rive || c.hasAttribute('data-rive')) return 'rive';
266
+ if (g.three) return 'three.js';
267
+ if (g.babylon) return 'babylon';
268
+ if (g.pixi) return 'pixi';
269
+ if (g.regl) return 'regl';
270
+ if (g.ogl) return 'ogl';
271
+ // Production sites bundle their framework, so window.THREE etc. usually
272
+ // aren't global. Fall back to weaker signals: devtools hooks the libraries
273
+ // register, and script-src fingerprints. Lower confidence, still useful.
274
+ if (W.__THREE__ || W.__THREE_DEVTOOLS__ || scriptSrcMatches(/three(\.module)?(\.min)?\.js|three@|\/three\//i)) return 'three.js?';
275
+ if (scriptSrcMatches(/babylon/i)) return 'babylon?';
276
+ if (scriptSrcMatches(/pixi/i)) return 'pixi?';
277
+ if (scriptSrcMatches(/\bogl\b/i)) return 'ogl?';
278
+ return 'unknown';
279
+ }
280
+
281
+ function scriptSrcMatches(re) {
282
+ return [...D.querySelectorAll('script[src]')].some((s) => re.test(s.src));
283
+ }
284
+
285
+ function detectScrollTimelineCSS() {
286
+ // Scroll-driven CSS animations run on a scroll/view timeline rather than the
287
+ // default document timeline; their tell is animation-timeline plus
288
+ // scroll-timeline-name / view-timeline-name. Sample a bounded set of nodes
289
+ // (full-DOM getComputedStyle is expensive) and report if any use them.
290
+ const nodes = [...D.querySelectorAll('*')].slice(0, 400);
291
+ return nodes.some((el) => {
292
+ const cs = getComputedStyle(el);
293
+ return (
294
+ (cs.animationTimeline && cs.animationTimeline !== 'auto') ||
295
+ (cs.scrollTimelineName && cs.scrollTimelineName !== 'none') ||
296
+ (cs.viewTimelineName && cs.viewTimelineName !== 'none')
297
+ );
298
+ });
299
+ }
300
+
301
+ function detectScrollSnap() {
302
+ const nodes = [...D.querySelectorAll('*')].slice(0, 400);
303
+ return nodes.some((el) => {
304
+ const t = getComputedStyle(el).scrollSnapType;
305
+ return t && t !== 'none';
306
+ });
307
+ }
308
+
309
+ function coverage(r, vw, vh) {
310
+ const w = Math.max(0, Math.min(r.right, vw) - Math.max(r.left, 0));
311
+ const h = Math.max(0, Math.min(r.bottom, vh) - Math.max(r.top, 0));
312
+ return Math.round(((w * h) / (vw * vh)) * 100);
313
+ }
314
+
315
+ function round(r) {
316
+ return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) };
317
+ }
318
+
319
+ function cssPath(el) {
320
+ if (el.id) return `#${el.id}`;
321
+ const parts = [];
322
+ let node = el;
323
+ while (node && node.nodeType === 1 && parts.length < 5) {
324
+ let sel = node.nodeName.toLowerCase();
325
+ if (node.className && typeof node.className === 'string') {
326
+ const cls = node.className.trim().split(/\s+/).slice(0, 2).join('.');
327
+ if (cls) sel += '.' + cls;
328
+ }
329
+ const parent = node.parentNode;
330
+ if (parent) {
331
+ const sameTag = [...parent.children].filter((c) => c.nodeName === node.nodeName);
332
+ if (sameTag.length > 1) sel += `:nth-of-type(${sameTag.indexOf(node) + 1})`;
333
+ }
334
+ parts.unshift(sel);
335
+ node = node.parentNode;
336
+ }
337
+ return parts.join(' > ');
338
+ }
339
+
340
+ function routingSummary(list) {
341
+ const gpu = list.filter((s) => /WEBG/.test(s.surface));
342
+ // A GPU platform can be loaded while its canvas hasn't initialized yet —
343
+ // many effect libs lazy-init on scroll/interaction or ship video previews on
344
+ // their own marketing pages. If we detect the platform global but found no
345
+ // live GPU canvas, that's a signal to interact and re-scan, NOT to conclude
346
+ // the page is pure DOM.
347
+ const platformGlobals = ['unicornStudio', 'spline', 'rive', 'three', 'babylon', 'pixi', 'regl', 'ogl']
348
+ .filter((k) => globals[k]);
349
+ const opaque = list.filter((s) => s.surface === 'IFRAME_OPAQUE');
350
+ const lazyGpuLikely = gpu.length === 0 && platformGlobals.length > 0;
351
+ return {
352
+ hasGpuSurfaces: gpu.length > 0,
353
+ gpuHeroPresent: gpu.some((s) => s.fullscreenHero),
354
+ sendToShaderExtract: gpu.map((s) => s.selector),
355
+ opaqueIframes: opaque.map((s) => s.src || s.selector),
356
+ platformGlobalsDetected: platformGlobals,
357
+ lazyGpuLikely,
358
+ note: gpu.length
359
+ ? 'GPU surfaces present — route these to luckiest-website-cloner-shaders; the DOM track will render them as empty boxes if cloned as HTML.'
360
+ : lazyGpuLikely
361
+ ? `A GPU effect platform (${platformGlobals.join(', ')}) is loaded but no live canvas was captured — likely lazy/interaction-gated or shown as video preview. Scroll/interact and re-run surfaceMap() before concluding this is a pure DOM page.`
362
+ : 'No GPU surfaces detected — luckiest-website-cloner-dom alone is sufficient; still verify animated video/SVG are re-embedded, not snapshotted.',
363
+ };
364
+ }
365
+ }
366
+
367
+ /* Optional: sample whether a canvas is actively animating (a running rAF draw
368
+ * loop). Run in the page over ~500ms; compares pixel hashes across frames.
369
+ * Useful to separate a live shader from a one-shot canvas paint. */
370
+ async function isCanvasAnimating(selector, ms = 500) {
371
+ const c = document.querySelector(selector);
372
+ if (!c) return null;
373
+ const snap = () => {
374
+ try {
375
+ const t = document.createElement('canvas');
376
+ t.width = 32; t.height = 32;
377
+ t.getContext('2d').drawImage(c, 0, 0, 32, 32);
378
+ return t.getContext('2d').getImageData(0, 0, 32, 32).data.join(',');
379
+ } catch (_) {
380
+ return null; // cross-origin / context read blocked
381
+ }
382
+ };
383
+ const a = snap();
384
+ await new Promise((r) => setTimeout(r, ms));
385
+ const b = snap();
386
+ if (a == null || b == null) return 'unknown-readback-blocked';
387
+ return a !== b;
388
+ }
389
+
390
+ if (typeof module !== 'undefined' && module.exports) {
391
+ module.exports = { instrumentGetContext, surfaceMap, isCanvasAnimating };
392
+ }
@@ -0,0 +1,178 @@
1
+ /*
2
+ * tokens-probe.js — design-system extractor for /luckiest-website-cloner.
3
+ *
4
+ * Purpose: pull the page's REAL design tokens from computed styles of visible
5
+ * elements — palette, type scale, font stack + @font-face sources, spacing
6
+ * rhythm, radii, shadows, z-index layers, breakpoints, container widths —
7
+ * ranked by how often each value is used. This replaces "skim the CSS file and
8
+ * guess the system": a CSS file has every value the site ever declared; the
9
+ * computed DOM has the values that actually paint. Frequency separates the
10
+ * system from the one-offs.
11
+ *
12
+ * Run post-hoc in the page (any browser eval tool or Playwright
13
+ * page.evaluate) after the page has settled. Run it ONCE PER BREAKPOINT
14
+ * (1440 / 768 / 390) — the type scale and spacing change; the palette usually
15
+ * doesn't. Save to output/<hostname>/tokens-<width>.json.
16
+ *
17
+ * Nothing here mutates the page.
18
+ */
19
+ function tokensProbe(opts) {
20
+ const o = Object.assign({ maxElements: 6000, minCount: 2 }, opts || {});
21
+ const W = window, D = document;
22
+ const out = {
23
+ url: location.href,
24
+ viewport: { w: W.innerWidth, h: W.innerHeight, dpr: W.devicePixelRatio },
25
+ colors: {}, fonts: {}, fontFaces: [], typeScale: {}, lineHeights: {}, letterSpacing: {},
26
+ spacing: {}, radii: {}, shadows: {}, zIndex: {}, gradients: {}, filters: {}, blendModes: {},
27
+ maxWidths: {}, breakpoints: [], customProps: {}, body: {}, headings: {}, buttons: [], links: {},
28
+ notes: [],
29
+ };
30
+
31
+ const bump = (map, key, meta) => {
32
+ if (!key || key === 'none' || key === 'normal' || key === 'auto' || key === 'rgba(0, 0, 0, 0)') return;
33
+ if (!map[key]) map[key] = { count: 0, where: new Set() };
34
+ map[key].count++;
35
+ if (meta && map[key].where.size < 3) map[key].where.add(meta);
36
+ };
37
+ const sel = (el) => {
38
+ if (el.id) return '#' + el.id;
39
+ const c = typeof el.className === 'string' && el.className.trim() ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '';
40
+ return el.tagName.toLowerCase() + c;
41
+ };
42
+ const finalize = (map) =>
43
+ Object.entries(map)
44
+ .map(([value, v]) => ({ value, count: v.count, where: Array.from(v.where) }))
45
+ .filter((x) => x.count >= o.minCount)
46
+ .sort((a, b) => b.count - a.count)
47
+ .slice(0, 40);
48
+
49
+ // Visible-ish elements only: skip zero-size, hidden, script/style
50
+ const all = Array.from(D.body.querySelectorAll('*')).filter((el) => {
51
+ if (/^(SCRIPT|STYLE|META|LINK|NOSCRIPT|TEMPLATE|svg|path|g|defs)$/i.test(el.tagName)) return false;
52
+ const r = el.getBoundingClientRect();
53
+ return r.width > 0 && r.height > 0;
54
+ });
55
+ if (all.length > o.maxElements) out.notes.push(`sampled ${o.maxElements} of ${all.length} visible elements`);
56
+ const els = all.slice(0, o.maxElements);
57
+
58
+ const textTags = /^(H1|H2|H3|H4|H5|H6|P|A|SPAN|LI|BUTTON|LABEL|SMALL|STRONG|EM|BLOCKQUOTE|FIGCAPTION|TD|TH|DT|DD)$/;
59
+
60
+ for (const el of els) {
61
+ let cs;
62
+ try { cs = getComputedStyle(el); } catch (_) { continue; }
63
+ const s = sel(el);
64
+ const hasText = textTags.test(el.tagName) && el.textContent.trim().length > 0;
65
+
66
+ // Colors — text only counted on elements that actually have text
67
+ if (hasText) bump(out.colors, cs.color, 'text ' + s);
68
+ bump(out.colors, cs.backgroundColor, 'bg ' + s);
69
+ if (cs.borderTopWidth !== '0px') bump(out.colors, cs.borderTopColor, 'border ' + s);
70
+ if (cs.backgroundImage && cs.backgroundImage.includes('gradient')) bump(out.gradients, cs.backgroundImage, s);
71
+
72
+ // Type
73
+ if (hasText) {
74
+ bump(out.fonts, cs.fontFamily + ' / ' + cs.fontWeight, s);
75
+ bump(out.typeScale, cs.fontSize + ' / ' + cs.lineHeight + ' / ' + cs.fontWeight, s);
76
+ bump(out.lineHeights, cs.lineHeight, s);
77
+ bump(out.letterSpacing, cs.letterSpacing, s);
78
+ if (cs.textTransform !== 'none') bump(out.letterSpacing, 'transform:' + cs.textTransform, s);
79
+ }
80
+
81
+ // Spacing rhythm — paddings + margins + gaps as individual values
82
+ for (const p of ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'marginTop', 'marginBottom', 'rowGap', 'columnGap']) {
83
+ const v = cs[p];
84
+ if (v && v !== '0px' && v !== 'normal' && !v.includes('-')) bump(out.spacing, v, p.replace(/(Top|Bottom|Left|Right)$/, '') + ' ' + s);
85
+ }
86
+
87
+ // Surfaces
88
+ if (cs.borderTopLeftRadius !== '0px') bump(out.radii, cs.borderTopLeftRadius, s);
89
+ if (cs.boxShadow !== 'none') bump(out.shadows, cs.boxShadow, s);
90
+ if (cs.filter !== 'none') bump(out.filters, cs.filter, s);
91
+ if (cs.backdropFilter && cs.backdropFilter !== 'none') bump(out.filters, 'backdrop:' + cs.backdropFilter, s);
92
+ if (cs.mixBlendMode !== 'normal') bump(out.blendModes, cs.mixBlendMode, s);
93
+ if (cs.zIndex !== 'auto' && (cs.position === 'fixed' || cs.position === 'sticky' || cs.position === 'absolute' || cs.position === 'relative'))
94
+ bump(out.zIndex, cs.zIndex + ' (' + cs.position + ')', s);
95
+ if (cs.maxWidth !== 'none' && cs.maxWidth.endsWith('px')) bump(out.maxWidths, cs.maxWidth, s);
96
+ }
97
+
98
+ // Body / headings / buttons — the anchor values, verbatim
99
+ try {
100
+ const b = getComputedStyle(D.body);
101
+ out.body = { fontFamily: b.fontFamily, fontSize: b.fontSize, lineHeight: b.lineHeight, color: b.color, background: b.backgroundColor, letterSpacing: b.letterSpacing };
102
+ for (const tag of ['h1', 'h2', 'h3', 'h4', 'p']) {
103
+ const el = D.querySelector(tag);
104
+ if (!el) continue;
105
+ const c = getComputedStyle(el);
106
+ out.headings[tag] = { fontFamily: c.fontFamily, fontSize: c.fontSize, fontWeight: c.fontWeight, lineHeight: c.lineHeight, letterSpacing: c.letterSpacing, textTransform: c.textTransform, color: c.color, marginBottom: c.marginBottom };
107
+ }
108
+ const seenBtn = new Set();
109
+ for (const el of Array.from(D.querySelectorAll('button, a[class*=btn], a[class*=button], [role=button], input[type=submit]')).slice(0, 60)) {
110
+ const c = getComputedStyle(el);
111
+ const key = [c.backgroundColor, c.color, c.borderRadius, c.padding, c.fontSize, c.fontWeight, c.borderTopColor].join('|');
112
+ if (seenBtn.has(key)) continue;
113
+ seenBtn.add(key);
114
+ out.buttons.push({ example: sel(el), text: el.textContent.trim().slice(0, 30), background: c.backgroundColor, color: c.color, border: c.borderTopWidth + ' ' + c.borderTopStyle + ' ' + c.borderTopColor, radius: c.borderRadius, padding: c.padding, fontSize: c.fontSize, fontWeight: c.fontWeight, letterSpacing: c.letterSpacing, textTransform: c.textTransform, shadow: c.boxShadow, transition: c.transitionProperty + ' ' + c.transitionDuration + ' ' + c.transitionTimingFunction });
115
+ if (out.buttons.length >= 8) break;
116
+ }
117
+ const a = D.querySelector('p a, li a, nav a');
118
+ if (a) { const c = getComputedStyle(a); out.links = { color: c.color, decoration: c.textDecorationLine, weight: c.fontWeight }; }
119
+ } catch (_) {}
120
+
121
+ // @font-face sources + what actually loaded
122
+ try {
123
+ for (const ss of Array.from(D.styleSheets)) {
124
+ let rules; try { rules = ss.cssRules; } catch (_) { out.notes.push('cross-origin stylesheet (no @font-face read): ' + ss.href); continue; }
125
+ const walk = (list) => {
126
+ for (const r of Array.from(list)) {
127
+ if (r.type === CSSRule.FONT_FACE_RULE) {
128
+ const st = r.style;
129
+ out.fontFaces.push({ family: st.getPropertyValue('font-family').replace(/["']/g, ''), weight: st.getPropertyValue('font-weight'), style: st.getPropertyValue('font-style'), display: st.getPropertyValue('font-display'), src: st.getPropertyValue('src').slice(0, 400), sheet: ss.href || 'inline' });
130
+ } else if (r.cssRules && (r.type === CSSRule.MEDIA_RULE || r.type === CSSRule.SUPPORTS_RULE)) walk(r.cssRules);
131
+ if (r.type === CSSRule.MEDIA_RULE && /width/.test(r.conditionText || r.media.mediaText)) {
132
+ const m = (r.conditionText || r.media.mediaText).match(/(\d+(?:\.\d+)?)(px|em|rem)/g);
133
+ if (m) out.breakpoints.push(...m);
134
+ }
135
+ }
136
+ };
137
+ walk(rules);
138
+ }
139
+ out.breakpoints = Array.from(new Set(out.breakpoints)).sort((a, b) => parseFloat(a) - parseFloat(b));
140
+ if (D.fonts) {
141
+ out.loadedFonts = [];
142
+ D.fonts.forEach((f) => { if (f.status === 'loaded') out.loadedFonts.push(f.family.replace(/["']/g, '') + ' ' + f.weight + ' ' + f.style); });
143
+ out.loadedFonts = Array.from(new Set(out.loadedFonts));
144
+ }
145
+ // Google Fonts / external font links
146
+ out.fontLinks = Array.from(D.querySelectorAll('link[href*="fonts.googleapis"], link[href*="typekit"], link[href*="fonts.bunny"], link[as=font]')).map((l) => l.href);
147
+ } catch (e) { out.notes.push('font probe error: ' + e.message); }
148
+
149
+ // :root custom properties — the site's own token names, if it has them
150
+ try {
151
+ const cs = getComputedStyle(D.documentElement);
152
+ for (const ss of Array.from(D.styleSheets)) {
153
+ let rules; try { rules = ss.cssRules; } catch (_) { continue; }
154
+ for (const r of Array.from(rules)) {
155
+ if (r.selectorText === ':root' || r.selectorText === 'html' || r.selectorText === 'body') {
156
+ for (const p of Array.from(r.style)) if (p.startsWith('--')) out.customProps[p] = cs.getPropertyValue(p).trim();
157
+ }
158
+ }
159
+ }
160
+ } catch (_) {}
161
+
162
+ // Page-level: scroll height, sections, fixed chrome
163
+ try {
164
+ out.page = {
165
+ scrollHeight: D.documentElement.scrollHeight,
166
+ sections: Array.from(D.querySelectorAll('main > *, body > section, section, [class*=section]')).filter((s) => s.getBoundingClientRect().height > 120).slice(0, 40).map((s) => { const r = s.getBoundingClientRect(); return { selector: sel(s), top: Math.round(r.top + W.scrollY), height: Math.round(r.height), background: getComputedStyle(s).backgroundColor }; }),
167
+ fixed: Array.from(D.querySelectorAll('*')).filter((e) => { const p = getComputedStyle(e).position; return p === 'fixed' || p === 'sticky'; }).slice(0, 20).map((e) => { const r = e.getBoundingClientRect(); return { selector: sel(e), position: getComputedStyle(e).position, rect: [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)], z: getComputedStyle(e).zIndex }; }),
168
+ };
169
+ } catch (_) {}
170
+
171
+ for (const k of ['colors', 'fonts', 'typeScale', 'lineHeights', 'letterSpacing', 'spacing', 'radii', 'shadows', 'zIndex', 'gradients', 'filters', 'blendModes', 'maxWidths']) out[k] = finalize(out[k]);
172
+
173
+ return out;
174
+ }
175
+
176
+ if (typeof module !== 'undefined' && module.exports) {
177
+ module.exports = { tokensProbe };
178
+ }
@@ -0,0 +1,4 @@
1
+ # Attribution
2
+
3
+ See `../luckiest-website-cloner/ATTRIBUTION.md`. Luckiest edition of the
4
+ open-source site-clone skill collection, MIT licensed.
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ First Luckiest edition of `luckiest-website-cloner-dom`. Renamed from the upstream site-clone
6
+ collection, guardrails added to every entry point, recipes added for running the
7
+ probes from an in-app browser pane, frontmatter normalized to the Luckiest
8
+ package format. See ATTRIBUTION.md.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chase Hannegan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.