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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/luckiest-ab-testing/SKILL.md +1 -1
- package/skills/luckiest-ad-creative/SKILL.md +1 -1
- package/skills/luckiest-ads/SKILL.md +1 -1
- package/skills/luckiest-advisors/SKILL.md +1 -1
- package/skills/luckiest-aeo-grader/SKILL.md +1 -1
- package/skills/luckiest-ai-seo/SKILL.md +1 -1
- package/skills/luckiest-analytics/SKILL.md +1 -1
- package/skills/luckiest-aso/SKILL.md +1 -1
- package/skills/luckiest-churn-prevention/SKILL.md +1 -1
- package/skills/luckiest-co-marketing/SKILL.md +1 -1
- package/skills/luckiest-coder/SKILL.md +1 -1
- package/skills/luckiest-coder-brainstorming/SKILL.md +1 -1
- package/skills/luckiest-coder-consistency/SKILL.md +1 -1
- package/skills/luckiest-coder-constitution/SKILL.md +1 -1
- package/skills/luckiest-coder-debugging/SKILL.md +1 -1
- package/skills/luckiest-coder-dispatching-parallel-agents/SKILL.md +1 -1
- package/skills/luckiest-coder-executing-plans/SKILL.md +1 -1
- package/skills/luckiest-coder-finishing-a-branch/SKILL.md +1 -1
- package/skills/luckiest-coder-git-worktrees/SKILL.md +1 -1
- package/skills/luckiest-coder-guard/SKILL.md +1 -1
- package/skills/luckiest-coder-qa/SKILL.md +1 -1
- package/skills/luckiest-coder-receiving-code-review/SKILL.md +1 -1
- package/skills/luckiest-coder-requesting-code-review/SKILL.md +1 -1
- package/skills/luckiest-coder-shipping/SKILL.md +1 -1
- package/skills/luckiest-coder-subagent-driven-development/SKILL.md +1 -1
- package/skills/luckiest-coder-tdd/SKILL.md +1 -1
- package/skills/luckiest-coder-verification/SKILL.md +1 -1
- package/skills/luckiest-coder-writing-plans/SKILL.md +1 -1
- package/skills/luckiest-coder-writing-skills/SKILL.md +1 -1
- package/skills/luckiest-cold-email/SKILL.md +1 -1
- package/skills/luckiest-community-marketing/SKILL.md +1 -1
- package/skills/luckiest-competitor-profiling/SKILL.md +1 -1
- package/skills/luckiest-competitors/SKILL.md +1 -1
- package/skills/luckiest-content-strategy/SKILL.md +1 -1
- package/skills/luckiest-copy-editing/SKILL.md +1 -1
- package/skills/luckiest-copywriting/SKILL.md +1 -1
- package/skills/luckiest-cro/SKILL.md +1 -1
- package/skills/luckiest-customer-research/SKILL.md +1 -1
- package/skills/luckiest-directory-submissions/SKILL.md +1 -1
- package/skills/luckiest-emails/SKILL.md +1 -1
- package/skills/luckiest-extract-design-system/SKILL.md +1 -1
- package/skills/luckiest-free-tools/SKILL.md +1 -1
- package/skills/luckiest-image/SKILL.md +1 -1
- package/skills/luckiest-launch/SKILL.md +1 -1
- package/skills/luckiest-lead-magnets/SKILL.md +1 -1
- package/skills/luckiest-marketing-ideas/SKILL.md +1 -1
- package/skills/luckiest-marketing-plan/SKILL.md +1 -1
- package/skills/luckiest-marketing-psychology/SKILL.md +1 -1
- package/skills/luckiest-model-router/SKILL.md +1 -1
- package/skills/luckiest-offers/SKILL.md +1 -1
- package/skills/luckiest-onboarding/SKILL.md +1 -1
- package/skills/luckiest-paywalls/SKILL.md +1 -1
- package/skills/luckiest-popups/SKILL.md +1 -1
- package/skills/luckiest-pricing/SKILL.md +1 -1
- package/skills/luckiest-product-marketing/SKILL.md +1 -1
- package/skills/luckiest-programmatic-seo/SKILL.md +1 -1
- package/skills/luckiest-prompt-rewrite/SKILL.md +1 -1
- package/skills/luckiest-prospecting/SKILL.md +1 -1
- package/skills/luckiest-public-relations/SKILL.md +1 -1
- package/skills/luckiest-referrals/SKILL.md +1 -1
- package/skills/luckiest-revops/SKILL.md +1 -1
- package/skills/luckiest-sales-enablement/SKILL.md +1 -1
- package/skills/luckiest-schema/SKILL.md +1 -1
- package/skills/luckiest-seo-audit/SKILL.md +1 -1
- package/skills/luckiest-session-handoff/SKILL.md +1 -1
- package/skills/luckiest-signup/SKILL.md +1 -1
- package/skills/luckiest-site-architecture/SKILL.md +1 -1
- package/skills/luckiest-sms/SKILL.md +1 -1
- package/skills/luckiest-social/SKILL.md +1 -1
- package/skills/luckiest-thinker/SKILL.md +1 -1
- package/skills/luckiest-trends/SKILL.md +1 -1
- package/skills/luckiest-video/SKILL.md +1 -1
- package/skills/luckiest-website-cloner/ATTRIBUTION.md +29 -0
- package/skills/luckiest-website-cloner/CHANGELOG.md +8 -0
- package/skills/luckiest-website-cloner/LICENSE +21 -0
- package/skills/luckiest-website-cloner/SKILL.md +289 -0
- package/skills/luckiest-website-cloner/references/builtin-browser-recipes.md +74 -0
- package/skills/luckiest-website-cloner/references/compositing.md +65 -0
- package/skills/luckiest-website-cloner/references/fidelity-fast-path.md +92 -0
- package/skills/luckiest-website-cloner/references/playwright-cli-recipes.md +68 -0
- package/skills/luckiest-website-cloner/references/teardown.md +142 -0
- package/skills/luckiest-website-cloner/references/visual-qa.md +102 -0
- package/skills/luckiest-website-cloner/scripts/build-bundle.cjs +12 -0
- package/skills/luckiest-website-cloner/scripts/motion-probe.js +423 -0
- package/skills/luckiest-website-cloner/scripts/surface-map.js +392 -0
- package/skills/luckiest-website-cloner/scripts/tokens-probe.js +178 -0
- package/skills/luckiest-website-cloner-dom/ATTRIBUTION.md +4 -0
- package/skills/luckiest-website-cloner-dom/CHANGELOG.md +8 -0
- package/skills/luckiest-website-cloner-dom/LICENSE +21 -0
- package/skills/luckiest-website-cloner-dom/SKILL.md +170 -0
- package/skills/luckiest-website-cloner-dom/references/asset-resolution.md +93 -0
- package/skills/luckiest-website-cloner-dom/references/interaction-fingerprints.md +85 -0
- package/skills/luckiest-website-cloner-remix/ATTRIBUTION.md +4 -0
- package/skills/luckiest-website-cloner-remix/CHANGELOG.md +8 -0
- package/skills/luckiest-website-cloner-remix/LICENSE +21 -0
- package/skills/luckiest-website-cloner-remix/SKILL.md +202 -0
- package/skills/luckiest-website-cloner-remix/references/directions.md +82 -0
- package/skills/luckiest-website-cloner-remix/references/reskin.md +84 -0
- package/skills/luckiest-website-cloner-remix/references/tweak-panel.md +72 -0
- package/skills/luckiest-website-cloner-remix/scripts/apply-overrides.cjs +105 -0
- package/skills/luckiest-website-cloner-remix/scripts/gallery.cjs +71 -0
- package/skills/luckiest-website-cloner-remix/scripts/tokenize-css.cjs +127 -0
- package/skills/luckiest-website-cloner-remix/scripts/tweak-panel.js +169 -0
- package/skills/luckiest-website-cloner-shaders/ATTRIBUTION.md +11 -0
- package/skills/luckiest-website-cloner-shaders/CHANGELOG.md +8 -0
- package/skills/luckiest-website-cloner-shaders/LICENSE +21 -0
- package/skills/luckiest-website-cloner-shaders/SKILL.md +129 -0
- package/skills/luckiest-website-cloner-shaders/references/capture-backends.md +89 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/SKILL.md +123 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/capture-backends.md +201 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/evidence-policy.md +93 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/operating-contract.md +82 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/qa-failure-policy.md +100 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/recon-kernel.md +206 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/replay-policy.md +145 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/shaders-com.md +212 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/source-analysis.md +112 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/surface-discovery.md +113 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/target-lock.md +205 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/three-shader-reconstruction.md +155 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/tool-capability-matrix.md +57 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/references/unicorn-studio.md +387 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/scripts/fetch-rendered-dom.mjs +178 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/scripts/scan-bundle.sh +80 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/extraction-report.md +41 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/known-gaps.md +14 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/qa-report.md +74 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/replay-manifest.json +147 -0
- package/skills/luckiest-website-cloner-shaders/vendor/web-shader-extractor/templates/run-state.json +75 -0
- 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,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.
|