From 9f0074e44a80c851f5771f6012ade59d3828842c Mon Sep 17 00:00:00 2001 From: James Date: Thu, 7 May 2026 05:33:22 +0000 Subject: [PATCH] fix(cli): handle reverse-order CSS in applyResolutionPreset and document scope --- packages/cli/src/commands/init.test.ts | 57 ++++++++++++++++++++++++++ packages/cli/src/commands/init.ts | 26 ++++++++---- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/commands/init.test.ts b/packages/cli/src/commands/init.test.ts index ef7dbf561..59cfa7ca4 100644 --- a/packages/cli/src/commands/init.test.ts +++ b/packages/cli/src/commands/init.test.ts @@ -253,4 +253,61 @@ describe("applyResolutionPreset", () => { rmSync(dir, { recursive: true, force: true }); } }); + + it("rewrites height-before-width inline CSS", () => { + withFixture((dir) => { + const file = join(dir, "index.html"); + // Reversed property order — same as the parser's stageMatchReverse path. + const reversedOrderHtml = sampleHtml.replace( + "html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; }", + "html, body { margin: 0; height: 1080px; width: 1920px; overflow: hidden; }", + ); + writeFileSync(file, reversedOrderHtml, "utf-8"); + + applyResolutionPreset(dir, "landscape-4k"); + const out = readFileSync(file, "utf-8"); + + expect(out).toContain("height: 2160px"); + expect(out).toContain("width: 3840px"); + expect(out).not.toContain("1080px"); + expect(out).not.toContain("1920px"); + }); + }); + + it("is a no-op on a file with no dimension fingerprint (does not error)", () => { + withFixture((dir) => { + const file = join(dir, "fragment.html"); + // No data-width/height, no html/body block, no viewport — just markup. + const minimal = "

hi

"; + writeFileSync(file, minimal, "utf-8"); + + expect(() => applyResolutionPreset(dir, "landscape-4k")).not.toThrow(); + const out = readFileSync(file, "utf-8"); + // The htmlOpenRe path adds `data-resolution="landscape-4k"` because + // the tag is present. That's correct: an explicit signal of + // intended resolution survives even when no dim fields exist. + expect(out).toContain('data-resolution="landscape-4k"'); + }); + }); + + it("accepts uppercase --resolution value (4K)", () => { + const dir = mkdtempSync(join(tmpdir(), "hf-init-test-")); + const target = join(dir, "proj"); + try { + const res = runInit([ + target, + "--example", + "blank", + "--resolution", + "4K", + "--non-interactive", + "--skip-skills", + ]); + expect(res.status).toBe(0); + const html = readFileSync(join(target, "index.html"), "utf-8"); + expect(html).toContain('data-width="3840"'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 4f20a1544..d31cad686 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -451,15 +451,16 @@ async function handleVideoFile( // --------------------------------------------------------------------------- /** - * Templates ship with 1920×1080 dimensions baked into multiple places. - * When the user picks a non-default resolution we walk every HTML file in - * the scaffold and rewrite the dimension fingerprint: - * - `data-width` / `data-height` on the root composition - * - `data-resolution` on the element (added when missing) - * - the `width`/`height` declared in the inline `html, body { ... }` CSS - * - the `` tag's `width=`/`height=` parts - * We rewrite by regex rather than DOM-parsing because templates have - * decorative comments and indentation we want to preserve byte-for-byte. + * Rewrite the canvas dimensions in every scaffolded HTML file to match a + * preset. We rewrite by regex rather than DOM-parsing so template comments + * and indentation survive byte-for-byte — these are review-target files, + * not transient build artifacts. + * + * Scope: HTML files only. Templates whose `#stage` dimensions live in an + * external `.css` stylesheet are not patched — the bundled `blank` template + * inlines its CSS, and that's the convention for new templates. If you + * author a template with external CSS, replicate the dimension swap there + * by hand or move the dimensions inline. */ export function applyResolutionPreset(destDir: string, resolution: CanvasResolution): void { const { width, height } = CANVAS_DIMENSIONS[resolution]; @@ -494,11 +495,18 @@ export function applyResolutionPreset(destDir: string, resolution: CanvasResolut } } + // Inline `html, body { ... }` CSS: handle width-before-height and + // height-before-width orderings. Hand-authored templates can use either. const bodyCssRe = /(html\s*,\s*body\s*\{[^}]*?width:\s*)\d+px([^}]*?height:\s*)\d+px/i; if (bodyCssRe.test(html)) { html = html.replace(bodyCssRe, `$1${width}px$2${height}px`); changed = true; } + const bodyCssReverseRe = /(html\s*,\s*body\s*\{[^}]*?height:\s*)\d+px([^}]*?width:\s*)\d+px/i; + if (bodyCssReverseRe.test(html)) { + html = html.replace(bodyCssReverseRe, `$1${height}px$2${width}px`); + changed = true; + } const viewportRe = /(]*name=["']viewport["'][^>]*content=["'])width=\d+,\s*height=\d+/i; if (viewportRe.test(html)) {