@hyperframes/studio-server 0.8.114 → 0.8.115

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,
@@ -1578,7 +1581,9 @@ async function applyGsapMutations(c, res, mutations) {
1578
1581
  } catch (error) {
1579
1582
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
1580
1583
  }
1584
+ const mutationChanges = [];
1581
1585
  for (const mutation of mutations) {
1586
+ const previousScript = block.scriptText;
1582
1587
  const result = await executeGsapMutation(mutation, block, respond, writer);
1583
1588
  if (result instanceof Response) return result;
1584
1589
  let newScript = typeof result === "string" ? result : result.script;
@@ -1588,6 +1593,7 @@ async function applyGsapMutations(c, res, mutations) {
1588
1593
  if (HOLD_SYNC_MUTATION_TYPES.has(mutation.type)) {
1589
1594
  newScript = writer === "acorn" ? syncPositionHoldsBeforeKeyframes(newScript) : (await loadGsapParser()).syncPositionHoldsBeforeKeyframes(newScript);
1590
1595
  }
1596
+ mutationChanges.push(newScript !== previousScript);
1591
1597
  block.scriptText = newScript;
1592
1598
  }
1593
1599
  const changed = block.scriptText !== initialScript;
@@ -1612,6 +1618,7 @@ async function applyGsapMutations(c, res, mutations) {
1612
1618
  ok: true,
1613
1619
  changed,
1614
1620
  mutated: changed,
1621
+ mutationChanges,
1615
1622
  parsed: parseGsapScriptAcorn(block.scriptText),
1616
1623
  before: beforeHtml,
1617
1624
  after: newHtml,
@@ -2671,11 +2678,14 @@ function registerFileRoutes(api, adapter) {
2671
2678
  return c.json({ error: "targets required" }, 400);
2672
2679
  }
2673
2680
  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);
2681
+ return writeIfChanged(
2682
+ c,
2683
+ ctx.project.dir,
2684
+ ctx.filePath,
2685
+ ctx.absPath,
2686
+ originalContent,
2687
+ removeElementsFromHtml(originalContent, targets)
2688
+ );
2679
2689
  });
2680
2690
  api.post("/projects/:id/file-mutations/split-batch", async (c) => {
2681
2691
  const body = await c.req.json().catch(() => null);
@@ -3079,6 +3089,19 @@ function registerFileRoutes(api, adapter) {
3079
3089
  const exists = probeElementInSource(content, parsed.target);
3080
3090
  return c.json({ exists });
3081
3091
  });
3092
+ api.post("/projects/:id/file-mutations/probe-elements/*", async (c) => {
3093
+ const ctx = await resolveFileMutationContext(c, adapter, "probe-elements");
3094
+ if ("error" in ctx) return ctx.error;
3095
+ const body = await c.req.json().catch(() => null);
3096
+ if (!Array.isArray(body?.targets)) return c.json({ error: "targets required" }, 400);
3097
+ let content;
3098
+ try {
3099
+ content = readFileSync3(ctx.absPath, "utf-8");
3100
+ } catch {
3101
+ return c.json({ exists: body.targets.map(() => false) });
3102
+ }
3103
+ return c.json({ exists: probeElementsInSource(content, body.targets) });
3104
+ });
3082
3105
  api.patch("/projects/:id/files/*", async (c) => {
3083
3106
  const res = await resolveProjectFile(c, adapter, { mustExist: true });
3084
3107
  if ("error" in res) return res.error;
@@ -3162,17 +3185,18 @@ function registerFileRoutes(api, adapter) {
3162
3185
  });
3163
3186
  if ("error" in res) return res.error;
3164
3187
  const html = readFileSync3(res.absPath, "utf-8");
3188
+ const etag = `${fileContentVersion(html).slice(0, -1)}:gsap-animations:v1"`;
3189
+ const headers = { ETag: etag, "Cache-Control": "no-cache" };
3190
+ if (c.req.header("If-None-Match") === etag) return new Response(null, { status: 304, headers });
3165
3191
  const block = extractGsapScriptBlock(html);
3166
3192
  if (!block) {
3167
- return c.json({
3168
- animations: [],
3169
- timelineVar: "tl",
3170
- preamble: "",
3171
- postamble: ""
3172
- });
3193
+ return c.json(
3194
+ { animations: [], timelineVar: "tl", preamble: "", postamble: "" },
3195
+ 200,
3196
+ headers
3197
+ );
3173
3198
  }
3174
- const parsed = parseGsapScriptAcorn(block.scriptText);
3175
- return c.json(parsed);
3199
+ return c.json(parseGsapScriptAcorn(block.scriptText), 200, headers);
3176
3200
  });
3177
3201
  api.get("/projects/:id/gsap-mutation-capabilities", async (c) => {
3178
3202
  const project = await adapter.resolveProject(c.req.param("id"));
@@ -3349,6 +3373,9 @@ function tagRootCompositionFile(bodyHtml, compPath) {
3349
3373
  if (bodyHtml.slice(match.index, tagEnd).includes("data-composition-file")) return bodyHtml;
3350
3374
  return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
3351
3375
  }
3376
+ function rootHeadContent(rootHtml) {
3377
+ return rootHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] ?? "";
3378
+ }
3352
3379
  function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
3353
3380
  const compFile = join6(projectDir, compPath);
3354
3381
  if (!existsSync4(compFile)) return null;
@@ -3384,11 +3411,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
3384
3411
  rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
3385
3412
  const indexPath = join6(projectDir, "index.html");
3386
3413
  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
- }
3414
+ if (existsSync4(indexPath)) headContent = rootHeadContent(readFileSync4(indexPath, "utf-8"));
3392
3415
  if (baseHref && !hasBaseElement(headContent)) {
3393
3416
  headContent = `<base href="${baseHref}">
3394
3417
  ${headContent}`;
@@ -4544,10 +4567,11 @@ import {
4544
4567
  writeFileSync as writeFileSync4
4545
4568
  } from "fs";
4546
4569
  import { join as join11 } from "path";
4547
- import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
4570
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
4548
4571
 
4549
4572
  // src/helpers/compositionInputs.ts
4550
4573
  import { readFileSync as readFileSync8 } from "fs";
4574
+ import { createHash as createHash3 } from "crypto";
4551
4575
  var ROOT_COMPOSITION = "index.html";
4552
4576
  var COMPOSITION_SRC = /\bdata-composition-src\s*=\s*(["'])(.*?)\1/g;
4553
4577
  function projectReader(projectDir) {
@@ -4584,27 +4608,51 @@ function closureOf(read, compPath) {
4584
4608
  return closure;
4585
4609
  }
4586
4610
  var inputSignatures = /* @__PURE__ */ new Map();
4611
+ var rootMemos = /* @__PURE__ */ new Map();
4612
+ var sha1 = (text2) => createHash3("sha1").update(text2).digest("hex");
4613
+ function rootHashes(read) {
4614
+ const source = read(ROOT_COMPOSITION) ?? "";
4615
+ return { root: sha1(source), head: sha1(rootHeadContent(source)) };
4616
+ }
4587
4617
  function compositionInputSignature(projectDir, compPath, projectSignature) {
4588
4618
  const key = `${projectDir}\0${compPath}`;
4589
4619
  const known = inputSignatures.get(key);
4590
4620
  if (known?.projectSignature === projectSignature) return known.inputSignature;
4591
4621
  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));
4622
+ const comp = normalizeSource(compPath) ?? compPath;
4623
+ const inputs = closureOf(read, comp).add(ROOT_COMPOSITION);
4624
+ const excluded = new Set(
4625
+ [...closureOf(read, ROOT_COMPOSITION)].filter((path) => !inputs.has(path))
4626
+ );
4627
+ let inputSignature;
4628
+ if (comp === ROOT_COMPOSITION) {
4629
+ inputSignature = createProjectSignature(projectDir, excluded);
4630
+ } else {
4631
+ const hashes = rootHashes(read);
4632
+ if (!rootMemos.has(projectDir)) rootMemos.set(projectDir, { ...hashes, affected: null });
4633
+ inputSignature = `${createProjectSignature(projectDir, excluded.add(ROOT_COMPOSITION))}:${hashes.head}`;
4634
+ }
4595
4635
  inputSignatures.set(key, { projectSignature, inputSignature });
4596
4636
  return inputSignature;
4597
4637
  }
4598
4638
  function compositionsAffectedBy(projectDir, changedPath) {
4599
4639
  const changed = changedPath.replace(/\\/g, "/");
4600
4640
  const read = projectReader(projectDir);
4641
+ if (changed === ROOT_COMPOSITION) {
4642
+ const now = rootHashes(read);
4643
+ const memo = rootMemos.get(projectDir);
4644
+ if (memo?.root === now.root) return memo.affected;
4645
+ const affected = memo?.head === now.head ? [ROOT_COMPOSITION] : null;
4646
+ rootMemos.set(projectDir, { ...now, affected });
4647
+ return affected;
4648
+ }
4601
4649
  const mounted = closureOf(read, ROOT_COMPOSITION);
4602
- if (changed === ROOT_COMPOSITION || !mounted.has(changed)) return null;
4650
+ if (!mounted.has(changed)) return null;
4603
4651
  return [...mounted].filter((path) => closureOf(read, path).has(changed));
4604
4652
  }
4605
4653
 
4606
4654
  // src/routes/thumbnail.ts
4607
- var THUMBNAIL_CACHE_VERSION = "v4";
4655
+ var THUMBNAIL_CACHE_VERSION = "v5";
4608
4656
  var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
4609
4657
  var THUMBNAIL_MAX_OUTPUT_HEIGHT = 135;
4610
4658
  var THUMBNAIL_CACHE_MAX_BYTES = 512 * 1024 * 1024;
@@ -4677,7 +4725,7 @@ function readFileOnce(file) {
4677
4725
  function manifestKey(file) {
4678
4726
  const manifest = readFileOnce(file);
4679
4727
  if (typeof manifest === "string") return { key: "", mtimeMs: -Infinity };
4680
- const hash = createHash3("sha1").update(manifest.data).digest("hex").slice(0, 16);
4728
+ const hash = createHash4("sha1").update(manifest.data).digest("hex").slice(0, 16);
4681
4729
  return { key: `_${hash}`, mtimeMs: Math.round(manifest.mtimeMs) };
4682
4730
  }
4683
4731
  function registerThumbnailRoutes(api, adapter) {
@@ -4714,7 +4762,7 @@ function registerThumbnailRoutes(api, adapter) {
4714
4762
  let sourceKey = "";
4715
4763
  if (source !== "missing") {
4716
4764
  const html = source.data.toString("utf-8");
4717
- sourceKey = `_${createHash3("sha1").update(html).digest("hex").slice(0, 16)}`;
4765
+ sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
4718
4766
  sourceMtime = Math.round(source.mtimeMs);
4719
4767
  if (!vpWidth) {
4720
4768
  const wMatch = html.match(/data-width=["'](\d+)["']/);
@@ -4730,9 +4778,9 @@ function registerThumbnailRoutes(api, adapter) {
4730
4778
  const previewPath = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
4731
4779
  const previewUrl = `${previewPath}?${PREVIEW_CAPTURE_PARAM}=1`;
4732
4780
  const cacheDir = join11(project.dir, ".thumbnails");
4733
- const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
4781
+ const selectorKey = selector ? `_${createHash4("sha1").update(selector).digest("hex").slice(0, 16)}_${selectorIndex ?? 0}` : "";
4734
4782
  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)}`;
4783
+ const inputSignatureKey = `_${createHash4("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
4736
4784
  const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
4737
4785
  const outputWidth = Math.max(1, Math.round(compW * outputScale));
4738
4786
  const outputHeight = Math.max(1, Math.round(compH * outputScale));
@@ -4852,7 +4900,7 @@ import { findFfBinary as findFfBinary3 } from "@hyperframes/parsers/ff-binaries"
4852
4900
  import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
4853
4901
 
4854
4902
  // src/helpers/freezeFrame.ts
4855
- import { createHash as createHash4, randomBytes } from "crypto";
4903
+ import { createHash as createHash5, randomBytes } from "crypto";
4856
4904
  import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
4857
4905
  import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
4858
4906
  import { resolveRateSpec, sourceTimeAt } from "@hyperframes/core/speed-ramp";
@@ -4879,7 +4927,7 @@ function freezeExtractArgs(src, mediaTime, output) {
4879
4927
  var randomStillToken = () => randomBytes(4).toString("hex");
4880
4928
  function freezeStillFileName(clipId, playhead, token = randomStillToken()) {
4881
4929
  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);
4930
+ const idHash = createHash5("sha256").update(clipId).digest("hex").slice(0, 10);
4883
4931
  return `${stem}-${idHash}-${Math.round(playhead * 1e3)}-${token}.png`;
4884
4932
  }
4885
4933
  function readFreezeSource(html, target, playhead) {
@@ -6087,13 +6135,13 @@ function numericAttribute(element, name) {
6087
6135
  }
6088
6136
 
6089
6137
  // src/helpers/previewDocumentStore.ts
6090
- import { createHash as createHash5 } from "crypto";
6138
+ import { createHash as createHash6 } from "crypto";
6091
6139
  import { readFileSync as readFileSync14, readdirSync as readdirSync5, rmSync as rmSync5 } from "fs";
6092
6140
  import { join as join17 } from "path";
6093
6141
  import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
6094
6142
  function createPreviewDocumentStore(projectDir, salt) {
6095
6143
  const dir = join17(projectDir, ".hyperframes", "preview");
6096
- const fileFor = (key) => join17(dir, `${createHash5("sha256").update(`${salt}
6144
+ const fileFor = (key) => join17(dir, `${createHash6("sha256").update(`${salt}
6097
6145
  ${key}`).digest("hex").slice(0, 32)}.html`);
6098
6146
  return {
6099
6147
  read(key) {
@@ -6203,6 +6251,7 @@ export {
6203
6251
  applyFileMutations,
6204
6252
  buildSubCompositionHtml,
6205
6253
  cleanupProxyCache,
6254
+ clearElementScreenshotIsolation,
6206
6255
  compositionsAffectedBy,
6207
6256
  consumeFileWriteReceipt,
6208
6257
  createBackgroundRemovalJob,