@hyperframes/studio-server 0.8.114 → 0.8.116

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers/screenshotClip.ts"],"sourcesContent":["export interface ScreenshotClip {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n// Serialised by page.evaluate, so self-contained; fades everything but the element's own branch.\nexport function getElementScreenshotClip(\n selector: string,\n selectorIndex?: number,\n): ScreenshotClip | undefined {\n // Guard against invalid CSS selectors (e.g. `#0` — a digit-leading id from\n // user HTML that upstream producers forgot to CSS.escape). querySelectorAll\n // throws SyntaxError on those, which bubbles out of page.evaluate and fails\n // the whole thumbnail. Returning undefined here falls back to a full-page\n // screenshot, so the user still sees a thumbnail instead of a broken image.\n let matches: HTMLElement[];\n try {\n matches = Array.from(document.querySelectorAll(selector)).filter(\n (el): el is HTMLElement => el instanceof HTMLElement,\n );\n } catch {\n return undefined;\n }\n const safeIndex = Math.max(0, Math.min(matches.length - 1, Math.floor(selectorIndex ?? 0)));\n const el = matches[safeIndex] ?? null;\n if (!(el instanceof HTMLElement)) return undefined;\n // Opacity, not visibility; each prior inline opacity is kept for clearElementScreenshotIsolation.\n const page = window as Window & { __hfThumbnailFaded?: [CSSStyleDeclaration, string, string][] };\n const faded = (page.__hfThumbnailFaded ??= []);\n for (let node: Element = el; node.parentElement; node = node.parentElement) {\n for (const sibling of Array.from(node.parentElement.children)) {\n const style = (sibling as HTMLElement).style;\n if (sibling === node || !style) continue;\n faded.push([style, style.getPropertyValue(\"opacity\"), style.getPropertyPriority(\"opacity\")]);\n style.setProperty(\"opacity\", \"0\", \"important\");\n }\n }\n const rect = el.getBoundingClientRect();\n if (rect.width < 4 || rect.height < 4) return undefined;\n const pad = 8;\n const x = Math.max(0, rect.left - pad);\n const y = Math.max(0, rect.top - pad);\n const maxWidth = window.innerWidth - x;\n const maxHeight = window.innerHeight - y;\n return {\n x,\n y,\n width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),\n height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight)),\n };\n}\n\nexport function clearElementScreenshotIsolation(): void {\n const page = window as Window & { __hfThumbnailFaded?: [CSSStyleDeclaration, string, string][] };\n for (const [style, value, priority] of (page.__hfThumbnailFaded ?? []).reverse()) {\n if (value) style.setProperty(\"opacity\", value, priority);\n else style.removeProperty(\"opacity\");\n }\n page.__hfThumbnailFaded = [];\n}\n"],"mappings":";AAQO,SAAS,yBACd,UACA,eAC4B;AAM5B,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,KAAK,SAAS,iBAAiB,QAAQ,CAAC,EAAE;AAAA,MACxD,CAACA,QAA0BA,eAAc;AAAA,IAC3C;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,SAAS,GAAG,KAAK,MAAM,iBAAiB,CAAC,CAAC,CAAC;AAC1F,QAAM,KAAK,QAAQ,SAAS,KAAK;AACjC,MAAI,EAAE,cAAc,aAAc,QAAO;AAEzC,QAAM,OAAO;AACb,QAAM,QAAS,KAAK,uBAAuB,CAAC;AAC5C,WAAS,OAAgB,IAAI,KAAK,eAAe,OAAO,KAAK,eAAe;AAC1E,eAAW,WAAW,MAAM,KAAK,KAAK,cAAc,QAAQ,GAAG;AAC7D,YAAM,QAAS,QAAwB;AACvC,UAAI,YAAY,QAAQ,CAAC,MAAO;AAChC,YAAM,KAAK,CAAC,OAAO,MAAM,iBAAiB,SAAS,GAAG,MAAM,oBAAoB,SAAS,CAAC,CAAC;AAC3F,YAAM,YAAY,WAAW,KAAK,WAAW;AAAA,IAC/C;AAAA,EACF;AACA,QAAM,OAAO,GAAG,sBAAsB;AACtC,MAAI,KAAK,QAAQ,KAAK,KAAK,SAAS,EAAG,QAAO;AAC9C,QAAM,MAAM;AACZ,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,OAAO,GAAG;AACrC,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG;AACpC,QAAM,WAAW,OAAO,aAAa;AACrC,QAAM,YAAY,OAAO,cAAc;AACvC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,MAAM,GAAG,QAAQ,CAAC;AAAA,IAC3D,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,SAAS,MAAM,GAAG,SAAS,CAAC;AAAA,EAChE;AACF;AAEO,SAAS,kCAAwC;AACtD,QAAM,OAAO;AACb,aAAW,CAAC,OAAO,OAAO,QAAQ,MAAM,KAAK,sBAAsB,CAAC,GAAG,QAAQ,GAAG;AAChF,QAAI,MAAO,OAAM,YAAY,WAAW,OAAO,QAAQ;AAAA,QAClD,OAAM,eAAe,SAAS;AAAA,EACrC;AACA,OAAK,qBAAqB,CAAC;AAC7B;","names":["el"]}
@@ -5,5 +5,6 @@ interface ScreenshotClip {
5
5
  height: number;
6
6
  }
7
7
  declare function getElementScreenshotClip(selector: string, selectorIndex?: number): ScreenshotClip | undefined;
8
+ declare function clearElementScreenshotIsolation(): void;
8
9
 
9
- export { type ScreenshotClip, getElementScreenshotClip };
10
+ export { type ScreenshotClip, clearElementScreenshotIsolation, getElementScreenshotClip };
@@ -1,7 +1,9 @@
1
1
  import {
2
+ clearElementScreenshotIsolation,
2
3
  getElementScreenshotClip
3
- } from "../chunk-W2SBTCO2.js";
4
+ } from "../chunk-IMG2GSY6.js";
4
5
  export {
6
+ clearElementScreenshotIsolation,
5
7
  getElementScreenshotClip
6
8
  };
7
9
  //# sourceMappingURL=screenshotClip.js.map
@@ -9,6 +9,11 @@ declare function parseSourceDocument(raw: string): {
9
9
  wrappedFragment: boolean;
10
10
  };
11
11
  declare function findTargetElement(document: Document, target: SourceMutationTarget): Element | null;
12
+ /**
13
+ * Removes every target in one parse and one serialization. A target nested inside one already
14
+ * removed no longer matches, which is a normal outcome rather than a failure.
15
+ */
16
+ declare function removeElementsFromHtml(source: string, targets: readonly SourceMutationTarget[]): string;
12
17
  declare function removeElementFromHtml(source: string, target: SourceMutationTarget): string;
13
18
  declare function isHTMLElement(el: Node): el is HTMLElement;
14
19
  declare function dedupeClonedCompositionId(document: Document, clone: Element): void;
@@ -23,6 +28,8 @@ declare function patchElementInHtml(source: string, target: SourceMutationTarget
23
28
  html: string;
24
29
  matched: boolean;
25
30
  };
31
+ /** Whether each target exists in `source`; the document is parsed once however many targets ask. */
32
+ declare function probeElementsInSource(source: string, targets: readonly SourceMutationTarget[]): boolean[];
26
33
  declare function probeElementInSource(source: string, target: SourceMutationTarget): boolean;
27
34
  interface SplitElementResult {
28
35
  html: string;
@@ -87,4 +94,4 @@ declare function unwrapElementsFromHtml(source: string, groupTarget: SourceMutat
87
94
  /** After a cut, give each linked group's right halves their own `data-link` and `data-sync-origin`. */
88
95
  declare function relinkSplitHalvesInHtml(source: string, rightHalfIds: readonly string[]): string;
89
96
 
90
- export { type ElementRebase, type PatchOperation, type SourceMutationTarget, type SplitElementResult, type UnwrapChildTrack, type UnwrapElementsResult, type WrapElementsResult, dedupeClonedCompositionId, findTargetElement, isHTMLElement, parseSourceDocument, patchElementInHtml, probeElementInSource, relinkSplitHalvesInHtml, removeElementFromHtml, splitElementInHtml, unwrapElementsFromHtml, wrapElementsInHtml };
97
+ export { type ElementRebase, type PatchOperation, type SourceMutationTarget, type SplitElementResult, type UnwrapChildTrack, type UnwrapElementsResult, type WrapElementsResult, dedupeClonedCompositionId, findTargetElement, isHTMLElement, parseSourceDocument, patchElementInHtml, probeElementInSource, probeElementsInSource, relinkSplitHalvesInHtml, removeElementFromHtml, removeElementsFromHtml, splitElementInHtml, unwrapElementsFromHtml, wrapElementsInHtml };
@@ -5,12 +5,14 @@ import {
5
5
  parseSourceDocument,
6
6
  patchElementInHtml,
7
7
  probeElementInSource,
8
+ probeElementsInSource,
8
9
  relinkSplitHalvesInHtml,
9
10
  removeElementFromHtml,
11
+ removeElementsFromHtml,
10
12
  splitElementInHtml,
11
13
  unwrapElementsFromHtml,
12
14
  wrapElementsInHtml
13
- } from "../chunk-Z4DIWF3Z.js";
15
+ } from "../chunk-G4YB7Q2Q.js";
14
16
  export {
15
17
  dedupeClonedCompositionId,
16
18
  findTargetElement,
@@ -18,8 +20,10 @@ export {
18
20
  parseSourceDocument,
19
21
  patchElementInHtml,
20
22
  probeElementInSource,
23
+ probeElementsInSource,
21
24
  relinkSplitHalvesInHtml,
22
25
  removeElementFromHtml,
26
+ removeElementsFromHtml,
23
27
  splitElementInHtml,
24
28
  unwrapElementsFromHtml,
25
29
  wrapElementsInHtml
@@ -10,7 +10,7 @@ import {
10
10
  listProjectHistories,
11
11
  openProjectHistory,
12
12
  pruneGoneProjectHistories
13
- } from "../chunk-EPL4YKGI.js";
13
+ } from "../chunk-E3HLZKPI.js";
14
14
  import "../chunk-TTWLTYSQ.js";
15
15
  export {
16
16
  DEFAULT_HISTORY_ROOT,
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { walkDir } from './helpers/safePath.js';
8
8
  export { stampProjectHfIds } from './helpers/hfIdPersist.js';
9
9
  import { SourceMutationTarget } from './helpers/sourceMutation.js';
10
10
  export { PatchOperation, dedupeClonedCompositionId, findTargetElement, parseSourceDocument, patchElementInHtml, removeElementFromHtml, splitElementInHtml } from './helpers/sourceMutation.js';
11
- export { ScreenshotClip, getElementScreenshotClip } from './helpers/screenshotClip.js';
11
+ export { ScreenshotClip, clearElementScreenshotIsolation, getElementScreenshotClip } from './helpers/screenshotClip.js';
12
12
  export { STUDIO_MANUAL_EDITS_PATH, StudioManualEditsRenderScriptOptions, createStudioManualEditsRenderBodyScript, createStudioPositionSeekReapplyScript } from './helpers/manualEditsRenderScript.js';
13
13
  export { STUDIO_MOTION_PATH, StudioMotionRenderScriptOptions, createStudioMotionRenderBodyScript } from './helpers/studioMotionRenderScript.js';
14
14
  export { isSafePath } from '@hyperframes/core/safe-path';
@@ -54,7 +54,8 @@ declare function createProjectSignature(projectDir: string, excluding?: Readonly
54
54
 
55
55
  /**
56
56
  * Compositions whose rendered frames a write at `changedPath` can change, or `null` for all
57
- * of them: the root, assets and any file the root does not mount reach every composition.
57
+ * of them: assets, a root head edit, and any file the root does not mount reach every
58
+ * composition. A head-preserving root write changes only the root's own frames.
58
59
  */
59
60
  declare function compositionsAffectedBy(projectDir: string, changedPath: string): string[] | null;
60
61
 
package/dist/index.js CHANGED
@@ -9,12 +9,14 @@ import {
9
9
  parseSourceDocument,
10
10
  patchElementInHtml,
11
11
  probeElementInSource,
12
+ probeElementsInSource,
12
13
  relinkSplitHalvesInHtml,
13
14
  removeElementFromHtml,
15
+ removeElementsFromHtml,
14
16
  splitElementInHtml,
15
17
  unwrapElementsFromHtml,
16
18
  wrapElementsInHtml
17
- } from "./chunk-Z4DIWF3Z.js";
19
+ } from "./chunk-G4YB7Q2Q.js";
18
20
  import {
19
21
  isCompositionSource,
20
22
  stampProjectHfIds
@@ -44,10 +46,11 @@ import {
44
46
  resolveProjectAndSignature,
45
47
  resolveProjectSignature,
46
48
  settledFileTag
47
- } from "./chunk-EPL4YKGI.js";
49
+ } from "./chunk-E3HLZKPI.js";
48
50
  import {
51
+ clearElementScreenshotIsolation,
49
52
  getElementScreenshotClip
50
- } from "./chunk-W2SBTCO2.js";
53
+ } from "./chunk-IMG2GSY6.js";
51
54
  import {
52
55
  DEFAULT_TARGET_LUFS,
53
56
  absoluteLoudnessPlan,
@@ -588,6 +591,7 @@ import {
588
591
  addMotionPathToScript,
589
592
  removeArcPathFromScript,
590
593
  addAnimationWithKeyframesToScript,
594
+ replaceTweenWithKeyframesInScript,
591
595
  splitAnimationsInScript,
592
596
  splitIntoPropertyGroupsFromScript,
593
597
  shiftPositionsInScript,
@@ -1459,6 +1463,21 @@ function bakeVisibilityOnDelete(document, anim) {
1459
1463
  } catch {
1460
1464
  }
1461
1465
  }
1466
+ function replaceInPlaceUnlessSetOrUnknown(scriptText, body) {
1467
+ const edit = { ...body, easeEach: resolveReplacementEaseEach(scriptText, body) };
1468
+ const inPlace = replaceTweenWithKeyframesInScript(scriptText, body.animationId, edit);
1469
+ if (inPlace !== null) return inPlace;
1470
+ const script = removeAnimationFromScript(scriptText, body.animationId);
1471
+ return addAnimationWithKeyframesToScript(
1472
+ script,
1473
+ body.targetSelector,
1474
+ body.position,
1475
+ body.duration,
1476
+ body.keyframes,
1477
+ body.ease,
1478
+ edit.easeEach
1479
+ ).script;
1480
+ }
1462
1481
  function resolveReplacementEaseEach(scriptText, request) {
1463
1482
  if (request.easeEach !== void 0) return request.easeEach;
1464
1483
  const original = parseGsapScriptAcorn(scriptText).animations.find(
@@ -1578,7 +1597,9 @@ async function applyGsapMutations(c, res, mutations) {
1578
1597
  } catch (error) {
1579
1598
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
1580
1599
  }
1600
+ const mutationChanges = [];
1581
1601
  for (const mutation of mutations) {
1602
+ const previousScript = block.scriptText;
1582
1603
  const result = await executeGsapMutation(mutation, block, respond, writer);
1583
1604
  if (result instanceof Response) return result;
1584
1605
  let newScript = typeof result === "string" ? result : result.script;
@@ -1588,6 +1609,7 @@ async function applyGsapMutations(c, res, mutations) {
1588
1609
  if (HOLD_SYNC_MUTATION_TYPES.has(mutation.type)) {
1589
1610
  newScript = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(newScript) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(newScript);
1590
1611
  }
1612
+ mutationChanges.push(newScript !== previousScript);
1591
1613
  block.scriptText = newScript;
1592
1614
  }
1593
1615
  const changed = block.scriptText !== initialScript;
@@ -1612,6 +1634,7 @@ async function applyGsapMutations(c, res, mutations) {
1612
1634
  ok: true,
1613
1635
  changed,
1614
1636
  mutated: changed,
1637
+ mutationChanges,
1615
1638
  parsed: parseGsapScriptAcorn(block.scriptText),
1616
1639
  before: beforeHtml,
1617
1640
  after: newHtml,
@@ -1861,17 +1884,7 @@ function executeGsapMutationAcorn(body, block, respond) {
1861
1884
  if (keyframesWritePosition(body.keyframes) || keyframesWriteRotation(body.keyframes)) {
1862
1885
  stripStudioEditsFromTarget(block.document, body.targetSelector);
1863
1886
  }
1864
- const script = removeAnimationFromScript(block.scriptText, body.animationId);
1865
- const added = addAnimationWithKeyframesToScript(
1866
- script,
1867
- body.targetSelector,
1868
- body.position,
1869
- body.duration,
1870
- body.keyframes,
1871
- body.ease,
1872
- resolveReplacementEaseEach(block.scriptText, body)
1873
- );
1874
- return added.script;
1887
+ return replaceInPlaceUnlessSetOrUnknown(block.scriptText, body);
1875
1888
  }
1876
1889
  case "split-animations": {
1877
1890
  if (typeof body.originalId !== "string" || !body.originalId || typeof body.newId !== "string" || !body.newId || typeof body.splitTime !== "number" || !Number.isFinite(body.splitTime) || typeof body.elementStart !== "number" || !Number.isFinite(body.elementStart) || typeof body.elementDuration !== "number" || !Number.isFinite(body.elementDuration) || body.elementDuration <= 0) {
@@ -2193,17 +2206,7 @@ async function executeGsapMutationRecast(body, block, respond) {
2193
2206
  if (keyframesWritePosition(body.keyframes) || keyframesWriteRotation(body.keyframes)) {
2194
2207
  stripStudioEditsFromTarget(block.document, body.targetSelector);
2195
2208
  }
2196
- const script = removeAnimationFromScript2(block.scriptText, body.animationId);
2197
- const added = addAnimationWithKeyframesToScript2(
2198
- script,
2199
- body.targetSelector,
2200
- body.position,
2201
- body.duration,
2202
- body.keyframes,
2203
- body.ease,
2204
- resolveReplacementEaseEach(block.scriptText, body)
2205
- );
2206
- return added.script;
2209
+ return replaceInPlaceUnlessSetOrUnknown(block.scriptText, body);
2207
2210
  }
2208
2211
  case "split-animations": {
2209
2212
  if (typeof body.originalId !== "string" || !body.originalId || typeof body.newId !== "string" || !body.newId || typeof body.splitTime !== "number" || !Number.isFinite(body.splitTime) || typeof body.elementStart !== "number" || !Number.isFinite(body.elementStart) || typeof body.elementDuration !== "number" || !Number.isFinite(body.elementDuration) || body.elementDuration <= 0) {
@@ -2671,11 +2674,14 @@ function registerFileRoutes(api, adapter) {
2671
2674
  return c.json({ error: "targets required" }, 400);
2672
2675
  }
2673
2676
  const originalContent = readFileSync3(ctx.absPath, "utf-8");
2674
- let next = originalContent;
2675
- for (const target of targets) {
2676
- next = removeElementFromHtml(next, target);
2677
- }
2678
- return writeIfChanged(c, ctx.project.dir, ctx.filePath, ctx.absPath, originalContent, next);
2677
+ return writeIfChanged(
2678
+ c,
2679
+ ctx.project.dir,
2680
+ ctx.filePath,
2681
+ ctx.absPath,
2682
+ originalContent,
2683
+ removeElementsFromHtml(originalContent, targets)
2684
+ );
2679
2685
  });
2680
2686
  api.post("/projects/:id/file-mutations/split-batch", async (c) => {
2681
2687
  const body = await c.req.json().catch(() => null);
@@ -3079,6 +3085,19 @@ function registerFileRoutes(api, adapter) {
3079
3085
  const exists = probeElementInSource(content, parsed.target);
3080
3086
  return c.json({ exists });
3081
3087
  });
3088
+ api.post("/projects/:id/file-mutations/probe-elements/*", async (c) => {
3089
+ const ctx = await resolveFileMutationContext(c, adapter, "probe-elements");
3090
+ if ("error" in ctx) return ctx.error;
3091
+ const body = await c.req.json().catch(() => null);
3092
+ if (!Array.isArray(body?.targets)) return c.json({ error: "targets required" }, 400);
3093
+ let content;
3094
+ try {
3095
+ content = readFileSync3(ctx.absPath, "utf-8");
3096
+ } catch {
3097
+ return c.json({ exists: body.targets.map(() => false) });
3098
+ }
3099
+ return c.json({ exists: probeElementsInSource(content, body.targets) });
3100
+ });
3082
3101
  api.patch("/projects/:id/files/*", async (c) => {
3083
3102
  const res = await resolveProjectFile(c, adapter, { mustExist: true });
3084
3103
  if ("error" in res) return res.error;
@@ -3162,17 +3181,18 @@ function registerFileRoutes(api, adapter) {
3162
3181
  });
3163
3182
  if ("error" in res) return res.error;
3164
3183
  const html = readFileSync3(res.absPath, "utf-8");
3184
+ const etag = `${fileContentVersion(html).slice(0, -1)}:gsap-animations:v1"`;
3185
+ const headers = { ETag: etag, "Cache-Control": "no-cache" };
3186
+ if (c.req.header("If-None-Match") === etag) return new Response(null, { status: 304, headers });
3165
3187
  const block = extractGsapScriptBlock(html);
3166
3188
  if (!block) {
3167
- return c.json({
3168
- animations: [],
3169
- timelineVar: "tl",
3170
- preamble: "",
3171
- postamble: ""
3172
- });
3189
+ return c.json(
3190
+ { animations: [], timelineVar: "tl", preamble: "", postamble: "" },
3191
+ 200,
3192
+ headers
3193
+ );
3173
3194
  }
3174
- const parsed = parseGsapScriptAcorn(block.scriptText);
3175
- return c.json(parsed);
3195
+ return c.json(parseGsapScriptAcorn(block.scriptText), 200, headers);
3176
3196
  });
3177
3197
  api.get("/projects/:id/gsap-mutation-capabilities", async (c) => {
3178
3198
  const project = await adapter.resolveProject(c.req.param("id"));
@@ -3349,6 +3369,9 @@ function tagRootCompositionFile(bodyHtml, compPath) {
3349
3369
  if (bodyHtml.slice(match.index, tagEnd).includes("data-composition-file")) return bodyHtml;
3350
3370
  return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
3351
3371
  }
3372
+ function rootHeadContent(rootHtml) {
3373
+ return rootHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] ?? "";
3374
+ }
3352
3375
  function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
3353
3376
  const compFile = join6(projectDir, compPath);
3354
3377
  if (!existsSync4(compFile)) return null;
@@ -3384,11 +3407,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
3384
3407
  rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
3385
3408
  const indexPath = join6(projectDir, "index.html");
3386
3409
  let headContent = "";
3387
- if (existsSync4(indexPath)) {
3388
- const indexHtml = readFileSync4(indexPath, "utf-8");
3389
- const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
3390
- headContent = headMatch?.[1] ?? "";
3391
- }
3410
+ if (existsSync4(indexPath)) headContent = rootHeadContent(readFileSync4(indexPath, "utf-8"));
3392
3411
  if (baseHref && !hasBaseElement(headContent)) {
3393
3412
  headContent = `<base href="${baseHref}">
3394
3413
  ${headContent}`;
@@ -4544,10 +4563,11 @@ import {
4544
4563
  writeFileSync as writeFileSync4
4545
4564
  } from "fs";
4546
4565
  import { join as join11 } from "path";
4547
- import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
4566
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
4548
4567
 
4549
4568
  // src/helpers/compositionInputs.ts
4550
4569
  import { readFileSync as readFileSync8 } from "fs";
4570
+ import { createHash as createHash3 } from "crypto";
4551
4571
  var ROOT_COMPOSITION = "index.html";
4552
4572
  var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
4553
4573
  function projectReader(projectDir) {
@@ -4584,27 +4604,51 @@ function closureOf(read, compPath) {
4584
4604
  return closure;
4585
4605
  }
4586
4606
  var inputSignatures = /* @__PURE__ */ new Map();
4607
+ var rootMemos = /* @__PURE__ */ new Map();
4608
+ var sha1 = (text2) => createHash3("sha1").update(text2).digest("hex");
4609
+ function rootHashes(read) {
4610
+ const source = read(ROOT_COMPOSITION) ?? "";
4611
+ return { root: sha1(source), head: sha1(rootHeadContent(source)) };
4612
+ }
4587
4613
  function compositionInputSignature(projectDir, compPath, projectSignature) {
4588
4614
  const key = `${projectDir}\0${compPath}`;
4589
4615
  const known = inputSignatures.get(key);
4590
4616
  if (known?.projectSignature === projectSignature) return known.inputSignature;
4591
4617
  const read = projectReader(projectDir);
4592
- const inputs = closureOf(read, normalizeSource(compPath) ?? compPath).add(ROOT_COMPOSITION);
4593
- const siblings = [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path));
4594
- const inputSignature = createProjectSignature(projectDir, new Set(siblings));
4618
+ const comp = normalizeSource(compPath) ?? compPath;
4619
+ const inputs = closureOf(read, comp).add(ROOT_COMPOSITION);
4620
+ const excluded = new Set(
4621
+ [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path))
4622
+ );
4623
+ let inputSignature;
4624
+ if (comp === ROOT_COMPOSITION) {
4625
+ inputSignature = createProjectSignature(projectDir, excluded);
4626
+ } else {
4627
+ const hashes = rootHashes(read);
4628
+ if (!rootMemos.has(projectDir)) rootMemos.set(projectDir, { ...hashes, affected: null });
4629
+ inputSignature = `${createProjectSignature(projectDir, excluded.add(ROOT_COMPOSITION))}:${hashes.head}`;
4630
+ }
4595
4631
  inputSignatures.set(key, { projectSignature, inputSignature });
4596
4632
  return inputSignature;
4597
4633
  }
4598
4634
  function compositionsAffectedBy(projectDir, changedPath) {
4599
4635
  const changed = changedPath.replace(/\\/g, "/");
4600
4636
  const read = projectReader(projectDir);
4637
+ if (changed === ROOT_COMPOSITION) {
4638
+ const now = rootHashes(read);
4639
+ const memo = rootMemos.get(projectDir);
4640
+ if (memo?.root === now.root) return memo.affected;
4641
+ const affected = memo?.head === now.head ? [ROOT_COMPOSITION] : null;
4642
+ rootMemos.set(projectDir, { ...now, affected });
4643
+ return affected;
4644
+ }
4601
4645
  const mounted = closureOf(read, ROOT_COMPOSITION);
4602
- if (changed === ROOT_COMPOSITION || !mounted.has(changed)) return null;
4646
+ if (!mounted.has(changed)) return null;
4603
4647
  return [...mounted].filter((path) => closureOf(read, path).has(changed));
4604
4648
  }
4605
4649
 
4606
4650
  // src/routes/thumbnail.ts
4607
- var THUMBNAIL_CACHE_VERSION = "v4";
4651
+ var THUMBNAIL_CACHE_VERSION = "v5";
4608
4652
  var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
4609
4653
  var THUMBNAIL_MAX_OUTPUT_HEIGHT = 135;
4610
4654
  var THUMBNAIL_CACHE_MAX_BYTES = 512 * 1024 * 1024;
@@ -4677,7 +4721,7 @@ function readFileOnce(file) {
4677
4721
  function manifestKey(file) {
4678
4722
  const manifest = readFileOnce(file);
4679
4723
  if (typeof manifest === "string") return { key: "", mtimeMs: -Infinity };
4680
- const hash = createHash3("sha1").update(manifest.data).digest("hex").slice(0, 16);
4724
+ const hash = createHash4("sha1").update(manifest.data).digest("hex").slice(0, 16);
4681
4725
  return { key: `_${hash}`, mtimeMs: Math.round(manifest.mtimeMs) };
4682
4726
  }
4683
4727
  function registerThumbnailRoutes(api, adapter) {
@@ -4714,7 +4758,7 @@ function registerThumbnailRoutes(api, adapter) {
4714
4758
  let sourceKey = "";
4715
4759
  if (source !== "missing") {
4716
4760
  const html = source.data.toString("utf-8");
4717
- sourceKey = `_${createHash3("sha1").update(html).digest("hex").slice(0, 16)}`;
4761
+ sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
4718
4762
  sourceMtime = Math.round(source.mtimeMs);
4719
4763
  if (!vpWidth) {
4720
4764
  const wMatch = html.match(/data-width=["'](\d+)["']/);
@@ -4730,9 +4774,9 @@ function registerThumbnailRoutes(api, adapter) {
4730
4774
  const previewPath = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
4731
4775
  const previewUrl = `${previewPath}?${PREVIEW_CAPTURE_PARAM}=1`;
4732
4776
  const cacheDir = join11(project.dir, ".thumbnails");
4733
- const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
4777
+ const selectorKey = selector ? `_${createHash4("sha1").update(selector).digest("hex").slice(0, 16)}_${selectorIndex ?? 0}` : "";
4734
4778
  const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
4735
- const inputSignatureKey = `_${createHash3("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
4779
+ const inputSignatureKey = `_${createHash4("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
4736
4780
  const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
4737
4781
  const outputWidth = Math.max(1, Math.round(compW * outputScale));
4738
4782
  const outputHeight = Math.max(1, Math.round(compH * outputScale));
@@ -4852,7 +4896,7 @@ import { findFfBinary as findFfBinary3 } from "@hyperframes/parsers/ff-binaries"
4852
4896
  import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
4853
4897
 
4854
4898
  // src/helpers/freezeFrame.ts
4855
- import { createHash as createHash4, randomBytes } from "crypto";
4899
+ import { createHash as createHash5, randomBytes } from "crypto";
4856
4900
  import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
4857
4901
  import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
4858
4902
  import { resolveRateSpec, sourceTimeAt } from "@hyperframes/core/speed-ramp";
@@ -4879,7 +4923,7 @@ function freezeExtractArgs(src, mediaTime, output) {
4879
4923
  var randomStillToken = () => randomBytes(4).toString("hex");
4880
4924
  function freezeStillFileName(clipId, playhead, token = randomStillToken()) {
4881
4925
  const stem = clipId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 48) || "clip";
4882
- const idHash = createHash4("sha256").update(clipId).digest("hex").slice(0, 10);
4926
+ const idHash = createHash5("sha256").update(clipId).digest("hex").slice(0, 10);
4883
4927
  return `${stem}-${idHash}-${Math.round(playhead * 1e3)}-${token}.png`;
4884
4928
  }
4885
4929
  function readFreezeSource(html, target, playhead) {
@@ -6087,13 +6131,13 @@ function numericAttribute(element, name) {
6087
6131
  }
6088
6132
 
6089
6133
  // src/helpers/previewDocumentStore.ts
6090
- import { createHash as createHash5 } from "crypto";
6134
+ import { createHash as createHash6 } from "crypto";
6091
6135
  import { readFileSync as readFileSync14, readdirSync as readdirSync5, rmSync as rmSync5 } from "fs";
6092
6136
  import { join as join17 } from "path";
6093
6137
  import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
6094
6138
  function createPreviewDocumentStore(projectDir, salt) {
6095
6139
  const dir = join17(projectDir, ".hyperframes", "preview");
6096
- const fileFor = (key) => join17(dir, `${createHash5("sha256").update(`${salt}
6140
+ const fileFor = (key) => join17(dir, `${createHash6("sha256").update(`${salt}
6097
6141
  ${key}`).digest("hex").slice(0, 32)}.html`);
6098
6142
  return {
6099
6143
  read(key) {
@@ -6203,6 +6247,7 @@ export {
6203
6247
  applyFileMutations,
6204
6248
  buildSubCompositionHtml,
6205
6249
  cleanupProxyCache,
6250
+ clearElementScreenshotIsolation,
6206
6251
  compositionsAffectedBy,
6207
6252
  consumeFileWriteReceipt,
6208
6253
  createBackgroundRemovalJob,