From 199359d0a677699fb24dfed4f1ac7305ed9d30b3 Mon Sep 17 00:00:00 2001 From: Samraaj Bath Date: Fri, 3 Jul 2026 18:33:10 -0700 Subject: [PATCH] Wave 4: reveal settling + replay, 206/magic-byte media fix, hidden-geometry banding, bare-zero utility gating 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 --- compiler/fixtures/reveal.html | 49 ++++++++++++ compiler/src/generate/css.ts | 47 ++++++++---- compiler/test/css.test.ts | 133 ++++++++++++++++++++++++++++++++ compiler/test/range206.test.ts | 102 +++++++++++++++++++++++++ compiler/test/reveal.test.ts | 135 +++++++++++++++++++++++++++++++++ compiler/test/tailwind.test.ts | 43 +++++++++++ 6 files changed, 494 insertions(+), 15 deletions(-) create mode 100644 compiler/fixtures/reveal.html create mode 100644 compiler/test/range206.test.ts create mode 100644 compiler/test/reveal.test.ts create mode 100644 compiler/test/tailwind.test.ts diff --git a/compiler/fixtures/reveal.html b/compiler/fixtures/reveal.html new file mode 100644 index 0000000..70ecc9a --- /dev/null +++ b/compiler/fixtures/reveal.html @@ -0,0 +1,49 @@ + + + + +Reveal fixture + + + +

Reveal fixture

+
+ +
+

Scroll-revealed content (IntersectionObserver)

+
+
+ +
+

Reveal keyed to a non-scroll trigger

+
+ +
permanently hidden
+ + + diff --git a/compiler/src/generate/css.ts b/compiler/src/generate/css.ts index 5df215c..b343609 100644 --- a/compiler/src/generate/css.ts +++ b/compiler/src/generate/css.ts @@ -3012,24 +3012,41 @@ export function collectNodeRules(ir: IR, assetMap: Map, includeN // (declsForViewport emits `visibility:hidden`; parent-equality keeps it own-only). } else if (ownNone || !node.visibleByVp[b.vp]) { const shownAtBase = node.visibleByVp[baseVp] && (node.computedByVp[baseVp]?.display || "") !== "none"; - if (ownNone) { - // Own display:none removes the box from layout entirely. Emit the hide even when the node - // is ALSO hidden at base — a visibility:hidden base still bakes an OCCUPYING box (see - // above), so without this band the canonical geometry would render at this width. Skip - // only when the base itself is display:none (the band would be redundant). - if ((node.computedByVp[baseVp]?.display || "") !== "none") { + // Hidden by an ANCESTOR's visibility here AND at base (the ancestor's own rule carries + // the hide at every width) — but a visibility:hidden box still PARTICIPATES in layout, + // and the base rule bakes CANONICAL geometry. Same policy as the own-hidden path above: + // a 0x0 box gets display:none; an occupying box falls through to the per-viewport delta + // so it sits where the capture measured it at THIS width — not parked at e.g. a desktop + // left:548px inside a 375px viewport (the cropin.com/cotton slider arrow, +210px of + // sideways scroll at 375 with the hide inherited from an elementor-widget ancestor). + const ancestorHiddenHere = !ownNone && /^(hidden|collapse)$/.test(vpCs.visibility || ""); + if (ancestorHiddenHere && !shownAtBase) { + const bb = node.bboxByVp[b.vp]; + if (!bb || bb.width <= 0 || bb.height <= 0) { nr.bands.push({ media: b.media, decls: new Map([["display", "none"]]) }); + continue; } - } else if (shownAtBase) { - // Hidden by an ancestor (or zero-size / opacity:0): geometry overrides are breakpoint - // noise — the ancestor's own hide (or the reveal replay, for scroll-reveal opacity) - // covers it. Emit only the hide the node itself carries. - const hide = new Map(); - if (pf(vpCs.opacity) === 0 && !animOwned.has("opacity")) hide.set("opacity", "0"); - if (/^(hidden|collapse)$/.test(vpCs.visibility || "")) hide.set("visibility", "hidden"); - if (hide.size) nr.bands.push({ media: b.media, decls: hide }); + // Occupying: fall through to the normal per-viewport delta below. + } else { + if (ownNone) { + // Own display:none removes the box from layout entirely. Emit the hide even when the node + // is ALSO hidden at base — a visibility:hidden base still bakes an OCCUPYING box (see + // above), so without this band the canonical geometry would render at this width. Skip + // only when the base itself is display:none (the band would be redundant). + if ((node.computedByVp[baseVp]?.display || "") !== "none") { + nr.bands.push({ media: b.media, decls: new Map([["display", "none"]]) }); + } + } else if (shownAtBase) { + // Hidden by an ancestor (or zero-size / opacity:0) but visible at base: geometry + // overrides are breakpoint noise — the ancestor's own hide (or the reveal replay, + // for scroll-reveal opacity) covers it. Emit only the hide the node itself carries. + const hide = new Map(); + if (pf(vpCs.opacity) === 0 && !animOwned.has("opacity")) hide.set("opacity", "0"); + if (/^(hidden|collapse)$/.test(vpCs.visibility || "")) hide.set("visibility", "hidden"); + if (hide.size) nr.bands.push({ media: b.media, decls: hide }); + } + continue; } - continue; } const centeredVp = stableCenter || (layoutParent ? centeredAtVp(node, layoutParent, b.vp) : false); const vpDecls = finalizeDecls(declsForViewport(node, parentNode?.computedByVp[b.vp], b.vp, assetMap, centeredVp, colorVar, ir.doc.perViewport[b.vp]?.scrollHeight, widthPlan, gridColsByVp?.get(b.vp), gridRowsByVp?.get(b.vp), flowH, dropInsets, leftPct, heightFill, geometry, dropGridRows, dropViewportMaxWidth), tokenResolver); diff --git a/compiler/test/css.test.ts b/compiler/test/css.test.ts index 7877602..ad68257 100644 --- a/compiler/test/css.test.ts +++ b/compiler/test/css.test.ts @@ -100,3 +100,136 @@ describe("generateCss visibility", () => { assert.ok(!baseRule(css, "n2").includes("visibility")); }); }); + +// Hidden-node banded geometry. A `visibility:hidden` box still PARTICIPATES in layout (unlike +// `display:none`), so the emitter must not let the base rule's baked CANONICAL geometry stand at +// widths where the capture measured something else — that is how a desktop `left:548px` slider +// arrow ends up parked, invisibly, 210px past the right edge of a 375px viewport. +describe("generateCss hidden-node banded geometry", () => { + const HVPS = [375, 1280, 1920]; + type VpState = { cs?: StyleMap; bbox?: BBox; visible?: boolean }; + function nodeAt(id: string, tag: string, byVp: Record, children: IRChild[] = []): IRNode { + const computedByVp: Record = {}; + const bboxByVp: Record = {}; + const visibleByVp: Record = {}; + for (const vp of HVPS) { + const s = byVp[vp] ?? {}; + computedByVp[vp] = computed(s.cs); + bboxByVp[vp] = s.bbox ?? { x: 0, y: 0, width: vp, height: 100 }; + visibleByVp[vp] = s.visible ?? true; + } + return { id, tag, attrs: {}, visibleByVp, bboxByVp, computedByVp, children }; + } + function ir3(root: IRNode): IR { + const ir = irWith(root); + ir.doc.viewports = HVPS; + ir.doc.sampleViewports = HVPS; + ir.doc.perViewport = Object.fromEntries(HVPS.map((vp) => [vp, { scrollHeight: 800, scrollWidth: vp, htmlBg: "rgb(255, 255, 255)", bodyBg: "rgb(255, 255, 255)", bodyColor: "rgb(0, 0, 0)", bodyFont: "Arial" }])); + return ir; + } + /** The `.c{…}` body inside the first @media block whose query matches `mediaRe`. */ + function bandRule(css: string, mediaRe: RegExp, id: string): string { + for (const m of css.matchAll(/@media ([^{]+) \{\n([\s\S]*?)\n\}/g)) { + if (!mediaRe.test(m[1]!)) continue; + const r = m[2]!.match(new RegExp(`\\.c${id}\\{([^}]*)\\}`)); + if (r) return r[1]!; + } + return ""; + } + const arrowCs = (left: string, hidden: boolean): StyleMap => + ({ display: "flex", position: "absolute", left, ...(hidden ? { visibility: "hidden" } : {}) }); + + it("emits the captured per-band geometry for a box its own visibility:hidden leaves occupying space", () => { + // Hidden at base AND at the mobile band with DIFFERENT lefts (the swiper-arrow shape): the + // band must carry the mobile left, not inherit the baked canonical one. + const arrow = nodeAt("n1", "div", { + 375: { cs: arrowCs("37px", true), bbox: { x: 37, y: 0, width: 46, height: 46 }, visible: false }, + 1280: { cs: arrowCs("548px", true), bbox: { x: 548, y: 0, width: 46, height: 46 }, visible: false }, + 1920: { cs: arrowCs("588px", false), bbox: { x: 588, y: 0, width: 46, height: 46 } }, + }); + const root = nodeAt("n0", "body", { 1280: { cs: { position: "relative" } } }, [arrow]); + const css = generateCss(ir3(root), new Map()); + assert.ok(baseRule(css, "n1").includes("visibility:hidden")); + assert.ok(baseRule(css, "n1").includes("left:548px")); + const mobile = bandRule(css, /max-width/, "n1"); + assert.ok(mobile.includes("left:37px"), `mobile band should carry the captured left, got: ${mobile}`); + assert.ok(!mobile.includes("display:none"), "an occupying hidden box must stay in layout"); + // The wide band where the node becomes visible keeps working: visibility restored + its left. + const wide = bandRule(css, /min-width/, "n1"); + assert.ok(wide.includes("visibility:inherit"), `wide band should restore visibility, got: ${wide}`); + assert.ok(wide.includes("left:588px"), `wide band should carry the 1920 left, got: ${wide}`); + }); + + it("hides a visibility:hidden box whose captured bbox is 0x0 with display:none at that band", () => { + // At 375 the hidden arrow occupied NOTHING (uninitialised swiper) — display:none reproduces + // "renders nothing, takes no space" and cannot extend the scrollable area. + const arrow = nodeAt("n1", "div", { + 375: { cs: arrowCs("calc(50% - 52px)", true), bbox: { x: 0, y: 0, width: 0, height: 0 }, visible: false }, + 1280: { cs: arrowCs("548px", true), bbox: { x: 548, y: 0, width: 46, height: 46 }, visible: false }, + 1920: { cs: arrowCs("588px", false), bbox: { x: 588, y: 0, width: 46, height: 46 } }, + }); + const root = nodeAt("n0", "body", { 1280: { cs: { position: "relative" } } }, [arrow]); + const css = generateCss(ir3(root), new Map()); + const mobile = bandRule(css, /max-width/, "n1"); + assert.ok(mobile.includes("display:none"), `0x0 hidden band should be display:none, got: ${mobile}`); + const wide = bandRule(css, /min-width/, "n1"); + assert.ok(wide.includes("visibility:inherit") && wide.includes("left:588px")); + }); + + it("emits display:none at a band where the node turns display:none even when hidden at base", () => { + // The base rule bakes an OCCUPYING visibility:hidden box (canonical geometry); without the + // band that box would render at mobile widths where the source had display:none. + const wrap = nodeAt("n1", "div", { + 375: { cs: { display: "none", visibility: "hidden" }, bbox: { x: 0, y: 0, width: 0, height: 0 }, visible: false }, + 1280: { cs: { visibility: "hidden" }, bbox: { x: 40, y: 0, width: 1200, height: 574 }, visible: false }, + 1920: { cs: {}, bbox: { x: 320, y: 0, width: 1280, height: 533 } }, + }); + const root = nodeAt("n0", "body", {}, [wrap]); + const css = generateCss(ir3(root), new Map()); + assert.ok(baseRule(css, "n1").includes("visibility:hidden")); + const mobile = bandRule(css, /max-width/, "n1"); + assert.ok(mobile.includes("display:none"), `own display:none band must emit even when hidden at base, got: ${mobile}`); + }); + + it("emits per-band geometry for an occupying box an ancestor hides at base AND at the band", () => { + // The cropin.com/cotton slider arrow: the elementor-widget ANCESTOR is visibility:hidden at + // 375/1280 (so the arrow is never ownHidden and never shownAtBase) yet the arrow's absolute + // box still occupies layout. Without a band the base's canonical left:548px parks it 210px + // past the right edge of a 375px viewport — the band must carry the captured mobile left. + const arrow = nodeAt("n2", "div", { + 375: { cs: arrowCs("120px", true), bbox: { x: 112, y: 0, width: 46, height: 46 }, visible: false }, + 1280: { cs: arrowCs("548px", true), bbox: { x: 548, y: 0, width: 46, height: 46 }, visible: false }, + 1920: { cs: arrowCs("588px", false), bbox: { x: 588, y: 0, width: 46, height: 46 } }, + }); + const parent = nodeAt("n1", "div", { + 375: { cs: { position: "relative", visibility: "hidden" }, visible: false }, + 1280: { cs: { position: "relative", visibility: "hidden" }, visible: false }, + 1920: { cs: { position: "relative" } }, + }, [arrow]); + const root = nodeAt("n0", "body", {}, [parent]); + const css = generateCss(ir3(root), new Map()); + const mobile = bandRule(css, /max-width/, "n2"); + assert.ok(mobile.includes("left:120px"), `mobile band should carry the captured left, got: ${mobile}`); + assert.ok(!mobile.includes("display:none"), "an occupying hidden box must stay in layout"); + }); + + it("still emits only the hide for a box an ANCESTOR's visibility:hidden covers", () => { + // Inherited hides stay breakpoint noise: the ancestor's own rule (and its geometry + // correction) covers the subtree — the child emits its hide, not geometry overrides. + const child = nodeAt("n2", "div", { + 375: { cs: { position: "absolute", left: "10px", visibility: "hidden" }, bbox: { x: 10, y: 0, width: 40, height: 40 }, visible: false }, + 1280: { cs: { position: "absolute", left: "500px" }, bbox: { x: 500, y: 0, width: 40, height: 40 } }, + 1920: { cs: { position: "absolute", left: "500px" }, bbox: { x: 500, y: 0, width: 40, height: 40 } }, + }); + const parent = nodeAt("n1", "div", { + 375: { cs: { position: "relative", visibility: "hidden" }, visible: false }, + 1280: { cs: { position: "relative" } }, + 1920: { cs: { position: "relative" } }, + }, [child]); + const root = nodeAt("n0", "body", {}, [parent]); + const css = generateCss(ir3(root), new Map()); + const mobile = bandRule(css, /max-width/, "n2"); + assert.ok(mobile.includes("visibility:hidden"), `child should carry the hide, got: ${mobile}`); + assert.ok(!mobile.includes("left:10px"), `ancestor-hidden child must not emit geometry, got: ${mobile}`); + }); +}); diff --git a/compiler/test/range206.test.ts b/compiler/test/range206.test.ts new file mode 100644 index 0000000..767674f --- /dev/null +++ b/compiler/test/range206.test.ts @@ -0,0 +1,102 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { createServer, type Server } from "node:http"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { captureSite, looksLikeVideoFile, type CaptureResult } from "../src/capture/capture.js"; + +// A minimal-but-valid mp4 shape: [size]"ftyp" brand box followed by payload bytes. +const FULL_MP4 = Buffer.concat([ + Buffer.from([0x00, 0x00, 0x00, 0x18]), + Buffer.from("ftypisom"), + Buffer.from([0x00, 0x00, 0x02, 0x00]), + Buffer.from("isomiso2"), + Buffer.alloc(64_000, 0x07), +]); +// The pathological range fragment observed in the wild: a tail slice (moov atom region) +// of the file — starts mid-container, no ftyp/EBML magic. +const TAIL_FRAGMENT = FULL_MP4.subarray(FULL_MP4.length - 1000); + +describe("looksLikeVideoFile (container magic)", () => { + it("accepts mp4-family (ftyp), webm/mkv (EBML), and ogg (OggS) heads", () => { + assert.equal(looksLikeVideoFile(FULL_MP4), true); + assert.equal(looksLikeVideoFile(Buffer.concat([Buffer.from([0x1a, 0x45, 0xdf, 0xa3]), Buffer.alloc(100)])), true); + assert.equal(looksLikeVideoFile(Buffer.concat([Buffer.from("OggS"), Buffer.alloc(100)])), true); + }); + + it("rejects range fragments, HTML error bodies, and tiny buffers", () => { + assert.equal(looksLikeVideoFile(TAIL_FRAGMENT), false, "moov-tail fragment is not a video file"); + assert.equal(looksLikeVideoFile(Buffer.from("404")), false); + assert.equal(looksLikeVideoFile(Buffer.from([0x00, 0x00])), false); + }); +}); + +// A