fix(core): render standalone sub-composition previews + surface captured video assets (cli) (#1631)

* fix(core): escape digit-leading id selectors in standalone sub-composition preview

A CSS identifier cannot start with a digit, so an authored rule like
`#01-wall-pushes-back { ... }` is an invalid selector and the browser drops
the whole rule — taking the root's size/background with it. A full
composition masks this (the host stretches/paints the frame), but a
standalone preview has no host, so the root collapses to height:0 +
transparent and renders blank.

extractFullDocumentParts now rewrites `#<digit-leading-id>` selectors to
their escaped valid form (`#\30 1-...`, still matching the element id),
scoped to ids actually present and matched only as `#id` not followed by an
ident char so hex colors are never touched. Also harden the <template>
inner-HTML extraction to use the DOM instead of a greedy regex. Tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit e43b377fc0)

* feat(cli): surface captured video clips in asset descriptions

generateAssetDescriptions now reads extracted/video-manifest.json and emits
each downloaded clip first, tagged [video], with its DOM heading/caption and
dimensions — motion clips are usually the strongest hero material and
downstream planners key off the [video] marker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(studio): import commitGsapPositionFromDrag from its actual module

The function was split out into gsapDragPositionCommit.ts in #1605, but
the test kept importing it from ./gsapDragCommit, which no longer exports
it — yielding `is not a function` at runtime. Import from the correct
module to match the production import in gsapRuntimeBridge.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): address review nits on standalone sub-composition preview

Review follow-ups (#1631), all non-blocking polish:

- contentExtractor: use path.basename() instead of localPath.split('/').pop()
  so video filenames resolve correctly on Windows-style paths too.
- subComposition: document that only the leading digit needs CSS escaping
  (CSS Syntax L3 §4.3.11) on escapeLeadingDigitIdent.
- tests: pin three previously-uncovered paths — multiple digit-leading ids in
  one composition, a digit-leading id inside compound/combinator selectors, and
  the promoteTemplateCompositionId no-op when the <template> has no id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
WaterrrForever
2026-06-22 22:49:54 +08:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 1967901b57
commit 0e75eb2510
3 changed files with 373 additions and 7 deletions
+33 -2
View File
@@ -10,7 +10,7 @@
import type { Page } from "puppeteer-core";
import { existsSync, readdirSync, statSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { basename, join } from "node:path";
import type sharpType from "sharp";
import type { CatalogedAsset } from "./assetCataloger.js";
import type { DesignTokens } from "./types.js";
@@ -494,5 +494,36 @@ export function generateAssetDescriptions(
/* no fonts dir */
}
return [...captionedLines, ...uncaptionedLines, ...svgLines, ...fontLines];
// Describe videos — high-value motion clips. The video-manifest.json (written
// earlier by captureVideoManifest) carries each clip's DOM heading/caption +
// dims. Surfaced FIRST and tagged `[video]`: for a product/demo these moving
// clips are usually the strongest hero material, and downstream planners key off
// the `[video]` marker. (The `videos/` dir is skipped in the image walk above —
// its entries come from the manifest, which has the captions the bare files lack.)
const videoLines: string[] = [];
try {
const manifest = JSON.parse(
readFileSync(join(outputDir, "extracted", "video-manifest.json"), "utf-8"),
) as Array<{
filename?: string;
localPath?: string;
caption?: string;
heading?: string;
width?: number;
height?: number;
}>;
for (const v of manifest) {
if (!v.localPath) continue; // only describe clips that actually downloaded
const base = basename(v.localPath) || v.filename || "";
if (!base) continue;
const desc =
(v.caption || v.heading || "").trim().replace(/\s+/g, " ").slice(0, 140) || "motion clip";
const dims = v.width && v.height ? `, ~${v.width}×${v.height}` : "";
videoLines.push(`${base} — [video] ${desc}${dims}`);
}
} catch {
/* no video manifest */
}
return [...videoLines, ...captionedLines, ...uncaptionedLines, ...svgLines, ...fontLines];
}