@trueclone/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scripts/tiles.mjs CHANGED
@@ -1,25 +1,41 @@
1
1
  // Cut a tall full-page screenshot into viewport-sized tiles you can actually read.
2
- // usage: node scripts/tiles.mjs <png> [tileHeight=1200] -> <png-dir>/tiles/<name>-01.png ...
2
+ // usage: node scripts/tiles.mjs <name> [--size desktop] [--clone] -> sites/<name>/reference/tiles/<size>-01.png ...
3
+ // node scripts/tiles.mjs <png> [tileHeight=1200] -> <png-dir>/tiles/<png-name>-01.png ...
3
4
  import fs from 'node:fs/promises';
4
5
  import path from 'node:path';
5
6
  import { PNG } from 'pngjs';
7
+ import { SCORED, parseArgs } from './lib.mjs';
6
8
 
7
- const [file, h = '1200'] = process.argv.slice(2);
8
- if (!file) {
9
- console.error('usage: npm run tiles -- <png> [tileHeight]');
9
+ const { pos, flags } = parseArgs(process.argv.slice(2));
10
+ const [target, h] = pos;
11
+ if (!target) {
12
+ console.error('usage: trueclone tiles <name> [--size desktop|tablet|mobile|laptop|wide] [--clone] or trueclone tiles <file.png> [tileHeight]');
10
13
  process.exit(1);
11
14
  }
12
- const tileH = Number(h);
13
- const src = PNG.sync.read(await fs.readFile(file));
14
- const dir = path.join(path.dirname(file), 'tiles');
15
- await fs.mkdir(dir, { recursive: true });
16
- const base = path.basename(file, '.png');
17
- let n = 0;
18
- for (let y = 0; y < src.height; y += tileH) {
19
- const th = Math.min(tileH, src.height - y);
20
- const out = new PNG({ width: src.width, height: th });
21
- PNG.bitblt(src, out, 0, y, src.width, th, 0, 0);
22
- const name = `${base}-${String(++n).padStart(2, '0')}.png`;
23
- await fs.writeFile(path.join(dir, name), PNG.sync.write(out));
15
+ let files;
16
+ let tileH = Number(h || 1200);
17
+ if (/\.png$/i.test(target)) files = [path.resolve(target)];
18
+ else {
19
+ // a site name: the reference's full-page shots (or the clone's last layout shots with --clone)
20
+ const root = path.resolve('sites', target);
21
+ const sizes = flags.size ? [flags.size] : SCORED.map((v) => v.name);
22
+ files = sizes.map((s) => (flags.clone ? path.join(root, 'compare', `${s}.mask.clone.png`) : path.join(root, 'reference', `${s}.png`)));
23
+ if (!h) tileH = 0; // one tile per screen of that size
24
+ }
25
+ for (const file of files) {
26
+ const src = await fs.readFile(file).then((b) => PNG.sync.read(b), () => null);
27
+ if (!src) { console.log(`skipped (not found): ${path.relative(process.cwd(), file)}`); continue; }
28
+ const size = SCORED.find((v) => path.basename(file).startsWith(`${v.name}.`));
29
+ const th = tileH || size?.height || 1200;
30
+ const dir = path.join(path.dirname(file), 'tiles');
31
+ await fs.mkdir(dir, { recursive: true });
32
+ const base = path.basename(file, '.png');
33
+ let n = 0;
34
+ for (let y = 0; y < src.height; y += th) {
35
+ const hh = Math.min(th, src.height - y);
36
+ const out = new PNG({ width: src.width, height: hh });
37
+ PNG.bitblt(src, out, 0, y, src.width, hh, 0, 0);
38
+ await fs.writeFile(path.join(dir, `${base}-${String(++n).padStart(2, '0')}.png`), PNG.sync.write(out));
39
+ }
40
+ console.log(`${n} tiles of ${th}px -> ${path.relative(process.cwd(), dir)} (${base}-NN.png)`);
24
41
  }
25
- console.log(`${n} tiles -> ${dir}`);
@@ -0,0 +1,198 @@
1
+ // Inline styles a script set, read at many screen sizes (capture), turned into what the clone may carry:
2
+ // - a value that is the same at every size stays as it is;
3
+ // - a value that is a function of the viewport (a·width + b·height + c, in px) becomes that function as a CSS calc(),
4
+ // for all sizes when one function fits them all, or per range of the site's own breakpoints;
5
+ // - a value that fits no such function (it depends on content, time, a container, randomness) is NOT carried: the
6
+ // clone's script must compute it, the way the site does. It is listed as script-owned.
7
+ // Nothing here is ever exact at the captured sizes only: that is the point.
8
+
9
+ const MODELS = [
10
+ { name: 'c', cols: () => [1] },
11
+ { name: 'w', cols: (w) => [w] },
12
+ { name: 'h', cols: (w, h) => [h] },
13
+ { name: 'w+c', cols: (w) => [w, 1] },
14
+ { name: 'h+c', cols: (w, h) => [h, 1] },
15
+ { name: 'w+h', cols: (w, h) => [w, h] },
16
+ { name: 'w+h+c', cols: (w, h) => [w, h, 1] },
17
+ ];
18
+
19
+ function solve(A, b) {
20
+ const n = b.length;
21
+ const M = A.map((row, i) => [...row, b[i]]);
22
+ for (let c = 0; c < n; c++) {
23
+ let p = c;
24
+ for (let r = c + 1; r < n; r++) if (Math.abs(M[r][c]) > Math.abs(M[p][c])) p = r;
25
+ if (Math.abs(M[p][c]) < 1e-9) return null;
26
+ [M[c], M[p]] = [M[p], M[c]];
27
+ for (let r = 0; r < n; r++) {
28
+ if (r === c) continue;
29
+ const f = M[r][c] / M[c][c];
30
+ for (let k = c; k <= n; k++) M[r][k] -= f * M[c][k];
31
+ }
32
+ }
33
+ return M.map((row, i) => row[n] / row[i]);
34
+ }
35
+
36
+ // points: [{ w, h, v }] -> { model, coef } or null. A model needs one point more than it has parameters, so every fit
37
+ // is checked on a point it was not determined by.
38
+ export function fitPoints(points, shaped = true) {
39
+ for (const m of MODELS) {
40
+ const k = m.cols(1, 1).length;
41
+ if (points.length < k + 1) continue;
42
+ const X = points.map((p) => m.cols(p.w, p.h));
43
+ const A = Array.from({ length: k }, () => new Array(k).fill(0));
44
+ const b = new Array(k).fill(0);
45
+ points.forEach((p, i) => { for (let r = 0; r < k; r++) { b[r] += X[i][r] * p.v; for (let c = 0; c < k; c++) A[r][c] += X[i][r] * X[i][c]; } });
46
+ const coef = solve(A, b);
47
+ if (!coef) continue;
48
+ const tol = Math.max(0.75, 0.003 * Math.max(...points.map((p) => Math.abs(p.v))));
49
+ if (points.every((p, i) => Math.abs(X[i].reduce((s, x, j) => s + x * coef[j], 0) - p.v) <= tol)) return { model: m.name, coef };
50
+ }
51
+ return shaped ? fitTwoLines(points) : null;
52
+ }
53
+
54
+ // The smaller or larger of two lines: min(L1, L2) / max(L1, L2), each L = a·w + b·h + c. This is what a script
55
+ // computes for "cover" (max(100vw, 177.8vh)), "contain", a cover image centred (min((w - 1.778·h) / 2, 0)), a tile
56
+ // of it, a size clamped at a limit. Which line applies switches at an aspect ratio, a width or a height: the points
57
+ // are split on each of those in turn, each side must fit one line on its own (checked as usual), and then every point
58
+ // must be the min (or max) of both lines. Eight points at least, so two short lines never fit noise.
59
+ function fitTwoLines(points) {
60
+ if (points.length < 8) return null;
61
+ const tol = Math.max(0.75, 0.003 * Math.max(...points.map((p) => Math.abs(p.v))));
62
+ for (const key of [(p) => p.w / p.h, (p) => p.w, (p) => p.h]) {
63
+ const sorted = [...points].sort((p, q) => key(p) - key(q));
64
+ for (let s = 2; s <= sorted.length - 2; s++) {
65
+ if (key(sorted[s - 1]) === key(sorted[s])) continue;
66
+ const lo = fitPoints(sorted.slice(0, s), false), hi = lo && fitPoints(sorted.slice(s), false);
67
+ if (!lo || !hi) continue;
68
+ for (const model of ['min', 'max']) {
69
+ const f = model === 'min' ? Math.min : Math.max;
70
+ if (points.every((p) => Math.abs(f(lineAt(lo, p.w, p.h), lineAt(hi, p.w, p.h)) - p.v) <= tol)) return { model, lines: [lo, hi] };
71
+ }
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ const lineAt = ({ model, coef }, w, h) => {
77
+ let i = 0, v = 0;
78
+ for (const t of model.split('+')) v += coef[i++] * (t === 'w' ? w : t === 'h' ? h : 1);
79
+ return v;
80
+ };
81
+
82
+ const r4 = (x) => +x.toFixed(4);
83
+ // a fitted px value as CSS: width terms use --tc-vw (1% of the layout width, scrollbar excluded, set by the seed)
84
+ export function cssOf({ model, coef, lines }) {
85
+ if (lines) return `${model}(${lines.map((l) => cssOf(l).replace(/^calc\((.*)\)$/, '$1')).join(', ')})`;
86
+ const parts = model.split('+');
87
+ let a = 0, b = 0, c = 0;
88
+ parts.forEach((p, i) => { if (p === 'w') a = coef[i]; else if (p === 'h') b = coef[i]; else c = coef[i]; });
89
+ const terms = [];
90
+ if (Math.abs(a) > 1e-6) terms.push([a * 100, (x) => `${r4(Math.abs(x))} * var(--tc-vw, 1vw)`]);
91
+ if (Math.abs(b) > 1e-6) terms.push([b * 100, (x) => `${r4(Math.abs(x))}vh`]);
92
+ if (Math.abs(c) > 0.005 || !terms.length) terms.push([c, (x) => `${+Math.abs(x).toFixed(2)}px`]);
93
+ if (terms.length === 1 && model === 'c') return `${+c.toFixed(2)}px`;
94
+ return `calc(${terms.map(([x, f], i) => `${i === 0 ? (x < 0 ? '-1 * ' : '') : x < 0 ? '- ' : '+ '}${f(x)}`).join(' ')})`;
95
+ }
96
+
97
+ const PX = /-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?px/gi; // 5.68434e-14px is a number too (a script's float noise)
98
+ // one spelling per meaning, so a library that writes translate3d(0px, 0px, 0px) at one size and translate(0px, 0px)
99
+ // at another is not mistaken for a value that changes
100
+ const normalise = (v) => v
101
+ .replace(/translate3d\(\s*([^,()]+),\s*([^,()]+),\s*0(?:px)?\s*\)/g, 'translate($1, $2)')
102
+ .replace(/translateX\(\s*([^,()]+)\)/g, 'translate($1, 0px)')
103
+ .replace(/translateY\(\s*([^,()]+)\)/g, 'translate(0px, $1)')
104
+ .replace(/translate\(\s*([^,()]+)\)/g, 'translate($1, 0px)')
105
+ .replace(/(^|[\s(,])0(?=[\s),]|$)/g, '$10px');
106
+ const parseDecls = (text) => {
107
+ const map = new Map();
108
+ for (const d of String(text || '').split(/;(?![^(]*\))/)) {
109
+ const i = d.indexOf(':');
110
+ if (i < 0) continue;
111
+ const k = d.slice(0, i).trim(), v = d.slice(i + 1).trim().replace(/\s*!important$/i, '');
112
+ if (k) map.set(k, /transform|translate/.test(k) ? normalise(v) : v);
113
+ }
114
+ return map;
115
+ };
116
+
117
+ // values: [{ w, h, v: string }] -> CSS value string, or null when no viewport function fits
118
+ function fitValue(values) {
119
+ const tmpl = values[0].v.replace(PX, '§');
120
+ if (values.some((x) => x.v.replace(PX, '§') !== tmpl)) return null; // keywords or unitless numbers change: not a function of size
121
+ if (values.every((x) => x.v === values[0].v)) return values[0].v;
122
+ const nums = values.map((x) => (x.v.match(PX) || []).map(parseFloat));
123
+ const out = [];
124
+ for (let t = 0; t < nums[0].length; t++) {
125
+ const fit = fitPoints(values.map((x, i) => ({ w: x.w, h: x.h, v: nums[i][t] })));
126
+ if (!fit) return null;
127
+ out.push(cssOf(fit));
128
+ }
129
+ let k = 0;
130
+ return tmpl.replace(/§/g, () => out[k++]);
131
+ }
132
+
133
+ // data: inline-styles.json ({ breakpoints, sizes, styles: { "WxH": { w, h, styles: { path: [style, class] } } } }), or
134
+ // the 0.3 shape ({ desktop: { path: style }, ... }). Returns, per element path: the declarations to set inline, the
135
+ // per-range rules, and the properties left to the clone's script.
136
+ export function planInlineStyles(data, viewports) {
137
+ let breakpoints = [], sizes = [];
138
+ if (data?.styles) {
139
+ breakpoints = data.breakpoints || [];
140
+ sizes = Object.values(data.styles).map((s) => ({ w: s.w, h: s.h, styles: Object.fromEntries(Object.entries(s.styles).map(([p, v]) => [p, Array.isArray(v) ? v[0] : v])) }));
141
+ } else if (data) {
142
+ sizes = Object.entries(data).map(([name, styles]) => { const vp = viewports.find((v) => v.name === name); return vp ? { w: vp.width, h: vp.height, styles } : null; }).filter(Boolean);
143
+ }
144
+ const edges = [0, ...breakpoints, 100000];
145
+ const ranges = edges.slice(0, -1).map((lo, i) => ({ lo: i === 0 ? 0 : lo + 1, hi: edges[i + 1] }));
146
+ const mediaOf = (r) => (r.lo === 0 ? `(max-width: ${r.hi}px)` : r.hi >= 100000 ? `(min-width: ${r.lo}px)` : `(min-width: ${r.lo}px) and (max-width: ${r.hi}px)`);
147
+ const plan = {};
148
+ const owned = [];
149
+ let fitted = 0, constant = 0;
150
+ const paths = new Set(sizes.flatMap((s) => Object.keys(s.styles)));
151
+ for (const p of paths) {
152
+ const per = sizes.map((s) => ({ w: s.w, h: s.h, d: parseDecls(s.styles[p]) }));
153
+ const props = new Set(per.flatMap((x) => [...x.d.keys()]));
154
+ const entry = { inline: {}, rules: [], dropped: [] };
155
+ for (const k of props) {
156
+ const vals = per.map((x) => ({ w: x.w, h: x.h, v: x.d.get(k) }));
157
+ if (vals.every((x) => x.v !== undefined && x.v === vals[0].v)) { constant++; continue; } // same everywhere: as captured
158
+ // one function for every size
159
+ if (vals.every((x) => x.v !== undefined)) {
160
+ const css = fitValue(vals);
161
+ if (css !== null) { entry.inline[k] = css; fitted++; continue; }
162
+ }
163
+ // per range of the site's breakpoints. Neighbouring ranges one function fits are one group (fewer rules, more
164
+ // points behind each fit); a range no captured size fell in joins its neighbour rather than losing the value.
165
+ const joins = (a, b) => {
166
+ if (!a.length || !b.length) return true;
167
+ const all = [...a, ...b];
168
+ const unset = all.filter((x) => x.v === undefined).length;
169
+ return unset === all.length || (!unset && fitValue(all) !== null);
170
+ };
171
+ const groups = [];
172
+ for (const r of ranges.length > 1 ? ranges : []) {
173
+ const inR = vals.filter((x) => x.w >= r.lo && x.w <= r.hi);
174
+ const last = groups[groups.length - 1];
175
+ if (last && joins(last.pts, inR)) { last.hi = r.hi; last.pts.push(...inR); } else groups.push({ lo: r.lo, hi: r.hi, pts: [...inR] });
176
+ }
177
+ let anyDrop = !groups.length;
178
+ const rules = [];
179
+ for (const g of groups) {
180
+ if (g.pts.every((x) => x.v === undefined)) continue; // the script doesn't set it at these sizes
181
+ // a fit needs a point it was not determined by; a group with unset and set sizes is not a function of size
182
+ if (g.pts.length < 2 || g.pts.some((x) => x.v === undefined)) { anyDrop = true; continue; }
183
+ const css = fitValue(g.pts);
184
+ if (css === null) { anyDrop = true; continue; }
185
+ rules.push({ media: mediaOf(g), prop: k, value: css });
186
+ }
187
+ // all or nothing: a property the clone's script must compute at some sizes is the script's at every size
188
+ // (half carried by the seed, half computed, the two would fight at the edges)
189
+ const anyRule = !anyDrop && rules.length > 0;
190
+ if (anyRule) entry.rules.push(...rules);
191
+ entry.inline[k] = null; // the captured constant no longer applies
192
+ if (anyRule) fitted++;
193
+ if (anyDrop || !anyRule) { entry.dropped.push(k); owned.push({ path: p, prop: k, samples: vals.slice(0, 5).map((x) => `${x.w}x${x.h}: ${x.v ?? '(unset)'}`) }); }
194
+ }
195
+ if (Object.keys(entry.inline).length || entry.rules.length || entry.dropped.length) plan[p] = entry;
196
+ }
197
+ return { plan, owned, fitted, constant, breakpoints, sizes: sizes.length };
198
+ }
package/skill/SKILL.md CHANGED
@@ -1,187 +1,161 @@
1
1
  ---
2
2
  name: trueclone
3
- description: Build a working twin of a website from its URL - HTML/CSS/JS clone, Next.js + Tailwind + shadcn app, DESIGN.md and shadcn globals.css - and prove the match with layout, scroll-state, motion, interaction, hover and style scores at mobile, tablet and desktop widths. Use when the user gives a URL and asks to clone, replicate, rebuild or migrate a site or page, or to extract its design system.
3
+ description: Build the exact same website from its URL - HTML/CSS/JS clone and Next.js + Tailwind + shadcn app, identical at every screen size, plus DESIGN.md and a shadcn globals.css - and prove it with layout, scroll, canvas, motion, hover, interaction, style and load checks at seed and held-out screen sizes, and a frame-by-frame visual review. Use when the user gives a URL and asks to clone, replicate, rebuild or migrate a site or page, or to extract its design system.
4
4
  ---
5
5
 
6
- # Build a true clone of a website
7
-
8
- Everything for a site lives in `sites/<name>/` (gitignored, stays local):
9
- - `clone/`: plain HTML/CSS/JS, the source of truth.
10
- - `next/`: the same site as a Next.js + Tailwind v4 + shadcn app, generated from `clone/`.
11
- - `design-tokens.json`: the **one canonical source** of the design system, rendered to `DESIGN.md` (Google design.md format, linted), `globals.css` (shadcn theme) and `clone/tokens.css`.
12
- - `REPORT.md`: every score against its target, what to fix first, what was not copied. This is the deliverable.
13
- - `notes.md`: your record, written by the seed with fixed sections; `REPORT.md` reads Findings and Accepted differences from it (see 2).
14
-
15
- Run every step as `npx trueclone <step> <name>` from the project folder (`npx trueclone help` lists them).
16
-
17
- ## 0. Pick the mode
18
-
19
- | Mode | Use it when | Content in the clone |
20
- |---|---|---|
21
- | `template` (default) | Anyone else's site: learning, layout study, a starting template | Placeholder brand, text, images and logos; scored with `--structure` |
22
- | `exact` | The user owns the site or is authorised to copy it (a client migration, their own Webflow/Framer/WordPress site) | The site's own text and media |
23
-
24
- Exact mode refuses to run without `--permission "<who authorised it>"`, and the value is recorded in `meta.json` and in `REPORT.md`. Never use exact mode on a site the user doesn't own or have permission for. If permission is unclear, ask.
6
+ # Build the same website, at every screen size
7
+
8
+ The goal: the user opens the clone on localhost on any device, at any window size, and it looks and behaves exactly
9
+ like the original. Not at three widths: at every width. Accuracy comes first, time second, scores third: a score is
10
+ an instrument; when it and your eyes disagree, your eyes win and you say so in `notes.md` (Findings).
11
+
12
+ Everything for a site lives in `sites/<name>/` (gitignored, local): `clone/` (the source of truth), `next/` (the
13
+ Next.js app generated from it), `notes.md` (your record, a contract), `REPORT.md` (the deliverable). Run every step as
14
+ `npx trueclone <step> <name>` from the project folder (`npx trueclone help` lists them).
15
+
16
+ ## The rules
17
+
18
+ 1. **Structure comes from the seed; behaviour is code.** The seed gives the site's markup, stylesheets, media and
19
+ every value that is the same at every size or a plain function of the screen size. Everything a script does
20
+ (layout it computes, scenes it builds, motion, hover, state, canvases) you re-implement in `clone/script.js`,
21
+ **porting the site's own rule** from `reference/scripts/` (`INDEX.md` ranks the scripts by what actually ran and
22
+ points every event listener at file:line). Read the algorithm; write your own code; never paste the site's code.
23
+ 2. **Every screen size.** Your code computes from the live viewport, the way the site's does (`innerWidth`,
24
+ `innerHeight`, element boxes, `resize`, the site's own breakpoints). Never put a pixel value you measured or
25
+ recorded at one size into the clone. Elements marked `data-tc-script-owned` (and `reference/script-owned.json`) hold
26
+ values the site computes that no simple function of the screen size explains: they are yours to compute.
27
+ 3. **Never edit generated files.** `clone/index.html` is seed output. What a clone needs on top goes in
28
+ `clone/patches/` (see Build). A fix that needs the generated markup changed is a tool defect: write it in Findings.
29
+ 4. **Look, don't just score.** `npx trueclone review <name>` shows reference and clone side by side at a size the
30
+ clone was never built from, at load, at every section, with the pointer on a control. You give every frame a
31
+ verdict in `notes.md`. A frame that differs is open, whatever the numbers say.
32
+ 5. **Done means done.** Every check passes at every scored size, the held-out sizes included; every review frame is
33
+ "same"; an accepted difference is only for the last points of a check already ≥ 95 and names an evidence file.
34
+ There is no pass limit: keep fixing until that holds or every remaining gap is proven a tool defect in Findings.
35
+ 6. **Exact mode only with permission.** `template` (default) is for anyone's site: placeholder text and media, the
36
+ original's structure and behaviour. `exact` copies the site's own text and media and refuses to run without
37
+ `--permission "<who authorised it>"`; never use it on a site the user doesn't own or have permission for. Never get
38
+ past bot checks, logins or paywalls. No tracking scripts; forms submit nowhere; `noindex`.
25
39
 
26
40
  ## 1. Capture
27
41
 
28
42
  ```powershell
29
43
  npx trueclone capture <url> [name] [--hide "<cookie banner / popup selectors>"] [--mode exact --permission "<owner>"]
30
- npx trueclone states <name> # live scroll states + motion inventory (animations running)
31
- npx trueclone motion <name> # animation runtime: ScrollTriggers, timelines, reveals, cursor tweens, loops, Lenis/Swiper, CSS
32
- npx trueclone spec <name> # measured boxes and styles per viewport
33
- npx trueclone understand <name> # code evidence with REA: which scripts ran, each control's handler with its source -> code.md
34
- ```
35
-
36
- `states`, `motion`, `spec` and `understand` only read the reference: run them together in the background while you read the capture.
37
-
38
- `npx trueclone <url> [name]` is a shortcut for capture + design-system draft.
39
-
40
- `reference/` then holds:
41
- - full-page PNGs and their `*.mask.png` structure versions, `sections.json` and `rendered.html`;
42
- - `tokens.json` (colours, type, radii, CSS variables), `assets/` with `manifest.json`, and `desktop.har` (every request, including the data/API calls);
43
- - **`states/<vp>/NN.png`**: what a visitor sees at every half-viewport scroll step, after motion settles;
44
- - **`states/<vp>.summary.json`** (the motion inventory): `reveals`, `animated`, `parallax`, `pinned` and `fixed` elements;
45
- - **`motion.<vp>.md`**: the exact animation recipe when the site uses GSAP/ScrollTrigger, Lenis, Swiper or CSS animations: trigger, start/end, scrub, pin, properties and ranges, duration, easing, stagger. Rebuild these with the same public libraries and these numbers, in your own code;
46
- - in exact mode, `mirror/index.html`: a script-free local snapshot with assets rewritten to local copies.
47
-
48
- Read the motion inventory before building. Each entry is a behaviour the clone must have.
49
-
50
- If the page is blank or blocked, capture through your own Chrome over CDP (`chrome --remote-debugging-port=9222 --user-data-dir=.chrome-profile`, then `--cdp`). Never try to get past bot checks, logins or paywalls.
51
-
52
- ## 2. Understand it
53
-
54
- ```powershell
55
- npx trueclone understand <name> # code evidence with REA -> reference/code.md (2-4 min; run it with states/motion/spec)
44
+ npx trueclone states <name> # scroll states at every scored size (reference/states/)
45
+ npx trueclone motion <name> # the animation runtime: ScrollTriggers, timelines, tweens, loops, Lenis/Swiper, CSS
46
+ npx trueclone spec <name> # measured boxes and styles per size
56
47
  ```
57
48
 
58
- The capture says what the page looks like and does; `code.md` says **which code does it**. trueclone owns the browser and drives REA (Reverse Engineer Anything, bundled) for you; you never call REA directly for this. `code.md` has:
59
- - the scripts ranked by how much of them ran while scrolling the page and hovering its controls: a script with nothing that ran is not needed for what you are rebuilding;
60
- - every interactive element's handler, with the original code around it (`click → engine.js:412` and the twenty lines there), plus the delegated handlers on `body`/`html`: **read the rule, write your own**. When `code.md` has the handler, copy the rule (its constants, its conditions), never guess it from recorded effects;
61
- - the endpoints that exist in the code, fired during the capture or not (a form's POST, a filter's fetch): they go into the report's "Not copied";
62
- - original sources when the site publishes source maps (most don't);
63
- - what REA could not see (delegated handlers through a framework, minified names) under Unknowns.
64
-
65
- For the part no capture or listener reaches (a bundled physics rule, a shader, a module's data flow), REA's own CLI is one command away: `npx trueclone rea --help`. Record what you learn in `notes.md` under Behaviours and Unknowns.
66
-
67
- **`notes.md` is a contract**, not a diary. The seed writes it with six sections, and `REPORT.md` reads two of them back:
68
- - Stack, Behaviours (one line per motion entry and interaction: element, trigger, property, range, timing), Own-code map (file → behaviours → where each parameter came from), Unknowns;
69
- - **Findings**: defects in the tool you met (symptom, cause, workaround). They appear in `REPORT.md` and are how the tool gets fixed;
70
- - **Accepted differences**: what cannot or should not match, one per line with the why (`detail desktop hero chips: random walk on both sides`). A check below target that is covered by an entry naming it (and its width) is marked "accepted" in `REPORT.md` instead of "below target". Every entry needs a reason that would convince the site's owner; an entry without one is a gap you are hiding.
49
+ `states`, `motion` and `spec` only read the reference: run them one after another in the background while you read.
50
+ Capture shoots five sizes: the seed sizes 390×844, 768×1024, 1440×900 (the clone is built from these) and the
51
+ held-out 1280×720 and 1920×1080 (scored, never used to build). It also reads inline styles at extra sizes inside each
52
+ of the site's breakpoint ranges, and saves every script that ran (`reference/scripts/`). If the page is blank or
53
+ blocked, capture through your own Chrome over CDP (`--cdp`); never get around a bot check.
54
+
55
+ ## 2. Understand
56
+
57
+ Write `notes.md` (the seed creates it; its sections are a contract): **Stack**, **Behaviours** (one line each:
58
+ element, trigger, property, range, timing, *and the file:line of the rule in reference/scripts/*), **Own-code map**,
59
+ **Findings**, **Accepted differences**, **Unknowns**, **Review**.
60
+
61
+ - `reference/scripts/INDEX.md`: the scripts by what ran, readable copies of minified ones, every listener's handler.
62
+ This is where a scene's algorithm is: a grid computed from the viewport, a spring, a pin length, a hover panel.
63
+ - `reference/script-owned.json`: every value the site's scripts set that no function of the screen size explains,
64
+ with its samples at many sizes (random, content-fitted, state- or scroll-driven); `reference/inline-styles.json`
65
+ `scriptQueries`: the widths the scripts themselves switch at (`matchMedia`).
66
+ - `reference/motion.<size>.md`: ScrollTriggers, tweens, easings, pins, as the runtime reports them.
67
+ - `reference/states/<size>.summary.json`: reveals, parallax, pinned and fixed elements; `npx trueclone tiles <name>`
68
+ cuts the full-page shots into readable screens.
69
+ - For a part no script reading explains (a bundled physics rule, a shader, WASM), REA is bundled:
70
+ `npx trueclone rea --help`. Rarely needed.
71
71
 
72
72
  ## 3. Design system
73
73
 
74
74
  ```powershell
75
- npx trueclone design <name> # drafts design-tokens.json if missing, renders DESIGN.md, globals.css, clone/tokens.css + lints
76
- npx trueclone design <name> --redraft # fresh draft in design-tokens.draft.json (never overwrites your edits)
75
+ npx trueclone design <name> # design-tokens.json -> DESIGN.md (must lint with 0 errors), globals.css, clone/tokens.css
77
76
  ```
78
77
 
79
- Review the draft: role names, the `notes` prose (overview, do's and don'ts), anything marked `origin: generated`. Re-run after every edit. `DESIGN.md` must lint with **0 errors**.
78
+ Review the draft (role names, notes, anything `origin: generated`) and re-run after every edit.
80
79
 
81
- ## 4. Build (`clone/`)
82
-
83
- **Start every clone from the mirror** (both modes):
80
+ ## 4. Build
84
81
 
85
82
  ```powershell
86
- npx trueclone reveals <name> # records, at all three widths: scroll reveals frame by frame, scroll-linked curves (parallax), pins, class changes
87
- npx trueclone widgets <name> # clicks every tab and menu and records each state (they often aren't in the page until clicked)
88
- npx trueclone seed <name> # clone/ from the script-free mirror; replays what reveals and widgets recorded
83
+ npx trueclone reveals <name> # one-shot scroll reveals and scroll class changes, recorded at the seed sizes
84
+ npx trueclone widgets <name> # every tab and menu state (template mode replays them)
85
+ npx trueclone seed <name> # clone/ from the script-free mirror
89
86
  ```
90
87
 
91
- - **Exact mode:** the site's own markup, text, stylesheets and media at local paths. The mirror fetches media the capture never saw (lazy images, hover and state images, fonts behind `@import`), so nothing points at the site.
92
- - **Template mode:** the same markup and stylesheets, but every text becomes lorem words of the same laid-out width (lines break where the reference's break), every image, video, large SVG and iframe a same-size placeholder, CSS backgrounds a transparent one, every link `#`, brand metadata and tracking hints dropped. No text or media of the site ends up in `clone/`; fonts stay for local study (swap licensed fonts before reusing the result).
93
- - **Recorded behaviour comes with it:** one-shot scroll reveals replay their measured curve (delay, duration, easing, spring overshoot) with the Web Animations API; scroll-linked curves (parallax, a scrubbed fade) follow the scroll position through the recorded samples; scroll class changes (a header that gets its backdrop past 12px) toggle the same classes at the same point. Every record exists per width, and the player uses the visitor's width, so phone offsets are never desktop pixels. All of it lives in `clone/trueclone-reveals.js`; don't rewrite it by hand. When your own script must drive an element the player also drives (a hero scene you rebuild whole), hand it over: `el.removeAttribute('data-tc-reveal')` / `'data-tc-scrub'` / `'data-tc-class'` at the start of `script.js`; the player leaves it alone and its recorded starting state (a stylesheet rule on the mark) goes with the mark. Never let both run on one element.
94
- - **Runtime artefacts are cleaned:** the mirror is a snapshot taken after the site's scripts ran. The seed turns every GSAP pin into its CSS equivalent, a `position: sticky; top: 0` element inside a spacer whose padding is the pin distance (`[data-tc-pin-spacer]` around `[data-tc-runtime="pin"][data-tc-pin="<px>"]`), so layout and scroll states hold with no script. Animate inside it as it is; if you must recreate the pin with ScrollTrigger (`pin: true`), unwrap the spacer and clear the sticky style first, or the page gets both. `reveals` lists transform-driven pins with their start and end positions. Every `<canvas>` is marked `data-tc-runtime="canvas"` (its buffer size is dropped when CSS sizes it; your scene sets it). Regions a script builds at runtime (a grid a hero script assembles) are captured in their built state: your script owns them, so rebuild them from the captured markup rather than beside it.
95
- - **Per-width values come with it:** the capture reads every inline style at all three widths. What a script set differently per width (a container's computed height, a card's offset, a custom property, a pin distance) becomes a media-query rule on `[data-tc-w]` elements (`<style id="tc-width-styles">`), and a pin whose distance changes per width carries `data-tc-pin-desktop/tablet/mobile`. Open shadow roots (web components, a Webflow code island) are carried as declarative shadow DOM (`<template shadowrootmode="open">` inside the host), so they render with no script.
96
- - **Recorded widgets come with it too** (template mode): every tab set and expandable menu, with each state the original showed after a click, placeholdered like the page. `clone/trueclone-widgets.js` swaps the region on click (menus in a header or nav also on hover). Popovers rendered in a portal (comboboxes, tooltips) are listed as not replayed: build those in `script.js`. In interaction scenarios, `selector@n` picks the n-th match in `at`, `click` and `expectChange`; mind hidden duplicates (a mobile menu button is often the first match).
97
-
98
- **Never edit `clone/index.html` by hand.** It is seed output and every re-seed rewrites it. Anything a clone needs on top of the seed goes in `clone/patches/`, applied by the seed in name order: `head-*.html` before `</head>`, `*.css` linked before `</head>` (after the site's stylesheets), `body-*.html` before the clone's own `<script src="script.js">` (vendored libraries, a hand-over prelude), `*.js` as a deferred script before `script.js`. A fix that needs the generated markup to change is a missing seed feature: report it in `notes.md` under Findings.
99
-
100
- Then write `clone/script.js`: every remaining interaction in your own code, using the parameters you read from the reference (timings, physics constants, offsets, breakpoints). Never copy the site's scripts. JS-set initial states, such as a `<details>` closed on phones, are part of the layout, so check every viewport. Data the page fetches live (a star count, a feed) stays as captured; no requests to third parties.
101
-
102
- A seed is where layout ends up at 99%+ with zero hand-written CSS; your work is behaviour. Build by hand (below) only what the mirror can't carry.
103
-
104
- After each build round, `npx trueclone owned <name>` lists the sections your script rebuilds at boot (the clone loaded with scripts blocked vs running). Those are yours: hand their marks over to `script.js` and hide them until boot if the original hides them too. Everything else stays with the seed's players.
105
-
106
- **Building parts by hand:**
107
-
108
- - `styles.css` starts with `@import url("tokens.css");` and uses its variables. Never hard-code a token value.
109
- - Use the site's breakpoints and root font-size behaviour (in `design-tokens.json`, under `layout`).
110
- - Match `sections.json` heights. In template mode write your own placeholder words, then run `npx trueclone textshape <name> --write` (desktop, then `--viewport tablet` and `mobile`) so lines break where the reference's break. Use `--list "<ref row>" "<clone row>"` for repeated lists (calendar rows, cards), and give every data row its own placeholder text.
111
-
112
- **Both modes:**
113
-
114
- - **Copy the reference's CSS mechanism, not its look.** Before writing CSS for an element, run `npx trueclone rules <name> "<selector>"` and port the matching rules (with their `@media`). Look-alikes drift in flex/grid: a "capsize" `::before/::after` text trim is not `text-box-trim`, `height: 14.5rem` is not `flex: 1`, and `width: 100%; max-width: 40rem` with default flex shrink is not `flex: 0 10 auto` or `width: min-content`.
115
- - **Positional selectors:** `:nth-of-type` counts siblings of the same tag only. A logo `<a>` before the column `<div>`s shifts every column by one, so use `:nth-child` or a class.
116
- - **Stacked media:** keep every layer (colour, grayscale or depth-map images, overlays). Never flatten them into one image.
117
- - **Canvas/WebGL:** a site can draw its hero or a game on a `<canvas>`. Rebuild the drawing in your own code with the reference's parameters (exact mode) or a placeholder scene. Never drop it silently.
118
- - **Motion:** implement every inventory entry: reveals, parallax speeds, pinned sections, fixed elements.
119
- - **Interactions:** sliders, tabs, hover states, cursor followers, menus, counters, demos. Scope query selectors to their own component: a hidden duplicate (a mobile-only list) must never feed a desktop counter.
120
- - **Sliders in flex/grid:** give the slider and every flex/grid ancestor `min-width: 0`, or Swiper and similar grow to fit their slides and run away. `states --clone` warns when the page height drifts more than 20%.
121
- - `<meta name="robots" content="noindex">`, forms submit nowhere, no tracking scripts.
88
+ What the seed gives you, and what it leaves to you:
89
+ - the original's markup, stylesheets and media (template mode: lorem text of the same laid-out width, same-size
90
+ placeholders), open shadow roots as declarative shadow DOM, missing media fetched;
91
+ - **inline values a script set, as functions of the screen size** (`calc()` in `--tc-vw`/`vh`, or `max()`/`min()` for
92
+ cover/contain, per range of the site's and its scripts' breakpoints) when one fits every captured size; a property
93
+ that fits at no size or only at some is removed everywhere and marked `data-tc-script-owned`;
94
+ - **pins** as a sticky element in a spacer whose tail is the pin length (`--tc-pin`, a function of the viewport when
95
+ the site's is);
96
+ - one-shot reveals and scroll class changes replayed by `trueclone-reveals.js` at the visitor's size; **scroll-linked
97
+ curves (parallax, scrubbed fades) are not replayed**: their samples are evidence in `reference/reveals.<size>.json`,
98
+ and you write the rule;
99
+ - the boot contract: `<html class="tc-boot">` until your scripts have run; `--hide-until-boot "<selectors>"` keeps
100
+ regions your script builds invisible meanwhile; `tcBoot.hold()` / `tcBoot.release()` for an intro that waits.
101
+
102
+ Then `clone/script.js`, scene by scene from `notes.md` Behaviours: find the rule in `reference/scripts/`, port it,
103
+ compute everything from the viewport. When your script drives an element the seed's player also drives, hand it
104
+ over: remove its `data-tc-reveal` / `data-tc-class` mark at the top of `script.js`. After each build round,
105
+ `npx trueclone owned <name>` lists the sections your script rebuilds (hide those until boot if the original hides them).
106
+
107
+ `clone/patches/` (applied on every seed, or alone in a second with `npx trueclone seed <name> --patches`):
108
+ `head-*.html` before `</head>`, `*.css` linked after the site's stylesheets, `body-*.html` before `script.js`
109
+ (vendored libraries, a hand-over prelude), `*.js` deferred before `script.js`. Vendor libraries at the site's versions.
110
+
111
+ Copy the reference's CSS mechanism, not its look: `npx trueclone rules <name> "<selector>"` (add `--clone` for yours).
112
+ Canvases are scored: rebuild the drawing from the site's scene code with its parameters (exact mode) or as your own
113
+ scene with the same composition (template mode); never drop one.
122
114
 
123
115
  ## 5. Verify
124
116
 
125
117
  ```powershell
126
- npx trueclone score <name> --quick # layout, desktop states, interactions: the loop while building (about a minute)
127
- npx trueclone score <name> # everything, in parallel groups, then REPORT.md
128
- npx trueclone score <name> --only detail,load --viewport desktop # after a fix: just the checks it can move, at that width
129
- npx trueclone explain <name> <y> # one place on the page: what every check saw there + a reference|clone|diff crop
118
+ npx trueclone score <name> # every check at every scored size, then REPORT.md
119
+ npx trueclone score <name> --only layout,states --viewport wide # after a fix: the checks it can move, where
120
+ npx trueclone explain <name> <y | "section label"> --viewport wide # one place: what every check saw + a crop
121
+ npx trueclone review <name> --size wide # side by side by eye; then laptop, tablet, mobile
130
122
  ```
131
123
 
132
- The steps, if you need one alone:
133
-
134
- ```powershell
135
- npx trueclone diff <name> [--structure] # layout: full page, frozen (structure = text and media masked)
136
- npx trueclone states <name> --clone [--structure] # scroll states: the reference's scroll positions replayed on the clone
137
- npx trueclone curves <name> --draft # motion-map.draft.json from the inventory: fill in clone selectors, save as motion-map.json
138
- npx trueclone curves <name> [--viewport tablet] # motion: property curves (scroll- and time-driven), reference vs clone
139
- npx trueclone interact <name> --draft # interactions.draft.json: fill in TODOs, save as interactions.json
140
- npx trueclone interact <name> # interactions: click/hover/drag/key scenarios + expectChange (geometry, text, state attributes, canvas pixels)
141
- npx trueclone detail <name> --viewport all # styles at matching points: font, size, weight, line height, colour, boxes
142
- npx trueclone hovers <name> # hover effects present on the reference but missing on the clone
143
- npx trueclone load <name> # load sequence: how settled each side is 0.25-4 s after navigation (cold cache, same network)
144
- ```
145
-
146
- Reference measurements are cached until the next capture, so only the clone is re-measured. Sites without scroll/time motion or without interactions simply skip those steps.
147
-
148
- **Spend scoring time only on what changed.** The first run is a full `score`. After a fix, re-run only the checks it can move, at the width it concerns: `npx trueclone score <name> --only layout,detail --viewport tablet`. A check whose inputs (the clone's files, the capture) have not changed since its last run is reported from that run and not re-measured (`--fresh` forces it). `REPORT.md` shows where the scoring time went. To understand one gap, run `npx trueclone explain <name> <y or "section label"> --viewport <vp>`: it prints what layout, states, detail and hovers saw there and writes a reference | clone | diff crop, so you read the tool's data instead of re-measuring with a browser.
149
-
150
- **Unstable probes are not your gaps.** Detail probes and hover effects are read twice on the reference; what does not agree with itself (random motion, a hover that lands by timing, a rotating word) is listed as unstable and excluded from the score. The load check reports its own noise (±). Do not chase a number inside the noise: a gap of 0.3 on a check with ±1 noise is not a gap.
151
-
152
- Fix in this order: page height drift, the worst layout section, the worst state step, the lowest motion curve, any interaction marked FAIL, the load sequence, then detail and hovers. `REPORT.md` lists them under "Fix first". For a drift, compare the element's ancestor boxes on both sides until one differs, then `rules` that ancestor: the cause is usually one declaration. Fix the cause; never tune numbers to the score.
153
-
154
- **Done when:** layout ≥ 98% at every viewport, states within 5 points of layout, motion ≥ 95%, detail ≥ 95%, hovers ≥ 90%, load ≥ 95%, and every interaction passes. Otherwise stop after 8 passes and report what's left and why.
155
-
156
- **The load sequence is scored too.** A visitor's first impression is the load, so both pages are photographed 0.25 to 4 s after navigation and compared with their own settled frame. A clone that paints a captured, pre-script state (a hero grid a script builds, a card layout a script places) until its scripts run shows a gap here that no other check sees. The seed's boot contract handles it: `<html class="tc-boot">` from the first byte until the clone's scripts have run and every hold is released; elements marked `data-tc-boot-hide` stay invisible meanwhile (3 s failsafe). Pass the regions your script builds at boot to the seed: `npx trueclone seed <name> --hide-until-boot "<selectors>"`. If your script needs more than "has run" (an intro that waits for its image), call `tcBoot.hold()` at the top of `script.js` and `tcBoot.release()` when the first frame is ready. Hide only what the original also does not show before its scripts run (its first frames are in `reference/load/`); hiding more than the original hides is a gap of its own.
157
-
158
- How detail measures, so you can trust it:
159
- - motion on (many originals ignore `prefers-reduced-motion`, and a clone that honours it lays out differently);
160
- - both sides measured the same way: a hit test at the same point and scroll position, so sticky and pinned content is compared in place and an identical page scores 100%;
161
- - through `pointer-events: none` overlays, and never on content the browser doesn't paint (closed `<details>`);
162
- - colours written by scroll-scrubbed JS show up as in-between values: that is a motion difference, so check the motion inventory.
124
+ The checks: layout (full page, structure), scroll states (every half screen, scrolled with the wheel), canvases (what
125
+ each scene draws), motion curves, detail (styles at matching points), hovers (every control and every element with a
126
+ hover listener: the region after the hover must match), interactions (`interactions.json`: the change must happen on
127
+ both sides and end in the same state), load (0.25-4 s after navigation, cold cache, same network). Unstable probes
128
+ (random motion, timing) are measured twice on the reference and excluded; do not chase a number inside the noise.
129
+ Unchanged checks are not re-measured (`--fresh` forces it). `drift` opens every full score: if the live site changed
130
+ since the capture, the live-measured checks are marked stale; re-capture before trusting them.
163
131
 
164
- How layout and states measure:
165
- - the structure mask hides text everywhere, gradient text (`background-clip: text`) and `::before/::after` photo backgrounds included;
166
- - content that a component mounts only while in view (marked `data-tc-viewmount` by capture) can't appear in a full-page still on either side: layout paints its box the same on both and reports the count; the scroll-state check, with a real viewport, scores it;
167
- - endless animations (marquees, rotating borders) are frozen on their first frame on both sides before every state screenshot: their phase depends on load time, not on the clone.
132
+ Fix in this order: anything the review shows, the held-out sizes, page height drift, the worst layout section, the
133
+ worst state step, canvases, interactions, load, then detail and hovers. Fix the cause; never tune a number to a score.
168
134
 
169
- The live site can change after capture (a redeploy, a new nav item, a rotating headline). Every full score starts with `drift`: the live page's height and sections against the capture's. When it reports a change, `REPORT.md` marks detail, hovers, motion, interactions and load **stale capture**: they now measure a page the clone was never a copy of. Re-capture (`capture`, then `states`, `reveals`, `seed --force`) before reading them; never fix the clone against a stale reference.
135
+ **Review verdicts** in `notes.md` under `## Review`, one per frame of `compare/review/<size>/index.md`:
136
+ `- 03-section-2 wide: same` or `- 07-section-4-hover wide: differs: the panel opens on the wrong card`.
137
+ **Accepted differences**: `- <check> <size> <where>: <why>; evidence: compare/explain/wide-5200.png`, only for a check
138
+ already ≥ 95.
170
139
 
171
140
  ## 6. Next.js (after the HTML clone passes)
172
141
 
173
142
  ```powershell
174
- npx trueclone next <name> --verify # generates sites/<name>/next, installs, builds, serves, and runs every score on it
143
+ npx trueclone next <name> --verify # generates next/, builds, serves and scores it into REPORT.next.md
175
144
  ```
176
145
 
177
- The HTML clone stays the source of truth: change `clone/`, then regenerate. The app has one component per section, the motion script as a client component (GSAP, Lenis, Swiper and three from npm at the clone's versions), `app/globals.css` = shadcn theme + tokens + the clone's stylesheet + preflight parity, and `components.json` so `npx shadcn add <component>` works. It is done when its scores match the HTML clone's within a point.
146
+ The HTML clone stays the source of truth: change `clone/`, then regenerate. One component per section, the clone's
147
+ scripts preloaded and run at hydration, the site's stylesheets after the shadcn theme in the site's cascade-layer order,
148
+ `components.json` for `npx shadcn add`. Done when its report matches the HTML clone's within a point everywhere.
178
149
 
179
150
  ## 7. Report
180
151
 
181
- `npx trueclone score <name>` writes `REPORT.md` (or run `npx trueclone report <name>`): mode and permission, every score against its target, "Fix first", the motion found on the reference, and what was not copied (server behaviour, tracking, and each data/API call the reference makes). Hand it over with the clone.
152
+ `REPORT.md` (and `REPORT.next.md`) is the deliverable: mode and permission, every check at every size, the review,
153
+ open frames, accepted differences with evidence, Findings, what to fix first, and what was not copied (server
154
+ behaviour, tracking, each data/API call). Hand it over with the clone.
182
155
 
183
156
  ## Honest limits
184
157
 
185
- - Server-side behaviour is never visible from outside: databases, accounts, payments, search. The clone reproduces the client; data calls are listed in the report.
186
- - A live site is never pixel-identical twice: rotating content, A/B tests and font hinting vary.
187
- - Template clones of other people's sites stay local. Exact clones need recorded permission and also stay local unless the owner deploys them.
158
+ - Server-side behaviour is never visible from outside: databases, accounts, payments, search. The clone reproduces
159
+ the client; data calls are listed in the report.
160
+ - A live site is never pixel-identical twice: rotating content, A/B tests and font hinting vary; that is what the
161
+ unstable-probe and noise handling is for, not a licence to stop.