@hyperframes/studio 0.8.102 → 0.8.103

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.
Files changed (44) hide show
  1. package/dist/assets/{hyperframes-player-BuRiFtrF.js → hyperframes-player-DG795Hwu.js} +1 -1
  2. package/dist/assets/{index-C2utDZt-.js → index-BPJRf2X9.js} +118 -118
  3. package/dist/assets/{index-BT3sauee.js → index-BiEtDkae.js} +1 -1
  4. package/dist/assets/{index-xB5O0hPw.css → index-CQN3M56N.css} +1 -1
  5. package/dist/assets/{index-BM35fjFf.js → index-DuCZ3pQw.js} +1 -1
  6. package/dist/{chunk-MTAVNAGW.js → chunk-46XP7J3B.js} +2 -2
  7. package/dist/{chunk-S65AQH6M.js → chunk-DI4LCJHL.js} +3 -2
  8. package/dist/chunk-DI4LCJHL.js.map +1 -0
  9. package/dist/{chunk-7X4XRP3L.js → chunk-HIGHVWYD.js} +2 -2
  10. package/dist/{domEditingLayers-NU6Z5QWA.js → domEditingLayers-AQBDFNUC.js} +3 -3
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.html +2 -2
  13. package/dist/index.js +122 -84
  14. package/dist/index.js.map +1 -1
  15. package/dist/player.js +2 -2
  16. package/dist/styles.css +1 -1
  17. package/package.json +8 -8
  18. package/src/components/editor/DomEditOverlay.tsx +2 -1
  19. package/src/components/editor/PropertyPanel.tsx +2 -2
  20. package/src/components/editor/domEditOverlayGestures.ts +1 -1
  21. package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
  22. package/src/components/editor/manualOffsetDrag.test.ts +16 -0
  23. package/src/components/editor/manualOffsetDrag.ts +5 -5
  24. package/src/components/editor/plainTranslate.test.ts +38 -22
  25. package/src/components/editor/plainTranslate.ts +66 -36
  26. package/src/components/editor/previewReadOnly.overlay.test.tsx +30 -0
  27. package/src/components/editor/previewReadOnlyContext.tsx +3 -1
  28. package/src/components/editor/propertyPanelTransformCommit.test.ts +51 -0
  29. package/src/components/editor/propertyPanelTransformCommit.ts +3 -1
  30. package/src/components/editor/useColorGradingController.test.ts +36 -1
  31. package/src/components/editor/useColorGradingController.ts +2 -2
  32. package/src/components/editor/useDomEditNudge.test.tsx +1 -0
  33. package/src/components/editor/useDomEditNudge.ts +6 -3
  34. package/src/hooks/usePreviewInteraction.test.ts +32 -0
  35. package/src/hooks/usePreviewInteraction.ts +3 -2
  36. package/src/player/components/TimelineClipFades.test.tsx +103 -3
  37. package/src/player/components/TimelineClipFades.tsx +53 -50
  38. package/src/styles/theme.css +1 -1
  39. package/src/utils/projectRouting.test.ts +9 -0
  40. package/src/utils/projectRouting.ts +3 -1
  41. package/dist/chunk-S65AQH6M.js.map +0 -1
  42. /package/dist/{chunk-MTAVNAGW.js.map → chunk-46XP7J3B.js.map} +0 -0
  43. /package/dist/{chunk-7X4XRP3L.js.map → chunk-HIGHVWYD.js.map} +0 -0
  44. /package/dist/{domEditingLayers-NU6Z5QWA.js.map → domEditingLayers-AQBDFNUC.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import{n as Qi}from"./index-C2utDZt-.js";/*!
1
+ import{n as Qi}from"./index-BPJRf2X9.js";/*!
2
2
  * Copyright (c) 2026-present, Vanilagy and contributors
3
3
  *
4
4
  * This Source Code Form is subject to the terms of the Mozilla Public
@@ -2,7 +2,7 @@ import {
2
2
  buildProjectApiPath,
3
3
  getSourceScopedSelectorIndex,
4
4
  withSelectorIndexPass
5
- } from "./chunk-S65AQH6M.js";
5
+ } from "./chunk-DI4LCJHL.js";
6
6
 
7
7
  // src/components/editor/probeSourceElement.ts
8
8
  async function probeSourceElement(projectId, sourceFile, target) {
@@ -1061,4 +1061,4 @@ export {
1061
1061
  readHfId,
1062
1062
  buildDomEditPatchTarget
1063
1063
  };
1064
- //# sourceMappingURL=chunk-MTAVNAGW.js.map
1064
+ //# sourceMappingURL=chunk-46XP7J3B.js.map
@@ -40,8 +40,9 @@ function getSourceScopedSelectorIndex(doc, el, selector, sourceFile, resolveSour
40
40
 
41
41
  // src/utils/projectRouting.ts
42
42
  var PROJECT_HASH_PREFIX = "#project/";
43
+ var WINDOWS_DRIVE_PREFIX = /^[a-z]:/i;
43
44
  function isValidProjectId(value) {
44
- return value.length > 0 && value !== "." && value !== ".." && !value.includes(":") && !value.includes("/") && !value.includes("\\") && !Array.from(value).some((char) => char.charCodeAt(0) < 32);
45
+ return value.length > 0 && value !== "." && value !== ".." && !WINDOWS_DRIVE_PREFIX.test(value) && !value.includes("/") && !value.includes("\\") && !Array.from(value).some((char) => char.charCodeAt(0) < 32);
45
46
  }
46
47
  function decodeHashProjectId(value) {
47
48
  try {
@@ -98,4 +99,4 @@ export {
98
99
  withSelectorIndexPass,
99
100
  getSourceScopedSelectorIndex
100
101
  };
101
- //# sourceMappingURL=chunk-S65AQH6M.js.map
102
+ //# sourceMappingURL=chunk-DI4LCJHL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/sourceScopedSelectorIndex.ts","../src/utils/projectRouting.ts"],"sourcesContent":["/**\n * Occurrence index for a selector within its source document.\n *\n * The preview flattens multiple composition files into one DOM, so a raw\n * `querySelectorAll` index is not a stable source-file identity. Callers supply\n * their existing source resolver; this helper alone owns occurrence scoping.\n */\n\n/** Every element matching one selector, mapped to the source file it belongs to\n * and its occurrence index within that file. One document query fills it. */\ntype SelectorOccurrences = Map<Element, { scope: string; index: number }>;\n\ninterface SelectorIndexPass {\n doc: Document;\n bySelector: Map<string, SelectorOccurrences>;\n}\n\nlet activePass: SelectorIndexPass | null = null;\n\n/**\n * Share one occurrence index per selector across every\n * `getSourceScopedSelectorIndex` call made synchronously inside `run`.\n *\n * Unshared, each call runs its own whole-document `querySelectorAll(selector)`\n * and resolves the source file of every match. A rebuild that walks n elements\n * sharing a selector therefore does n whole-document queries and n² resolver\n * calls — a composition of repeated cards or tiles is exactly that shape. One\n * index per selector makes the same pass linear.\n *\n * Correctness rests on the pass being ONE synchronous walk of a document that\n * does not change under it: nothing invalidates the index, so `run` must not\n * mutate `doc`, and every call inside it must resolve source files the same way\n * (the resolver may be a fresh closure per call, as long as it answers the\n * same). Outside a pass the index is per-call, exactly as before.\n */\nexport function withSelectorIndexPass<T>(doc: Document, run: () => T): T {\n const previous = activePass;\n activePass = { doc, bySelector: new Map() };\n try {\n return run();\n } finally {\n activePass = previous;\n }\n}\n\nfunction buildOccurrences(\n doc: Document,\n selector: string,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): SelectorOccurrences {\n const occurrences: SelectorOccurrences = new Map();\n const counts = new Map<string, number>();\n for (const candidate of doc.querySelectorAll(selector)) {\n const scope = resolveSourceFile(candidate) ?? \"index.html\";\n const index = counts.get(scope) ?? 0;\n counts.set(scope, index + 1);\n occurrences.set(candidate, { scope, index });\n }\n return occurrences;\n}\n\nexport function getSourceScopedSelectorIndex(\n doc: Document,\n el: Element,\n selector: string | undefined,\n sourceFile: string | undefined,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): number | undefined {\n if (!selector || selector.startsWith(\"#\") || selector.startsWith(\"[data-composition-id=\")) {\n return undefined;\n }\n\n try {\n const pass = activePass?.doc === doc ? activePass : null;\n let occurrences = pass?.bySelector.get(selector);\n if (!occurrences) {\n occurrences = buildOccurrences(doc, selector, resolveSourceFile);\n pass?.bySelector.set(selector, occurrences);\n }\n // An element outside the requested scope is not in that scope's occurrence\n // run at all, which is the `indexOf` miss the filtered form returned before.\n const hit = occurrences.get(el);\n return hit && hit.scope === (sourceFile ?? \"index.html\") ? hit.index : undefined;\n } catch {\n return undefined;\n }\n}\n","const PROJECT_HASH_PREFIX = \"#project/\";\n\nexport interface ProjectHashRoute {\n projectId: string;\n params: URLSearchParams;\n}\n\nconst WINDOWS_DRIVE_PREFIX = /^[a-z]:/i;\n\n/** Project names are single path segments, including when received from a hash or server. */\nexport function isValidProjectId(value: string): boolean {\n return (\n value.length > 0 &&\n value !== \".\" &&\n value !== \"..\" &&\n !WINDOWS_DRIVE_PREFIX.test(value) &&\n !value.includes(\"/\") &&\n !value.includes(\"\\\\\") &&\n !Array.from(value).some((char) => char.charCodeAt(0) < 32)\n );\n}\n\nfunction decodeHashProjectId(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeHashParams(\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): URLSearchParams {\n if (!params) return new URLSearchParams();\n if (params instanceof URLSearchParams) return params;\n\n const next = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (!key || value == null || value === \"\") continue;\n next.set(key, value);\n }\n return next;\n}\n\nexport function encodeProjectId(projectId: string): string {\n if (!isValidProjectId(projectId)) throw new Error(\"Invalid project ID\");\n return encodeURIComponent(projectId);\n}\n\nexport function buildProjectHash(\n projectId: string,\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): string {\n const search = normalizeHashParams(params).toString();\n return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : \"\"}`;\n}\n\nexport function parseProjectHashRoute(hash: string): ProjectHashRoute | null {\n if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;\n\n const route = hash.slice(PROJECT_HASH_PREFIX.length);\n const queryIndex = route.indexOf(\"?\");\n const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;\n if (!encodedProjectId || encodedProjectId.includes(\"/\")) return null;\n\n const projectId = decodeHashProjectId(encodedProjectId);\n if (!isValidProjectId(projectId)) return null;\n\n const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : \"\";\n return {\n projectId,\n params: new URLSearchParams(rawParams),\n };\n}\n\nexport function parseProjectIdFromHash(hash: string): string | null {\n return parseProjectHashRoute(hash)?.projectId ?? null;\n}\n\nexport function buildProjectApiPath(projectId: string, suffix = \"\"): string {\n const normalizedSuffix = suffix && !suffix.startsWith(\"/\") ? `/${suffix}` : suffix;\n return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  buildProjectApiPath,
6
6
  getSourceScopedSelectorIndex
7
- } from "./chunk-S65AQH6M.js";
7
+ } from "./chunk-DI4LCJHL.js";
8
8
 
9
9
  // src/player/lib/time.ts
10
10
  var STUDIO_PREVIEW_FPS = 30;
@@ -1890,4 +1890,4 @@ export {
1890
1890
  whenPreviewBooted,
1891
1891
  Player
1892
1892
  };
1893
- //# sourceMappingURL=chunk-7X4XRP3L.js.map
1893
+ //# sourceMappingURL=chunk-HIGHVWYD.js.map
@@ -20,8 +20,8 @@ import {
20
20
  resolveDomEditCapabilities,
21
21
  resolveDomEditSelection,
22
22
  serializeDomEditTextFields
23
- } from "./chunk-MTAVNAGW.js";
24
- import "./chunk-S65AQH6M.js";
23
+ } from "./chunk-46XP7J3B.js";
24
+ import "./chunk-DI4LCJHL.js";
25
25
  import "./chunk-PZ5AY32C.js";
26
26
  export {
27
27
  buildDefaultDomEditTextField,
@@ -46,4 +46,4 @@ export {
46
46
  resolveDomEditSelection,
47
47
  serializeDomEditTextFields
48
48
  };
49
- //# sourceMappingURL=domEditingLayers-NU6Z5QWA.js.map
49
+ //# sourceMappingURL=domEditingLayers-AQBDFNUC.js.map
package/dist/index.d.ts CHANGED
@@ -3089,7 +3089,7 @@ declare function useDomEditSession({ projectId, activeCompPath, compIdToSrc, cap
3089
3089
  getGsapAnimationsForSelection: (selection: DomEditSelection) => _hyperframes_parsers.GsapAnimation[] | Promise<_hyperframes_parsers.GsapAnimation[]>;
3090
3090
  handleAskAgent: () => void;
3091
3091
  handleAgentModalSubmit: (userInstruction: string) => Promise<void>;
3092
- handleBlockedDomMove: (selection: DomEditSelection) => void;
3092
+ handleBlockedDomMove: (selection: DomEditSelection, reason?: string) => void;
3093
3093
  handleDomManualDragStart: () => void;
3094
3094
  handleDomEditElementDelete: (selection: DomEditSelection, options?: {
3095
3095
  expandGroup?: boolean;
@@ -3450,7 +3450,7 @@ interface DomEditOverlayProps {
3450
3450
  revealPanel?: boolean;
3451
3451
  additive?: boolean;
3452
3452
  }) => void;
3453
- onBlockedMove: (selection: DomEditSelection) => void;
3453
+ onBlockedMove: (selection: DomEditSelection, reason?: string) => void;
3454
3454
  onManualDragStart?: () => void;
3455
3455
  onPathOffsetCommit: (selection: DomEditSelection, next: {
3456
3456
  x: number;
package/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-C2utDZt-.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-xB5O0hPw.css">
8
+ <script type="module" crossorigin src="/assets/index-BPJRf2X9.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CQN3M56N.css">
10
10
  </head>
11
11
  <body>
12
12
  <div data-hf-id="hf-aph5" id="root"></div>
package/dist/index.js CHANGED
@@ -67,7 +67,7 @@ import {
67
67
  whenPreviewBooted,
68
68
  wrapTimeline,
69
69
  writeStudioUiPreferences
70
- } from "./chunk-7X4XRP3L.js";
70
+ } from "./chunk-HIGHVWYD.js";
71
71
  import {
72
72
  Button,
73
73
  ContextMenu,
@@ -133,13 +133,13 @@ import {
133
133
  resolveDomEditSelection,
134
134
  serializeDomEditTextFields,
135
135
  setCompositionSourceMap
136
- } from "./chunk-MTAVNAGW.js";
136
+ } from "./chunk-46XP7J3B.js";
137
137
  import {
138
138
  buildProjectApiPath,
139
139
  buildProjectHash,
140
140
  parseProjectHashRoute,
141
141
  parseProjectIdFromHash
142
- } from "./chunk-S65AQH6M.js";
142
+ } from "./chunk-DI4LCJHL.js";
143
143
  import {
144
144
  __export
145
145
  } from "./chunk-PZ5AY32C.js";
@@ -13768,11 +13768,7 @@ import { Fragment as Fragment17, useId as useId4, useMemo as useMemo30 } from "r
13768
13768
  import { memo as memo10 } from "react";
13769
13769
 
13770
13770
  // src/player/components/TimelineClipFades.tsx
13771
- import {
13772
- useEffect as useEffect21,
13773
- useRef as useRef28,
13774
- useState as useState16
13775
- } from "react";
13771
+ import { useEffect as useEffect21, useRef as useRef28, useState as useState16 } from "react";
13776
13772
  import {
13777
13773
  HF_AUDIO_FADE_IN_ATTR,
13778
13774
  HF_AUDIO_FADE_OUT_ATTR,
@@ -13781,7 +13777,9 @@ import {
13781
13777
  } from "@hyperframes/core/audio-fade";
13782
13778
  import { Fragment as Fragment10, jsx as jsx29, jsxs as jsxs16 } from "react/jsx-runtime";
13783
13779
  var HANDLE_SIZE = 10;
13784
- var HANDLE_HIT = 16;
13780
+ var HANDLE_HIT = 24;
13781
+ var HANDLE_Z_ABOVE_CLIP_CONTENT = 30;
13782
+ var SUPPRESS_CLIP_NATIVE_TITLE = "";
13785
13783
  var DRAG_THRESHOLD_PX = 2;
13786
13784
  function TimelineClipFades({ el, pps, widthPx, showHandles }) {
13787
13785
  const { onSetElementAttributeLive, onSetElementAttributeQuiet, onRevertElementAttributeLive } = useTimelineEditContextOptional();
@@ -13868,29 +13866,36 @@ function TimelineClipFades({ el, pps, widthPx, showHandles }) {
13868
13866
  g.edge === "in" ? "Fade in" : "Fade out"
13869
13867
  );
13870
13868
  };
13871
- const onHandleKeyDown = (e) => {
13872
- const g = gesture.current;
13873
- if (e.key !== "Escape" || !g) return;
13874
- e.preventDefault();
13875
- gesture.current = null;
13876
- setDragging(null);
13877
- revertGesture(g);
13878
- };
13869
+ useEffect21(() => {
13870
+ if (dragging === null) return;
13871
+ const cancelOnWindowEscape = (e) => {
13872
+ const g = gesture.current;
13873
+ if (e.key !== "Escape" || !g) return;
13874
+ e.preventDefault();
13875
+ e.stopPropagation();
13876
+ gesture.current = null;
13877
+ setDragging(null);
13878
+ revertGesture(g);
13879
+ };
13880
+ window.addEventListener("keydown", cancelOnWindowEscape, { capture: true });
13881
+ return () => window.removeEventListener("keydown", cancelOnWindowEscape, { capture: true });
13882
+ });
13879
13883
  const showIn = fades.fadeIn > 0;
13880
13884
  const showOut = fades.fadeOut > 0;
13881
13885
  const handlesVisible = showHandles || dragging !== null;
13882
13886
  if (!showIn && !showOut && !handlesVisible) return null;
13883
- const handleStyle = (leftPx) => ({
13887
+ const hitWidth = Math.min(HANDLE_HIT, widthPx / 2);
13888
+ const handleStyle = (edge) => ({
13884
13889
  position: "absolute",
13885
13890
  top: -(HANDLE_HIT - HANDLE_SIZE) / 2 + 1,
13886
- left: leftPx - HANDLE_HIT / 2,
13887
- width: HANDLE_HIT,
13891
+ left: edge === "in" ? 0 : widthPx - hitWidth,
13892
+ width: hitWidth,
13888
13893
  height: HANDLE_HIT,
13889
13894
  display: "flex",
13890
13895
  alignItems: "center",
13891
13896
  justifyContent: "center",
13892
13897
  cursor: "ew-resize",
13893
- zIndex: 6,
13898
+ zIndex: HANDLE_Z_ABOVE_CLIP_CONTENT,
13894
13899
  opacity: handlesVisible ? 1 : 0,
13895
13900
  pointerEvents: handlesVisible && canEdit ? "auto" : "none",
13896
13901
  touchAction: "none",
@@ -13909,7 +13914,8 @@ function TimelineClipFades({ el, pps, widthPx, showHandles }) {
13909
13914
  height: "100%",
13910
13915
  pointerEvents: "none",
13911
13916
  zIndex: 5,
13912
- overflow: "visible"
13917
+ overflow: "hidden",
13918
+ borderRadius: "inherit"
13913
13919
  },
13914
13920
  viewBox: `0 0 ${Math.max(widthPx, 1)} 100`,
13915
13921
  preserveAspectRatio: "none",
@@ -13972,13 +13978,12 @@ function TimelineClipFades({ el, pps, widthPx, showHandles }) {
13972
13978
  direction: "in",
13973
13979
  value: fades.fadeIn,
13974
13980
  max: Math.max(0, el.duration - fades.fadeOut),
13975
- style: handleStyle(inPx),
13981
+ style: handleStyle("in"),
13976
13982
  dragging: dragging === "in",
13977
13983
  onPointerDown: onHandlePointerDown("in"),
13978
13984
  onPointerMove: onHandlePointerMove,
13979
13985
  onPointerUp: (e) => finish(e, false),
13980
- onPointerCancel: (e) => finish(e, true),
13981
- onKeyDown: onHandleKeyDown
13986
+ onPointerCancel: (e) => finish(e, true)
13982
13987
  }
13983
13988
  ),
13984
13989
  /* @__PURE__ */ jsx29(
@@ -13987,13 +13992,12 @@ function TimelineClipFades({ el, pps, widthPx, showHandles }) {
13987
13992
  direction: "out",
13988
13993
  value: fades.fadeOut,
13989
13994
  max: Math.max(0, el.duration - fades.fadeIn),
13990
- style: handleStyle(widthPx - outPx),
13995
+ style: handleStyle("out"),
13991
13996
  dragging: dragging === "out",
13992
13997
  onPointerDown: onHandlePointerDown("out"),
13993
13998
  onPointerMove: onHandlePointerMove,
13994
13999
  onPointerUp: (e) => finish(e, false),
13995
- onPointerCancel: (e) => finish(e, true),
13996
- onKeyDown: onHandleKeyDown
14000
+ onPointerCancel: (e) => finish(e, true)
13997
14001
  }
13998
14002
  )
13999
14003
  ] })
@@ -14008,11 +14012,10 @@ function FadeHandle({
14008
14012
  onPointerDown,
14009
14013
  onPointerMove,
14010
14014
  onPointerUp,
14011
- onPointerCancel,
14012
- onKeyDown
14015
+ onPointerCancel
14013
14016
  }) {
14014
14017
  const label = direction === "in" ? "Fade in" : "Fade out";
14015
- return /* @__PURE__ */ jsx29(
14018
+ return /* @__PURE__ */ jsx29(Tooltip, { label: `${label} ${formatFadeSeconds(value)} s`, children: /* @__PURE__ */ jsx29(
14016
14019
  "div",
14017
14020
  {
14018
14021
  role: "slider",
@@ -14023,18 +14026,17 @@ function FadeHandle({
14023
14026
  "aria-valuenow": value,
14024
14027
  "aria-valuetext": `${formatFadeSeconds(value)}s`,
14025
14028
  "data-testid": `clip-fade-handle-${direction}`,
14026
- title: `${label}: ${formatFadeSeconds(value)}s \u2014 drag to change`,
14029
+ title: SUPPRESS_CLIP_NATIVE_TITLE,
14027
14030
  style,
14028
14031
  onPointerDown,
14029
14032
  onPointerMove,
14030
14033
  onPointerUp,
14031
14034
  onPointerCancel,
14032
- onKeyDown,
14033
14035
  onClick: (e) => e.stopPropagation(),
14034
14036
  onDoubleClick: (e) => e.stopPropagation(),
14035
14037
  children: /* @__PURE__ */ jsx29(FadeDot, { active: dragging })
14036
14038
  }
14037
- );
14039
+ ) });
14038
14040
  }
14039
14041
  function FadeDot({ active }) {
14040
14042
  return /* @__PURE__ */ jsx29(
@@ -14043,10 +14045,12 @@ function FadeDot({ active }) {
14043
14045
  "aria-hidden": "true",
14044
14046
  style: {
14045
14047
  display: "block",
14048
+ flexShrink: 0,
14049
+ pointerEvents: "none",
14046
14050
  width: HANDLE_SIZE,
14047
14051
  height: HANDLE_SIZE,
14048
14052
  borderRadius: "50%",
14049
- background: active ? "var(--timeline-fade-dot-active)" : "var(--clip-handle)",
14053
+ background: "var(--timeline-fade-dot)",
14050
14054
  boxShadow: "0 0 0 1.5px var(--timeline-fade-dot-ring)",
14051
14055
  transform: active ? "scale(1.15)" : void 0,
14052
14056
  transition: "transform 80ms ease-out"
@@ -27804,10 +27808,11 @@ function usePreviewReadOnly() {
27804
27808
  function usePreviewReadOnlyReason() {
27805
27809
  return useContext8(PreviewReadOnlyContext).reason;
27806
27810
  }
27807
- function useManualEditDisabledFlags(capabilities) {
27811
+ function useManualEditDisabledFlags(capabilities, offset = { x: 0, y: 0 }) {
27808
27812
  const { readOnly } = useContext8(PreviewReadOnlyContext);
27813
+ const unreadable = !Number.isFinite(offset.x) || !Number.isFinite(offset.y);
27809
27814
  return {
27810
- manualOffsetEditingDisabled: !capabilities?.canApplyManualOffset || readOnly,
27815
+ manualOffsetEditingDisabled: !capabilities?.canApplyManualOffset || readOnly || unreadable,
27811
27816
  manualSizeEditingDisabled: !capabilities?.canApplyManualSize || readOnly,
27812
27817
  manualRotationEditingDisabled: !capabilities?.canApplyManualRotation || readOnly
27813
27818
  };
@@ -31301,7 +31306,7 @@ function OffCanvasIndicators({
31301
31306
  const selectOffCanvas = async () => {
31302
31307
  const el = elements.current.get(r.key);
31303
31308
  if (!el) return;
31304
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-NU6Z5QWA.js");
31309
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-AQBDFNUC.js");
31305
31310
  const acp = activeCompositionPathRef.current ?? "index.html";
31306
31311
  const sel = await resolveDomEditSelection2(el, {
31307
31312
  activeCompositionPath: acp,
@@ -32677,49 +32682,75 @@ function scanAllRuntimeKeyframes(iframe, clipById) {
32677
32682
  }
32678
32683
 
32679
32684
  // src/components/editor/plainTranslate.ts
32680
- var TERM = /^(-?\d*\.?\d+(?:e[-+]?\d+)?)(px|%)$/i;
32681
- var SIGN = { "+": 1, "-": -1 };
32682
- function termPx(term, side) {
32683
- const match = TERM.exec(term ?? "");
32684
- return match ? Number(match[1]) * (match[2] === "%" ? side / 100 : 1) : null;
32685
- }
32686
- function resolveLength(value, side) {
32687
- const parts = (/^calc\((.*)\)$/.exec(value)?.[1] ?? value).split(" ");
32688
- if (parts.length % 2 === 0) return null;
32689
- let total = termPx(parts[0], side);
32690
- for (let i = 1; i < parts.length && total !== null; i += 2) {
32691
- const term = termPx(parts[i + 1], side);
32692
- const sign = SIGN[parts[i] ?? ""];
32693
- total = term === null || !sign ? null : total + sign * term;
32694
- }
32695
- return total;
32696
- }
32697
- function borderBox(cs, size, a, b) {
32685
+ var TOKEN = /\s*(?:(\d*\.?\d+(?:e[-+]?\d+)?)(px|%)?|([a-z]+)\(|([-+*/(),]))/giy;
32686
+ var FUNCTIONS = {
32687
+ "(": ([a = Number.NaN]) => a,
32688
+ "calc(": ([a = Number.NaN]) => a,
32689
+ "min(": (args) => Math.min(...args),
32690
+ "max(": (args) => Math.max(...args),
32691
+ "clamp(": ([lo = Number.NaN, v = Number.NaN, hi = Number.NaN]) => Math.max(lo, Math.min(v, hi))
32692
+ };
32693
+ function tokenize(value, side) {
32694
+ const tokens = [];
32695
+ const end = value.trimEnd().length;
32696
+ TOKEN.lastIndex = 0;
32697
+ for (let m = TOKEN.exec(value); m; m = TOKEN.lastIndex < end ? TOKEN.exec(value) : null) {
32698
+ if (m[1]) tokens.push(Number(m[1]) * (m[2] === "%" ? side / 100 : 1));
32699
+ else tokens.push(m[3] ? `${m[3].toLowerCase()}(` : m[4] ?? "");
32700
+ }
32701
+ return TOKEN.lastIndex < end ? [] : tokens;
32702
+ }
32703
+ function evaluateLength(value, side) {
32704
+ const tokens = tokenize(value, side);
32705
+ let i = 0;
32706
+ const next = () => tokens[i++];
32707
+ const take = (...ops) => ops.includes(tokens[i]) && next();
32708
+ function sum() {
32709
+ let total2 = product();
32710
+ for (let op = take("+", "-"); op; op = take("+", "-"))
32711
+ total2 += (op === "+" ? 1 : -1) * product();
32712
+ return total2;
32713
+ }
32714
+ function product() {
32715
+ let total2 = unary();
32716
+ for (let op = take("*", "/"); op; op = take("*", "/"))
32717
+ total2 = op === "*" ? total2 * unary() : total2 / unary();
32718
+ return total2;
32719
+ }
32720
+ function unary() {
32721
+ return take("-") ? -unary() : atom();
32722
+ }
32723
+ function atom() {
32724
+ const token = next();
32725
+ if (typeof token === "number") return token;
32726
+ const fn = FUNCTIONS[token ?? ""];
32727
+ if (!fn) return Number.NaN;
32728
+ const args = [sum()];
32729
+ while (take(",")) args.push(sum());
32730
+ return take(")") ? fn(args) : Number.NaN;
32731
+ }
32732
+ const total = tokens.length ? sum() : Number.NaN;
32733
+ return i === tokens.length ? total : Number.NaN;
32734
+ }
32735
+ var CONTENT_BOXES = /* @__PURE__ */ new Set(["content-box", "fill-box"]);
32736
+ function referenceBox(cs, size, a, b) {
32698
32737
  const n = (prop) => Number.parseFloat(cs.getPropertyValue(prop)) || 0;
32699
32738
  const pad = n(`padding-${a}`) + n(`padding-${b}`) + n(`border-${a}-width`) + n(`border-${b}-width`);
32700
- return n(size) + (cs.boxSizing === "border-box" ? 0 : pad);
32701
- }
32702
- function resolveInTransform(el, x, y) {
32703
- const style = el.style;
32704
- const saved2 = [style.getPropertyValue("transform"), style.getPropertyPriority("transform")];
32705
- style.setProperty("transform", `translate(${x}, ${y})`, "important");
32706
- try {
32707
- const resolved2 = el.ownerDocument.defaultView?.getComputedStyle(el).transform ?? "";
32708
- const matrix = new DOMMatrixReadOnly(resolved2.startsWith("matrix") ? resolved2 : void 0);
32709
- return { x: matrix.m41, y: matrix.m42 };
32710
- } finally {
32711
- style.setProperty("transform", saved2[0] ?? "", saved2[1] ?? "");
32712
- }
32739
+ const content = n(size) - (cs.boxSizing === "border-box" ? pad : 0);
32740
+ return CONTENT_BOXES.has(cs.getPropertyValue("transform-box")) ? content : content + pad;
32713
32741
  }
32714
32742
  function readTranslatePx(el) {
32715
32743
  const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
32716
- const value = el.style.getPropertyValue("translate") || cs?.translate || "none";
32744
+ const value = cs?.translate || el.style.getPropertyValue("translate") || "none";
32717
32745
  if (value === "none") return { x: 0, y: 0 };
32718
32746
  const [x = "0px", y = "0px"] = splitTopLevelWhitespace(value);
32719
- const px = cs && resolveLength(x, borderBox(cs, "width", "left", "right"));
32720
- const py = cs && resolveLength(y, borderBox(cs, "height", "top", "bottom"));
32721
- return px != null && py != null ? { x: px, y: py } : resolveInTransform(el, x, y);
32747
+ const side = (size, a, b) => cs ? referenceBox(cs, size, a, b) : 0;
32748
+ return {
32749
+ x: evaluateLength(x, side("width", "left", "right")),
32750
+ y: evaluateLength(y, side("height", "top", "bottom"))
32751
+ };
32722
32752
  }
32753
+ var UNREADABLE_TRANSLATE = "Studio can't read this layer's translate. Move it in the Code tab.";
32723
32754
  function formatTranslatePx(p) {
32724
32755
  return `${roundTo3(p.x)}px ${roundTo3(p.y)}px`;
32725
32756
  }
@@ -32907,6 +32938,8 @@ function resolveManualOffsetForPointerDelta(input) {
32907
32938
  function createManualOffsetDragMember(input) {
32908
32939
  const plainTranslate = !!input.gesture && !gsapWritesPosition(input.element);
32909
32940
  const initialOffset = plainTranslate ? readTranslatePx(input.element) : readAppliedStudioPathOffset(input.element);
32941
+ if (!finitePoint(initialOffset))
32942
+ return { ok: false, reason: UNREADABLE_TRANSLATE, selection: input.selection };
32910
32943
  const win = input.element.ownerDocument.defaultView;
32911
32944
  const gsapX = !plainTranslate && win?.gsap?.getProperty?.(input.element, "x") || 0;
32912
32945
  const gsapY = !plainTranslate && win?.gsap?.getProperty?.(input.element, "y") || 0;
@@ -33707,7 +33740,7 @@ function startGroupDrag(e, opts) {
33707
33740
  restoreManualOffsetDragMembers(members);
33708
33741
  e.preventDefault();
33709
33742
  e.stopPropagation();
33710
- opts.onBlockedMoveRef.current(result.selection);
33743
+ opts.onBlockedMoveRef.current(result.selection, result.reason);
33711
33744
  return false;
33712
33745
  }
33713
33746
  members.push(result.member);
@@ -33799,7 +33832,7 @@ function startGesture(kind, e, opts, options) {
33799
33832
  gesture: "drag"
33800
33833
  });
33801
33834
  if (!result.ok) {
33802
- opts.onBlockedMoveRef.current(result.selection);
33835
+ opts.onBlockedMoveRef.current(result.selection, result.reason);
33803
33836
  return false;
33804
33837
  }
33805
33838
  pathOffsetMember = result.member;
@@ -34538,7 +34571,7 @@ function useDomEditNudge(params) {
34538
34571
  restoreManualOffsetDragMembers(session.members);
34539
34572
  endManualOffsetDragMembers(session.members);
34540
34573
  }, [params.allowCanvasMovement]);
34541
- const beginSession = () => {
34574
+ const beginSession = (event) => {
34542
34575
  const p = paramsRef.current;
34543
34576
  const groupItems = p.groupOverlayItemsRef.current;
34544
34577
  const isGroup = groupItems.length > 1;
@@ -34549,7 +34582,8 @@ function useDomEditNudge(params) {
34549
34582
  const result = createManualOffsetDragMember({ ...target, gesture: "nudge" });
34550
34583
  if (!result.ok) {
34551
34584
  restoreManualOffsetDragMembers(members);
34552
- return null;
34585
+ if (!event.repeat) p.onBlockedMoveRef.current(result.selection, result.reason);
34586
+ return "refused";
34553
34587
  }
34554
34588
  members.push(result.member);
34555
34589
  }
@@ -34562,7 +34596,8 @@ function useDomEditNudge(params) {
34562
34596
  if (shouldIgnoreNudgeKey(p, event)) return;
34563
34597
  const delta = resolveCanvasNudgeDelta(event);
34564
34598
  if (!delta) return;
34565
- const session = sessionRef.current ?? beginSession();
34599
+ const session = sessionRef.current ?? beginSession(event);
34600
+ if (session === "refused") return void event.preventDefault();
34566
34601
  if (!session) return;
34567
34602
  sessionRef.current = session;
34568
34603
  event.preventDefault();
@@ -36827,6 +36862,7 @@ var DomEditOverlay = memo25(function DomEditOverlay2({
36827
36862
  gestureRef,
36828
36863
  groupGestureRef,
36829
36864
  blockedMoveRef,
36865
+ onBlockedMoveRef,
36830
36866
  onManualDragStartRef,
36831
36867
  onPathOffsetCommitRef,
36832
36868
  onGroupPathOffsetCommitRef
@@ -47988,6 +48024,8 @@ function createTransformCommitHandlers({
47988
48024
  ))
47989
48025
  return;
47990
48026
  const current = readMoveOffset(element.element);
48027
+ if (!Number.isFinite(current.x) || !Number.isFinite(current.y))
48028
+ return void showToast?.(UNREADABLE_TRANSLATE);
47991
48029
  await Promise.resolve(
47992
48030
  onSetManualOffset(element, {
47993
48031
  x: axis === "x" ? parsed : current.x,
@@ -49603,13 +49641,13 @@ function useColorGradingController({
49603
49641
  useEffect69(() => {
49604
49642
  setMediaMetadata(null);
49605
49643
  if (!selectedAssetPath) return;
49606
- const cacheKey = `${projectId}:${selectedAssetPath}`;
49644
+ const cacheKey = mediaMetadataUrl(projectId, selectedAssetPath);
49607
49645
  if (MEDIA_METADATA_CACHE.has(cacheKey)) {
49608
49646
  setMediaMetadata(MEDIA_METADATA_CACHE.get(cacheKey) ?? null);
49609
49647
  return;
49610
49648
  }
49611
49649
  const controller = new AbortController();
49612
- fetch(mediaMetadataUrl(projectId, selectedAssetPath), { signal: controller.signal }).then(async (response) => {
49650
+ fetch(cacheKey, { signal: controller.signal }).then(async (response) => {
49613
49651
  if (!response.ok) return { ok: false };
49614
49652
  const data = await response.json();
49615
49653
  return { ok: true, metadata: data?.metadata ?? null };
@@ -65060,7 +65098,8 @@ var PropertyPanel = memo36(function PropertyPanel2(props) {
65060
65098
  scale: 1,
65061
65099
  transformPerspective: 0
65062
65100
  };
65063
- const { manualOffsetEditingDisabled, manualSizeEditingDisabled, manualRotationEditingDisabled } = useManualEditDisabledFlags(element?.capabilities);
65101
+ const manualOffset = element ? readMoveOffset(element.element) : { x: 0, y: 0 };
65102
+ const { manualOffsetEditingDisabled, manualSizeEditingDisabled, manualRotationEditingDisabled } = useManualEditDisabledFlags(element?.capabilities, manualOffset);
65064
65103
  if (!element) {
65065
65104
  return /* @__PURE__ */ jsx175(
65066
65105
  PropertyPanelEmptyState,
@@ -65077,7 +65116,6 @@ var PropertyPanel = memo36(function PropertyPanel2(props) {
65077
65116
  const sourceLabel = element.id ? `#${element.id}` : element.selector ?? "";
65078
65117
  const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
65079
65118
  const showEditableSections = element.capabilities.canEditStyles && sections.style;
65080
- const manualOffset = readMoveOffset(element.element);
65081
65119
  const manualSize = readStudioBoxSize(element.element);
65082
65120
  const resolvedWidth = manualSize.width > 0 ? manualSize.width : parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width;
65083
65121
  const resolvedHeight = manualSize.height > 0 ? manualSize.height : parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height;
@@ -73027,9 +73065,9 @@ function usePreviewInteraction({
73027
73065
  updateDomEditHoverSelection(null);
73028
73066
  }, [updateDomEditHoverSelection]);
73029
73067
  const handleBlockedDomMove = useCallback120(
73030
- (selection) => {
73068
+ (selection, reason) => {
73031
73069
  showToast(
73032
- selection.capabilities.reasonIfDisabled ?? "This element can't be adjusted directly from the preview.",
73070
+ reason ?? selection.capabilities.reasonIfDisabled ?? "This element can't be adjusted directly from the preview.",
73033
73071
  "info"
73034
73072
  );
73035
73073
  },