Three clone-fidelity fixes from manual review of the acceptance screenshots.
Fix 1 — mobile nav chip-strip overlap (generate/css.ts). A horizontally
scrollable flex strip (overflow-x:auto flex <ul>) of nowrap chips collapsed
because the base viewport reported min-width:0px on the chips (a mobile-only
strip that is 0-wide at desktop), so the generator emitted `min-w-0`, letting
the chips shrink below their content instead of overflowing — the nowrap text
then collided. Suppress `min-w-0` for a nowrap flex item whose flex parent
scrolls horizontally; scoped away from legitimate min-w-0 truncation (whose
parent does not scroll-x). Verified on a fresh ooni.com clone: 0 chip overlaps
at 375, scrollWidth 776 > clientWidth 375.
Fix 2 — reveal-replay stagger leaves tiles unpainted (generate/motion.ts).
DittoMotion re-hid each revealed element and replayed its entrance on
scroll-into-view preserving the captured delay/duration, so a fast scroll /
full-page screenshot caught most grid tiles mid-entrance. Cap the replayed
delay (<=300ms) and duration (<=600ms), settle each tile per-element a bounded
time after it enters view, and make the global failsafe settle (not re-animate)
so nothing stays hidden. Validator settle path (__dittoMotionStop) unchanged.
Verified: cropin cotton renders 12/12 crop tiles in a normal full-page pass;
motion gate reveals 24/24.
Fix 3 — scroll-linked text-fill frozen at end state (capture/stabilize.ts,
capture/capture.ts, generate/css.ts). A view-timeline text-fill
(animation-duration resolves to `auto`) was baked filled: the clone has no
scroll timeline, so emitting the animation made it jump to its end keyframe via
fill-mode:both. Capture-side, cancel scroll/view-timeline animations before each
snapshot so the DOM records the at-rest state; generation-side, suppress the
animation-* props when animation-duration is `auto` (we render the at-rest
state, never replay a scroll-linked animation). Teach the motion gate to exclude
these from the static-CSS expectation so the gate stays honest. Verified on a
fresh ooni clone: the "world's no.1 pizza ovens" em is white/gray (rgb 240,240,
240), not filled yellow, at rest; motion gate still passes.
Tests: +7 fixtures (css scroll-strip + scroll-timeline, reveal-replay caps,
motion-gate scroll-timeline exclusion); 123/123 compiler tests green, full test
suite green, typecheck clean. No gate regression on the fresh ooni run vs the
20260704-024247 baseline (all gate pass/fail unchanged; perceptual within
0.0004; motion restored to pass).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- motionGate: a captured CSS entrance animation the clone replays through
a DittoMotion reveal (re-hide on mount, restart @keyframes on scroll)
is graded by the scroll-reveal check, not the static-CSS expectation —
previously every such cid failed emitted=false. Also drive every
captured reveal instead of the first 16 (pages with more reveals could
never pass; this was the spurious "reveals 16/24").
- render/harness: the validator copies app sources over the preinstalled
harness without installing deps, so a Lottie clone's injected
lottie-web crashed webpack ("build failed" with no real signal).
Missing app deps are now installed into the harness pinned to the
app's exact version (--no-save, idempotent), with a clear gate issue
on install failure.
Cotton motion gate: 33 static + 10 replayed + 24/24 reveals -> pass.
Cropin home build gate: pass (was webpack Module not found).
116/116 compiler tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-3 fixes from gate validation + human visual review of fresh
cropin.com and ooni.com clones:
- app.ts: section data-var rename used an unanchored substring replace,
so Tile2_data matched inside MediaTile2_data and emitted a corrupted
Mediatile2Data usage (ReferenceError at prerender). Word-boundary
regex; every map site now shares one derivation.
- graft.ts: about:blank iframes were blanket-skipped, missing the
Klaviyo newsletter form (mounted into a blank same-origin frame via
JS) — the audit's #1 complaint. Blank frames now graft; the
visibility gate still excludes 0-size tracking pixels.
- walker.ts: isVisible() now rejects boxes wholly outside the viewport
(off-left always; off-right only when the page isn't horizontally
scrollable; fixed boxes fully above/below), so a closed slide-in
drawer's contents no longer count as expected text (91.8% -> 93.4%).
- css.ts: four sizing-regime corrections — pin captured px for
circular shrink-0 slides (Splide slide chain collapsed 0x0);
flex-basis:100% for full-width shrink-0 slides; keep fixed-px grid
templates for scrolling track lists (repeat(N,1fr) squished a
50-track carousel); single full-bleed fixed track -> minmax(0,1fr);
keep authored heights whose in-flow children are fill children
(aspect-video heroes inflated 240 -> 720px); width:100% for
inset-spanned aspect boxes.
Gate scores: ooni home 88.7 -> 93.3 (responsive 32 fails -> pass),
pizza-ovens 90.8 -> 97.6 (perceptual 46.7% -> 17.2%).
107/107 compiler tests, all workspaces green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Capture (4a):
- stabilize: dwell-scroll settling pass + known-library pre-reveal
neutralization after lazy promotion, so every viewport snapshot records
the post-reveal steady state (about-page sections no longer captured
blanket-hidden: 24 base `invisible` wrappers -> 0)
- probeReveals: extended to the visibility + entrance-animation family;
DittoMotion replays them (JS re-hide on mount, IntersectionObserver
reveal, 4s force-reveal failsafe) so SSR/non-JS still shows content
- assets: 206 range-response bodies are no longer stored as full assets
(full-fetch fallback) + video magic-byte validation - the about-page
hero video is now a valid 6.0MB ftyp mp4 instead of 18.9KB of garbage
Generate (4b):
- css: hidden-node geometry policy - a visibility:hidden box with a 0x0
captured bbox at a band becomes display:none there; an OCCUPYING hidden
box gets the captured per-viewport geometry instead of the baked
canonical one. Also covers the ancestor-hidden-at-base case (the
cropin.com/cotton slider arrow: hide inherited from an elementor-widget
ancestor at every width parked a desktop left:548px box inside a 375px
viewport -> body.scrollWidth 585; now 375 on both verified pages)
- tailwind: bare-zero utility gating - font-size:0 -> text-[0px] and
letter-spacing:0 -> tracking-[0px] (text-0/tracking-0 are silently
invalid in Tailwind v4; fixes permanently-visible map labels on the
cotton Global-presence section)
Verified: 87/87 compiler tests green, full-workspace suite green,
regenerated cropin.com + /cotton/ builds measure 375px scrollWidth at a
375px viewport, and a fresh /about/ clone renders all reveal sections
with scroll-replay against the live site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A successful clone printed a bare, timestamped app path that wrapped mid-word
in the terminal, so users' `cd` failed and `npm install && npm run dev` ran in
the wrong directory — a confusing failure from a successful clone. Plus there
was no first-party "see it locally" path, no central docs page, "clone" was
conflated with `git clone`, and key hygiene wasn't stated.
CLI (compiler):
- Add compiler/src/cliSummary.ts: a copy-paste-safe success block on stderr — a
single QUOTED `cd "<app>" && npm install && npm run dev` line that survives
terminal wrapping — plus the AGENTS.md safe-to-edit pointers (content.ts,
components/). The machine-readable {"event":"done",...} JSON stays on stdout.
- Add --serve (npm install + npm run dev in the generated app) and --open
(also open the browser at the detected dev URL). Wired into single + multi.
- Add writeLatestPointer(): refresh a runs/<site>/latest symlink to the newest
run so there's a stable, timestamp-free cd/script target. The reuse/regen
scanners already filter to digit-prefixed run dirs, so `latest` is ignored.
- Tests: cliSummary (quoting, single-line command, Next/Vite roots, stable-path
preference, --serve/--open hint) and runsLayout (symlink write + in-place
refresh, timestamp-free stable path).
Docs:
- Add docs/README.md as a central documentation index.
- State prominently that ditto "cloning" = generating a codebase from a live
URL, NOT `git clone` (no source repo required), in README + docs hub.
- Add "keys are secrets: use $DITTO_API_KEY, never inline/commit, rotate
anytime" beside the key/auth examples in README + docs/SERVICE.md.
- Document --serve/--open and the latest symlink in README + compiler/README.
Note: the live ditto.site/docs web route and the dashboard's inline-token
snippet live in the marketing-site repo and still need a change there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two follow-ups to the single-page Lottie support:
1. Multi-page (clone-site). generateSiteApp emitted only DittoWire; it now wires
DittoLottie per route exactly like wires — builds the route's lottie spec,
emits the import + JSX, writes DittoLottie.tsx once, and injects lottie-web
into the site package.json when any route replays an animation (the write
moved after the route loop so the dep reflects actual usage). cloneSite now
captures motion on full route captures (motion: full) so lotties are observed;
light sibling probes skip it. Per-route gating verified: a route with no
animation gets no DittoLottie.
2. Inline animationData. When a lottie is only in memory (data-driven init, no
fetchable URL), capture now writes the JSON to the assets store as a real,
content-hashed .json so it materializes to /assets/cloned/lottie like any
source — instead of embedding the blob in the page spec. buildLottieSpec then
resolves it to a local path; the spec carries path, not animationData.
Verified end-to-end: a 2-page fixture emits DittoLottie only on the animated
route with lottie-web added; a data-driven inline fixture materializes
fd7c…json and the spec references it by path.
Co-Authored-By: claude-flow <ruv@ruv.net>
lottie-web renders vector animations from a JSON document at runtime. As
third-party JS it falls outside the declarative CSS path and the WAAPI / reveal /
marquee probes, so clones shipped the empty container and the animation was gone.
This adds a Lottie subset to Stage 5 motion: capture the deterministic part — the
source JSON (materialized local file, or inline animationData) plus playback
config — and emit a fixed DittoLottie client that re-mounts lottie-web on the
cid'd container.
Capture (compiler/src/capture/lottie.ts): in-page detector covering lottie-web's
registry (animationData in memory), <lottie-player>/<dotlottie-player>, Elementor
data-settings (source_json.url), and generic markup. Keyed by data-cid-cap.
motion.ts merges it into MotionCapture; capture.ts registers source URLs as assets
so the existing fallback fetch downloads + materializes them to /assets/cloned/lottie.
Generate (compiler/src/generate/lottie.ts): buildLottieSpec resolves cap->cid and
source URL->materialized local path (or embeds inline animationData), then emits
DittoLottie + wire/import helpers, mirroring DittoMotion. app.ts writes the
component, wires import+JSX, and injects lottie-web into the generated package.json.
Delivery (compiler/src/cli.ts): stripDeliveryDataCids now treats DittoLottie as a
runtime consumer, so its container keeps a semantic data-ditto-id anchor instead of
being stripped (without this the animation never finds its mount point).
Lottie-free clones stay byte-identical. Verified end-to-end: cloning a page with a
<lottie-player> scrapes the JSON, emits a wired DittoLottie, and lottie-web mounts
and plays the captured animation (rendered SVG viewBox matches the source JSON).
Unit tests cover buildLottieSpec resolution + drop rules.
Co-Authored-By: claude-flow <ruv@ruv.net>