@hyperframes/studio 0.8.106 → 0.8.107

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 (110) hide show
  1. package/dist/assets/{hyperframes-player-dl6K6ABp.js → hyperframes-player-h4mktSdm.js} +1 -1
  2. package/dist/assets/{index-CMiYVGv5.js → index-CM1xA_jD.js} +195 -200
  3. package/dist/assets/{index-ooaZZhKf.js → index-D8GKc2D9.js} +1 -1
  4. package/dist/assets/{index-DKT5NF1Y.js → index-zoqPmBF6.js} +1 -1
  5. package/dist/{chunk-ICTZJPQQ.js → chunk-4OZC426H.js} +12 -3
  6. package/dist/chunk-4OZC426H.js.map +1 -0
  7. package/dist/index.d.ts +367 -11
  8. package/dist/index.html +1 -1
  9. package/dist/index.js +1064 -714
  10. package/dist/index.js.map +1 -1
  11. package/dist/player.js +1 -1
  12. package/package.json +8 -8
  13. package/src/App.tsx +5 -3
  14. package/src/agentToolsExport.test.ts +8 -0
  15. package/src/captions/keyboard.test.ts +6 -0
  16. package/src/components/StudioRightPanels.tsx +2 -0
  17. package/src/components/TimelineToolbar.test.tsx +27 -0
  18. package/src/components/TimelineToolbar.tsx +2 -5
  19. package/src/components/editor/DomEditOverlay.tsx +12 -18
  20. package/src/components/editor/PropertyPanel.tsx +1 -14
  21. package/src/components/editor/PropertyPanelFlat.tsx +3 -6
  22. package/src/components/editor/SnapToolbar.test.tsx +32 -1
  23. package/src/components/editor/SnapToolbar.tsx +3 -4
  24. package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
  25. package/src/components/editor/fontAssets.test.ts +12 -22
  26. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +96 -33
  27. package/src/components/editor/gsapAnimatesProperty.ts +3 -0
  28. package/src/components/editor/manualEdits.test.ts +4 -3
  29. package/src/components/editor/manualEditsDom.ts +28 -5
  30. package/src/components/editor/manualEditsDomPatches.ts +5 -1
  31. package/src/components/editor/manualOffsetDrag.ts +4 -1
  32. package/src/components/editor/mediaAudioEdits.test.ts +212 -0
  33. package/src/components/editor/mediaAudioEdits.ts +253 -0
  34. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +286 -87
  35. package/src/components/editor/propertyPanelFlatMediaSection.tsx +17 -20
  36. package/src/components/editor/propertyPanelFlatProps.ts +1 -0
  37. package/src/components/editor/propertyPanelMediaSection.tsx +17 -27
  38. package/src/components/editor/propertyPanelTypes.ts +2 -0
  39. package/src/components/editor/useDomEditNudge.test.tsx +134 -2
  40. package/src/components/editor/useDomEditNudge.ts +41 -23
  41. package/src/components/panels/SlideshowPanel.test.ts +27 -0
  42. package/src/components/panels/SlideshowPanel.tsx +12 -7
  43. package/src/contexts/DomEditContext.test.tsx +62 -0
  44. package/src/contexts/DomEditContext.tsx +94 -77
  45. package/src/hooks/canvasQuickSaves.test.tsx +72 -7
  46. package/src/hooks/domEditCommitTypes.ts +10 -0
  47. package/src/hooks/domEditPersistFailure.ts +12 -1
  48. package/src/hooks/domStyleCommit.fonts.test.ts +5 -4
  49. package/src/hooks/domStyleCommit.ts +2 -8
  50. package/src/hooks/gsapDragCommit.ts +3 -8
  51. package/src/hooks/gsapDragPositionCommit.ts +6 -1
  52. package/src/hooks/gsapEditOutcome.ts +2 -1
  53. package/src/hooks/gsapResizeElementSize.test.tsx +136 -0
  54. package/src/hooks/gsapResizeGeometrySweep.test.ts +3 -1
  55. package/src/hooks/gsapResizeIntercept.test.ts +3 -11
  56. package/src/hooks/gsapResizeIntercept.ts +6 -2
  57. package/src/hooks/gsapResizeMixedTween.test.ts +1 -2
  58. package/src/hooks/gsapRuntimeBridge.test.ts +55 -0
  59. package/src/hooks/gsapRuntimeBridge.ts +5 -3
  60. package/src/hooks/gsapRuntimeKeyframes.ts +1 -3
  61. package/src/hooks/inspectorSaveReload.test.tsx +107 -0
  62. package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +16 -1
  63. package/src/hooks/useAppHotkeys.test.ts +17 -0
  64. package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -1
  65. package/src/hooks/useAppHotkeys.ts +3 -3
  66. package/src/hooks/useAutomationSelectionKeyboard.test.tsx +17 -0
  67. package/src/hooks/useAutomationSelectionKeyboard.ts +3 -9
  68. package/src/hooks/useDomEditAttributeCommits.ts +71 -29
  69. package/src/hooks/useDomEditCommits.test.tsx +172 -92
  70. package/src/hooks/useDomEditCommits.ts +2 -0
  71. package/src/hooks/useDomEditCommitsHelpers.ts +4 -6
  72. package/src/hooks/useDomEditPersist.ts +28 -13
  73. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
  74. package/src/hooks/useDomEditSession.ts +2 -3
  75. package/src/hooks/useDomEditTextCommits.ts +4 -8
  76. package/src/hooks/useDomGeometryCommits.ts +11 -2
  77. package/src/hooks/useEditHistoryActions.paint.test.tsx +122 -3
  78. package/src/hooks/useGsapAwareEditing.ts +6 -6
  79. package/src/hooks/useGsapScriptCommits.test.tsx +19 -0
  80. package/src/hooks/useGsapScriptCommits.ts +5 -4
  81. package/src/hooks/useInlineTextEdit.test.tsx +27 -0
  82. package/src/hooks/useInlineTextEdit.ts +9 -7
  83. package/src/hooks/useKeyframeKeyboard.test.tsx +15 -0
  84. package/src/hooks/useKeyframeKeyboard.ts +2 -8
  85. package/src/hooks/usePreviewPersistence.ts +6 -0
  86. package/src/hooks/useTrackPendingTimelineEdit.ts +2 -6
  87. package/src/index.ts +7 -0
  88. package/src/player/components/Timeline.test.ts +0 -21
  89. package/src/player/components/Timeline.tsx +0 -1
  90. package/src/player/components/timelineLayout.ts +0 -28
  91. package/src/player/hooks/usePlaybackKeyboard.test.ts +43 -0
  92. package/src/player/lib/playbackShortcuts.ts +3 -7
  93. package/src/player/lib/timelineElementHelpers.test.ts +16 -0
  94. package/src/player/lib/timelineElementHelpers.ts +16 -2
  95. package/src/player/lib/timelineViewportBudgets.test.ts +2 -0
  96. package/src/player/lib/timelineViewportBudgets.ts +4 -0
  97. package/src/utils/gsapSoftReload.ts +3 -2
  98. package/src/utils/gsapUndoRestore.browser.test.ts +94 -0
  99. package/src/utils/gsapUndoRestore.test.ts +145 -1
  100. package/src/utils/gsapUndoRestore.ts +70 -21
  101. package/src/utils/sourcePatcher.ts +1 -1
  102. package/src/utils/studioFontHelpers.ts +2 -23
  103. package/src/utils/studioHelpers.test.ts +30 -0
  104. package/src/utils/studioHelpers.ts +18 -4
  105. package/src/utils/studioPendingEdits.test.ts +63 -0
  106. package/src/utils/studioPendingEdits.ts +16 -7
  107. package/src/utils/typingTarget.test.ts +13 -0
  108. package/src/utils/typingTarget.ts +5 -2
  109. package/src/webmcp/useStudioAgentTools.ts +3 -1
  110. package/dist/chunk-ICTZJPQQ.js.map +0 -1
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  isPreviewBooted,
42
42
  isRevealedAudioFxRequestCurrent,
43
43
  isTimelineIgnoredElement,
44
+ isVideoAudible,
44
45
  liveTime,
45
46
  panelsInZone,
46
47
  parseDockLayout,
@@ -67,7 +68,7 @@ import {
67
68
  whenPreviewBooted,
68
69
  wrapTimeline,
69
70
  writeStudioUiPreferences
70
- } from "./chunk-ICTZJPQQ.js";
71
+ } from "./chunk-4OZC426H.js";
71
72
  import {
72
73
  Button,
73
74
  ContextMenu,
@@ -9976,7 +9977,395 @@ import { useCallback as useCallback16 } from "react";
9976
9977
 
9977
9978
  // src/contexts/DomEditContext.tsx
9978
9979
  import { useCallback as useCallback15, useContext as useContext3, useMemo as useMemo9, useRef as useRef16 } from "react";
9980
+
9981
+ // src/utils/studioSaveDiagnostics.ts
9982
+ var STUDIO_SAVE_ATTEMPT_PROPERTY = "__studioSaveAttempt";
9983
+ var StudioSaveHttpError = class extends Error {
9984
+ statusCode;
9985
+ alreadyToasted;
9986
+ constructor(message, statusCode, options = {}) {
9987
+ super(message);
9988
+ this.name = "StudioSaveHttpError";
9989
+ this.statusCode = statusCode;
9990
+ this.alreadyToasted = options.alreadyToasted ?? false;
9991
+ }
9992
+ };
9993
+ var StudioSaveNetworkError = class extends Error {
9994
+ constructor(message, options) {
9995
+ super(message, options);
9996
+ this.name = "StudioSaveNetworkError";
9997
+ }
9998
+ };
9999
+ var StudioFileConflictError = class extends StudioSaveHttpError {
10000
+ filePath;
10001
+ currentVersion;
10002
+ currentContent;
10003
+ attemptedContent;
10004
+ constructor(input) {
10005
+ super(`Save conflict: ${input.filePath} changed outside this Studio session`, 409);
10006
+ this.name = "StudioFileConflictError";
10007
+ this.filePath = input.filePath;
10008
+ this.currentVersion = input.currentVersion;
10009
+ this.currentContent = input.currentContent;
10010
+ this.attemptedContent = input.attemptedContent;
10011
+ }
10012
+ };
10013
+ function readNumericProperty(value, key2) {
10014
+ const record = value;
10015
+ const property = record[key2];
10016
+ return typeof property === "number" && Number.isFinite(property) ? property : void 0;
10017
+ }
10018
+ function createStudioSaveAbortError() {
10019
+ if (typeof DOMException !== "undefined") return new DOMException("Save aborted", "AbortError");
10020
+ const error = new Error("Save aborted");
10021
+ error.name = "AbortError";
10022
+ return error;
10023
+ }
10024
+ function throwIfStudioSaveAborted(signal) {
10025
+ if (signal?.aborted) throw createStudioSaveAbortError();
10026
+ }
10027
+ function attachStudioSaveAttempt(error, attempt) {
10028
+ if (!error || typeof error !== "object") return error;
10029
+ try {
10030
+ Object.defineProperty(error, STUDIO_SAVE_ATTEMPT_PROPERTY, {
10031
+ value: attempt,
10032
+ configurable: true
10033
+ });
10034
+ } catch {
10035
+ }
10036
+ return error;
10037
+ }
10038
+ function getStudioSaveErrorMessage(error) {
10039
+ if (error instanceof Error && error.message) return error.message;
10040
+ if (typeof error === "string" && error.trim()) return error;
10041
+ return "Unknown save failure";
10042
+ }
10043
+ function markStudioSaveErrorAlreadyToasted(error) {
10044
+ if (!error || typeof error !== "object") return error;
10045
+ try {
10046
+ Object.defineProperty(error, "alreadyToasted", { value: true, configurable: true });
10047
+ } catch {
10048
+ }
10049
+ return error;
10050
+ }
10051
+ function isStudioSaveErrorAlreadyToasted(error) {
10052
+ if (!error || typeof error !== "object") return false;
10053
+ if (error.alreadyToasted === true) return true;
10054
+ const cause = error.cause;
10055
+ return cause !== error && isStudioSaveErrorAlreadyToasted(cause);
10056
+ }
10057
+ function getStudioSaveStatusCode(error) {
10058
+ if (!error || typeof error !== "object") return void 0;
10059
+ const direct = readNumericProperty(error, "statusCode") ?? readNumericProperty(error, "status") ?? readNumericProperty(error, "status_code");
10060
+ if (direct != null) return direct;
10061
+ const cause = error.cause;
10062
+ if (cause && cause !== error) return getStudioSaveStatusCode(cause);
10063
+ return void 0;
10064
+ }
10065
+ function getStudioSaveAttempt(error) {
10066
+ if (!error || typeof error !== "object") return void 0;
10067
+ const direct = readNumericProperty(error, STUDIO_SAVE_ATTEMPT_PROPERTY);
10068
+ if (direct != null) return direct;
10069
+ const cause = error.cause;
10070
+ if (cause && cause !== error) return getStudioSaveAttempt(cause);
10071
+ return void 0;
10072
+ }
10073
+ function isStudioSaveAbortError(error) {
10074
+ return error instanceof Error && error.name === "AbortError";
10075
+ }
10076
+ function isRetryableStudioSaveError(error) {
10077
+ if (isStudioSaveAbortError(error)) return false;
10078
+ if (error instanceof StudioSaveNetworkError) return true;
10079
+ const statusCode = getStudioSaveStatusCode(error);
10080
+ if (statusCode == null) return false;
10081
+ return statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
10082
+ }
10083
+ function buildStudioSaveFailureProperties(input) {
10084
+ const statusCode = input.statusCode ?? getStudioSaveStatusCode(input.error) ?? null;
10085
+ const attempt = input.attempt ?? getStudioSaveAttempt(input.error) ?? void 0;
10086
+ return {
10087
+ source: input.source,
10088
+ error_message: getStudioSaveErrorMessage(input.error),
10089
+ status_code: statusCode,
10090
+ file_path: input.filePath ?? input.targetSourceFile ?? void 0,
10091
+ mutation_type: input.mutationType ?? void 0,
10092
+ attempt,
10093
+ label: input.label ?? void 0,
10094
+ target_id: input.targetId ?? void 0,
10095
+ target_selector: input.targetSelector ?? void 0,
10096
+ target_source_file: input.targetSourceFile ?? void 0,
10097
+ block_detail: gsapBlockDetail(input.error)
10098
+ };
10099
+ }
10100
+ function gsapBlockDetail(error) {
10101
+ if (!(error instanceof Error) || error.name !== "GsapEditBlockedError") return void 0;
10102
+ const detail = error.detail;
10103
+ return typeof detail === "string" ? detail : void 0;
10104
+ }
10105
+ function trackStudioSaveFailure(input) {
10106
+ trackStudioEvent("save_failure", buildStudioSaveFailureProperties(input));
10107
+ }
10108
+ function trackStudioEditBlocked(input) {
10109
+ trackStudioEvent("edit_blocked", buildStudioSaveFailureProperties(input));
10110
+ }
10111
+ async function createStudioSaveHttpError(response, fallbackMessage, options = {}) {
10112
+ let body = "";
10113
+ try {
10114
+ body = await response.text();
10115
+ } catch {
10116
+ body = "";
10117
+ }
10118
+ const detail = body.trim().slice(0, 300);
10119
+ const message = detail ? `${fallbackMessage} (${response.status}): ${detail}` : `${fallbackMessage} (${response.status})`;
10120
+ return new StudioSaveHttpError(message, response.status, options);
10121
+ }
10122
+ async function retryStudioSave(operation, options = {}) {
10123
+ const retries = options.retries ?? 3;
10124
+ const baseDelayMs = options.baseDelayMs ?? 500;
10125
+ const maxDelayMs = options.maxDelayMs ?? 8e3;
10126
+ const jitterRatio = options.jitterRatio ?? 0.25;
10127
+ const random = options.random ?? Math.random;
10128
+ const shouldRetry = options.shouldRetry ?? isRetryableStudioSaveError;
10129
+ const sleep = options.sleep ?? ((delayMs, signal) => new Promise((resolve, reject) => {
10130
+ throwIfStudioSaveAborted(signal);
10131
+ const onAbort = () => {
10132
+ globalThis.clearTimeout(timeout);
10133
+ reject(createStudioSaveAbortError());
10134
+ };
10135
+ const timeout = globalThis.setTimeout(() => {
10136
+ signal?.removeEventListener("abort", onAbort);
10137
+ resolve();
10138
+ }, delayMs);
10139
+ signal?.addEventListener("abort", onAbort, { once: true });
10140
+ }));
10141
+ const maxAttempts = retries + 1;
10142
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
10143
+ try {
10144
+ throwIfStudioSaveAborted(options.signal);
10145
+ return await operation(attempt);
10146
+ } catch (error) {
10147
+ const failure = attachStudioSaveAttempt(error, attempt);
10148
+ if (attempt >= maxAttempts || !shouldRetry(failure, attempt)) throw failure;
10149
+ const retryIndex = attempt - 1;
10150
+ const exponentialDelay = Math.min(baseDelayMs * 2 ** retryIndex, maxDelayMs);
10151
+ const jitterSpan = exponentialDelay * jitterRatio;
10152
+ const jitteredDelay = Math.round(exponentialDelay + (random() * 2 - 1) * jitterSpan);
10153
+ const delayMs = Math.max(0, Math.min(maxDelayMs, jitteredDelay));
10154
+ await sleep(delayMs, options.signal);
10155
+ }
10156
+ }
10157
+ throw new Error("Save retry loop exited unexpectedly");
10158
+ }
10159
+
10160
+ // src/utils/typingTarget.ts
10161
+ function isTypingTarget(target) {
10162
+ const element = asElement(target);
10163
+ if (!element) return false;
10164
+ if (element.isContentEditable) return true;
10165
+ return element.closest(TYPING_SELECTOR) !== null;
10166
+ }
10167
+ function ownsPlainKeys(target) {
10168
+ return isTypingTarget(target) || asElement(target)?.closest(KEY_OWNING_CONTROLS) != null;
10169
+ }
10170
+ var KEY_OWNING_CONTROLS = "[role='slider'],video[controls],audio[controls]";
10171
+ var TYPING_SELECTOR = [
10172
+ "input",
10173
+ "textarea",
10174
+ "select",
10175
+ "[contenteditable]:not([contenteditable='false'])",
10176
+ "[role='textbox']",
10177
+ "[role='searchbox']",
10178
+ "[role='combobox']",
10179
+ // A switch is a button in the DOM, but the native checkbox it replaces was an
10180
+ // `input` and matched above. Without this row the shared Toggle would let a
10181
+ // global shortcut claim Space instead of flipping the control (KTD13).
10182
+ "[role='switch']",
10183
+ ".cm-editor"
10184
+ ].join(",");
10185
+ function asElement(target) {
10186
+ if (!target || typeof target !== "object") return null;
10187
+ const candidate = target;
10188
+ return typeof candidate.closest === "function" ? target : null;
10189
+ }
10190
+
10191
+ // src/utils/studioPendingEdits.ts
10192
+ var STUDIO_FLUSH_PENDING_EDITS_EVENT = "hf-studio-flush-pending-edits";
10193
+ var pendingEditPromises = /* @__PURE__ */ new Set();
10194
+ function waitForPostBlurEffects() {
10195
+ return new Promise((resolve) => setTimeout(resolve, 0));
10196
+ }
10197
+ function inspectDrainFailures(results) {
10198
+ let firstFailure;
10199
+ for (const result of results) {
10200
+ if (result.status !== "rejected") continue;
10201
+ if (result.reason instanceof StudioFileConflictError) return { conflict: result.reason };
10202
+ firstFailure ??= result;
10203
+ }
10204
+ return { firstFailure };
10205
+ }
10206
+ function focusedField() {
10207
+ const active = document.activeElement;
10208
+ return active instanceof HTMLElement && isTypingTarget(active) ? active : null;
10209
+ }
10210
+ function hasStudioPendingEdits() {
10211
+ return pendingEditPromises.size > 0 || focusedField() !== null;
10212
+ }
10213
+ function isStudioEditSaving() {
10214
+ return pendingEditPromises.size > 0;
10215
+ }
10216
+ function afterStudioPendingEdits(run) {
10217
+ let waiting = true;
10218
+ const check = () => {
10219
+ if (!waiting) return;
10220
+ if (isStudioEditSaving()) {
10221
+ void Promise.allSettled([...pendingEditPromises]).then(check);
10222
+ return;
10223
+ }
10224
+ waiting = false;
10225
+ run();
10226
+ };
10227
+ check();
10228
+ return () => {
10229
+ waiting = false;
10230
+ };
10231
+ }
10232
+ function trackStudioPendingEdit(result) {
10233
+ if (!result) return void 0;
10234
+ const promise = Promise.resolve(result);
10235
+ pendingEditPromises.add(promise);
10236
+ promise.then(
10237
+ () => pendingEditPromises.delete(promise),
10238
+ () => pendingEditPromises.delete(promise)
10239
+ );
10240
+ return promise;
10241
+ }
10242
+ function trackedStudioEdit(edit) {
10243
+ return (...args) => {
10244
+ const result = edit(...args);
10245
+ if (result instanceof Promise) trackStudioPendingEdit(result);
10246
+ return result;
10247
+ };
10248
+ }
10249
+ async function flushStudioPendingEdits() {
10250
+ const active = focusedField();
10251
+ if (active) {
10252
+ active.blur();
10253
+ await Promise.resolve();
10254
+ await waitForPostBlurEffects();
10255
+ }
10256
+ const detail = { promises: [] };
10257
+ window.dispatchEvent(
10258
+ new CustomEvent(STUDIO_FLUSH_PENDING_EDITS_EVENT, { detail })
10259
+ );
10260
+ let conflict;
10261
+ let firstFailure;
10262
+ while (detail.promises.length > 0 || pendingEditPromises.size > 0) {
10263
+ const promises = [...detail.promises, ...pendingEditPromises];
10264
+ detail.promises = [];
10265
+ const batchFailures = inspectDrainFailures(await Promise.allSettled(promises));
10266
+ conflict ??= batchFailures.conflict;
10267
+ firstFailure ??= batchFailures.firstFailure;
10268
+ }
10269
+ if (conflict) return { status: "conflict", error: conflict };
10270
+ return firstFailure ? { status: "failed", error: firstFailure.reason } : { status: "clean" };
10271
+ }
10272
+ function addStudioPendingEditFlushListener(handler) {
10273
+ const listener2 = (event) => {
10274
+ const detail = event.detail;
10275
+ if (!detail?.promises) return;
10276
+ const promise = trackStudioPendingEdit(handler());
10277
+ if (promise) detail.promises.push(promise);
10278
+ };
10279
+ window.addEventListener(STUDIO_FLUSH_PENDING_EDITS_EVENT, listener2);
10280
+ return () => window.removeEventListener(STUDIO_FLUSH_PENDING_EDITS_EVENT, listener2);
10281
+ }
10282
+
10283
+ // src/contexts/DomEditContext.tsx
9979
10284
  import { jsx as jsx19 } from "react/jsx-runtime";
10285
+ var SAVES_AN_EDIT = {
10286
+ handleTimelineElementSelect: false,
10287
+ handlePreviewCanvasMouseDown: false,
10288
+ handlePreviewCanvasPointerMove: false,
10289
+ handlePreviewCanvasPointerLeave: false,
10290
+ applyDomSelection: false,
10291
+ clearDomSelection: false,
10292
+ refreshDomEditSelectionFromPreview: false,
10293
+ handleDomStyleCommit: true,
10294
+ handleDomStyleCommitForSelection: true,
10295
+ handleDomAttributeCommit: true,
10296
+ handleDomAttributeLiveCommit: false,
10297
+ handleDomAttributeQuietCommit: true,
10298
+ handleDomHtmlAttributeCommit: true,
10299
+ handleDomAttributesCommit: true,
10300
+ handleDomAttributeBatchCommit: true,
10301
+ handleDomPathOffsetCommit: true,
10302
+ handleDomGroupPathOffsetCommit: true,
10303
+ handleDomZIndexReorderCommit: true,
10304
+ handleDomBoxSizeCommit: true,
10305
+ handleDomRotationCommit: true,
10306
+ handleDomManualEditsReset: true,
10307
+ handleDomTextCommit: true,
10308
+ handleDomTextCommitForSelection: true,
10309
+ handleDomRichTextCommit: true,
10310
+ handleDomTextFieldStyleCommit: true,
10311
+ handleDomAddTextField: true,
10312
+ handleDomRemoveTextField: true,
10313
+ getGsapAnimationsForSelection: false,
10314
+ handleAskAgent: false,
10315
+ handleAgentModalSubmit: false,
10316
+ handleBlockedDomMove: false,
10317
+ handleDomManualDragStart: false,
10318
+ handleDomEditElementDelete: true,
10319
+ handleGroupSelection: true,
10320
+ handleUngroupSelection: true,
10321
+ setActiveGroupElement: false,
10322
+ buildDomSelectionFromTarget: false,
10323
+ buildDomSelectionForTimelineElement: false,
10324
+ updateDomEditHoverSelection: false,
10325
+ resolveImportedFontAsset: false,
10326
+ setAgentModalOpen: false,
10327
+ setAgentPromptSelectionContext: false,
10328
+ setAgentModalAnchorPoint: false,
10329
+ handleGsapUpdateProperty: true,
10330
+ handleGsapUpdateMeta: true,
10331
+ handleGsapDeleteAnimation: true,
10332
+ handleGsapDeleteAllForElement: true,
10333
+ handleGsapAddAnimation: true,
10334
+ handleGsapAddProperty: true,
10335
+ handleGsapRemoveProperty: true,
10336
+ handleGsapUpdateFromProperty: true,
10337
+ handleGsapAddFromProperty: true,
10338
+ handleGsapRemoveFromProperty: true,
10339
+ handleGsapAddKeyframe: true,
10340
+ handleGsapAddKeyframeBatch: true,
10341
+ handleGsapRemoveKeyframe: true,
10342
+ handleGsapMoveKeyframeToPlayhead: true,
10343
+ handleGsapMoveKeyframe: true,
10344
+ handleGsapResizeKeyframedTween: true,
10345
+ handleGsapConvertToKeyframes: true,
10346
+ handleGsapRemoveAllKeyframes: true,
10347
+ handleResetSelectedElementKeyframes: true,
10348
+ commitAnimatedProperty: true,
10349
+ commitAnimatedProperties: true,
10350
+ handleSetArcPath: true,
10351
+ handleUpdateArcSegment: true,
10352
+ handleUnroll: true,
10353
+ invalidateGsapCache: false,
10354
+ previewIframeRef: false,
10355
+ commitMutation: true,
10356
+ applyMarqueeSelection: false,
10357
+ handleUpdateKeyframeEase: true,
10358
+ handleUpdateSegmentEase: true,
10359
+ handleSetAllKeyframeEases: true
10360
+ };
10361
+ function trackEditCommits(actions) {
10362
+ const tracked = { ...actions };
10363
+ for (const key2 of Object.keys(SAVES_AN_EDIT)) {
10364
+ if (!SAVES_AN_EDIT[key2]) continue;
10365
+ tracked[key2] = trackedStudioEdit(actions[key2]);
10366
+ }
10367
+ return tracked;
10368
+ }
9980
10369
  var DomEditActionsContext = createStableContext(
9981
10370
  "DomEditActionsContext",
9982
10371
  null
@@ -10028,6 +10417,7 @@ function DomEditProvider({
10028
10417
  handleDomAttributeQuietCommit,
10029
10418
  handleDomHtmlAttributeCommit,
10030
10419
  handleDomAttributesCommit,
10420
+ handleDomAttributeBatchCommit,
10031
10421
  handleDomPathOffsetCommit,
10032
10422
  handleDomGroupPathOffsetCommit,
10033
10423
  handleDomZIndexReorderCommit,
@@ -10100,7 +10490,7 @@ function DomEditProvider({
10100
10490
  (mutation, options) => commitMutationRef.current(mutation, options),
10101
10491
  []
10102
10492
  );
10103
- const actions = useMemo9(
10493
+ const untrackedActions = useMemo9(
10104
10494
  () => ({
10105
10495
  handleTimelineElementSelect,
10106
10496
  handlePreviewCanvasMouseDown,
@@ -10116,6 +10506,7 @@ function DomEditProvider({
10116
10506
  handleDomAttributeQuietCommit,
10117
10507
  handleDomHtmlAttributeCommit,
10118
10508
  handleDomAttributesCommit,
10509
+ handleDomAttributeBatchCommit,
10119
10510
  handleDomPathOffsetCommit,
10120
10511
  handleDomGroupPathOffsetCommit,
10121
10512
  handleDomZIndexReorderCommit,
@@ -10191,6 +10582,7 @@ function DomEditProvider({
10191
10582
  handleDomAttributeQuietCommit,
10192
10583
  handleDomHtmlAttributeCommit,
10193
10584
  handleDomAttributesCommit,
10585
+ handleDomAttributeBatchCommit,
10194
10586
  handleDomPathOffsetCommit,
10195
10587
  handleDomGroupPathOffsetCommit,
10196
10588
  handleDomZIndexReorderCommit,
@@ -10252,6 +10644,7 @@ function DomEditProvider({
10252
10644
  handleSetAllKeyframeEases
10253
10645
  ]
10254
10646
  );
10647
+ const actions = useMemo9(() => trackEditCommits(untrackedActions), [untrackedActions]);
10255
10648
  const selection = useMemo9(
10256
10649
  () => ({
10257
10650
  domEditSelection,
@@ -11855,6 +12248,8 @@ var TIMELINE_VIEWPORT_BUDGETS = Object.freeze({
11855
12248
  frameIntervalP95Ms: 33.3,
11856
12249
  constrainedInteractionP95Ms: 75,
11857
12250
  constrainedFrameIntervalP95Ms: 75,
12251
+ ciVirtualizedInteractionP95Ms: 58.3,
12252
+ ciVirtualizedFrameIntervalP95Ms: 25,
11858
12253
  longTaskLimitMs: 50,
11859
12254
  constrainedLongTaskLimitMs: 300,
11860
12255
  memoryReturnToleranceRatio: 0.15,
@@ -16956,36 +17351,6 @@ import {
16956
17351
  resolveAutomationRange as resolveAutomationRange5
16957
17352
  } from "@hyperframes/core/audio-automation";
16958
17353
 
16959
- // src/utils/typingTarget.ts
16960
- function isTypingTarget(target) {
16961
- const element = asElement(target);
16962
- if (!element) return false;
16963
- if (element.isContentEditable) return true;
16964
- return element.closest(TYPING_SELECTOR) !== null;
16965
- }
16966
- function ownsPlainKeys(target) {
16967
- return isTypingTarget(target) || asElement(target)?.closest("[role='slider']") != null;
16968
- }
16969
- var TYPING_SELECTOR = [
16970
- "input",
16971
- "textarea",
16972
- "select",
16973
- "[contenteditable]:not([contenteditable='false'])",
16974
- "[role='textbox']",
16975
- "[role='searchbox']",
16976
- "[role='combobox']",
16977
- // A switch is a button in the DOM, but the native checkbox it replaces was an
16978
- // `input` and matched above. Without this row the shared Toggle would let a
16979
- // global shortcut claim Space instead of flipping the control (KTD13).
16980
- "[role='switch']",
16981
- ".cm-editor"
16982
- ].join(",");
16983
- function asElement(target) {
16984
- if (!target || typeof target !== "object") return null;
16985
- const candidate = target;
16986
- return typeof candidate.closest === "function" ? target : null;
16987
- }
16988
-
16989
17354
  // src/utils/studioHelpers.ts
16990
17355
  function getTimelineElementLabel(element) {
16991
17356
  return element.label || element.id || element.tag;
@@ -17167,23 +17532,25 @@ function mediaMetadataUrl(projectId, assetPath) {
17167
17532
  }
17168
17533
  async function resolveDroppedAssetHasAudio(projectId, assetPath, kind) {
17169
17534
  if (kind !== "video") return false;
17535
+ return await resolveAssetHasAudio(projectId, assetPath) === true;
17536
+ }
17537
+ function isPlainRecord(value) {
17538
+ return typeof value === "object" && value !== null;
17539
+ }
17540
+ async function resolveAssetHasAudio(projectId, assetPath) {
17170
17541
  try {
17171
17542
  const response = await fetch(mediaMetadataUrl(projectId, assetPath));
17172
- if (!response.ok) return false;
17543
+ if (!response.ok) return null;
17173
17544
  const data = await response.json();
17174
- return data?.metadata?.hasAudio === true;
17545
+ const metadata = isPlainRecord(data) ? data.metadata : void 0;
17546
+ const hasAudio = isPlainRecord(metadata) ? metadata.hasAudio : void 0;
17547
+ return typeof hasAudio === "boolean" ? hasAudio : null;
17175
17548
  } catch {
17176
- return false;
17549
+ return null;
17177
17550
  }
17178
17551
  }
17179
17552
 
17180
17553
  // src/hooks/useAutomationSelectionKeyboard.ts
17181
- function isTextInput(el) {
17182
- if (!el) return false;
17183
- const tag = el.tagName;
17184
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
17185
- return el instanceof HTMLElement && el.isContentEditable;
17186
- }
17187
17554
  function isChord(e, letter) {
17188
17555
  return (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === letter;
17189
17556
  }
@@ -17286,7 +17653,7 @@ function handleCopy(e, state, lanes, sel) {
17286
17653
  return true;
17287
17654
  }
17288
17655
  function automationOwnsKey(e) {
17289
- if (isTextInput(document.activeElement)) return false;
17656
+ if (isTypingTarget(document.activeElement)) return false;
17290
17657
  const state = usePlayerStore.getState();
17291
17658
  if (isChord(e, "c")) return state.automationSelection !== null;
17292
17659
  if (!isChord(e, "v")) return false;
@@ -17311,7 +17678,7 @@ function useAutomationSelectionKeyboard({
17311
17678
  }) {
17312
17679
  useEffect25(() => {
17313
17680
  const handler = (e) => {
17314
- if (isTextInput(document.activeElement)) return;
17681
+ if (isTypingTarget(document.activeElement)) return;
17315
17682
  if (e.defaultPrevented) return;
17316
17683
  const state = usePlayerStore.getState();
17317
17684
  if (handlePaste(e, state, lanes)) return;
@@ -19908,185 +20275,6 @@ function serializeStudioFileMutations(writer, targetPaths, task) {
19908
20275
  return acquire(0);
19909
20276
  }
19910
20277
 
19911
- // src/utils/studioSaveDiagnostics.ts
19912
- var STUDIO_SAVE_ATTEMPT_PROPERTY = "__studioSaveAttempt";
19913
- var StudioSaveHttpError = class extends Error {
19914
- statusCode;
19915
- alreadyToasted;
19916
- constructor(message, statusCode, options = {}) {
19917
- super(message);
19918
- this.name = "StudioSaveHttpError";
19919
- this.statusCode = statusCode;
19920
- this.alreadyToasted = options.alreadyToasted ?? false;
19921
- }
19922
- };
19923
- var StudioSaveNetworkError = class extends Error {
19924
- constructor(message, options) {
19925
- super(message, options);
19926
- this.name = "StudioSaveNetworkError";
19927
- }
19928
- };
19929
- var StudioFileConflictError = class extends StudioSaveHttpError {
19930
- filePath;
19931
- currentVersion;
19932
- currentContent;
19933
- attemptedContent;
19934
- constructor(input) {
19935
- super(`Save conflict: ${input.filePath} changed outside this Studio session`, 409);
19936
- this.name = "StudioFileConflictError";
19937
- this.filePath = input.filePath;
19938
- this.currentVersion = input.currentVersion;
19939
- this.currentContent = input.currentContent;
19940
- this.attemptedContent = input.attemptedContent;
19941
- }
19942
- };
19943
- function readNumericProperty(value, key2) {
19944
- const record = value;
19945
- const property = record[key2];
19946
- return typeof property === "number" && Number.isFinite(property) ? property : void 0;
19947
- }
19948
- function createStudioSaveAbortError() {
19949
- if (typeof DOMException !== "undefined") return new DOMException("Save aborted", "AbortError");
19950
- const error = new Error("Save aborted");
19951
- error.name = "AbortError";
19952
- return error;
19953
- }
19954
- function throwIfStudioSaveAborted(signal) {
19955
- if (signal?.aborted) throw createStudioSaveAbortError();
19956
- }
19957
- function attachStudioSaveAttempt(error, attempt) {
19958
- if (!error || typeof error !== "object") return error;
19959
- try {
19960
- Object.defineProperty(error, STUDIO_SAVE_ATTEMPT_PROPERTY, {
19961
- value: attempt,
19962
- configurable: true
19963
- });
19964
- } catch {
19965
- }
19966
- return error;
19967
- }
19968
- function getStudioSaveErrorMessage(error) {
19969
- if (error instanceof Error && error.message) return error.message;
19970
- if (typeof error === "string" && error.trim()) return error;
19971
- return "Unknown save failure";
19972
- }
19973
- function markStudioSaveErrorAlreadyToasted(error) {
19974
- if (!error || typeof error !== "object") return error;
19975
- try {
19976
- Object.defineProperty(error, "alreadyToasted", { value: true, configurable: true });
19977
- } catch {
19978
- }
19979
- return error;
19980
- }
19981
- function isStudioSaveErrorAlreadyToasted(error) {
19982
- if (!error || typeof error !== "object") return false;
19983
- if (error.alreadyToasted === true) return true;
19984
- const cause = error.cause;
19985
- return cause !== error && isStudioSaveErrorAlreadyToasted(cause);
19986
- }
19987
- function getStudioSaveStatusCode(error) {
19988
- if (!error || typeof error !== "object") return void 0;
19989
- const direct = readNumericProperty(error, "statusCode") ?? readNumericProperty(error, "status") ?? readNumericProperty(error, "status_code");
19990
- if (direct != null) return direct;
19991
- const cause = error.cause;
19992
- if (cause && cause !== error) return getStudioSaveStatusCode(cause);
19993
- return void 0;
19994
- }
19995
- function getStudioSaveAttempt(error) {
19996
- if (!error || typeof error !== "object") return void 0;
19997
- const direct = readNumericProperty(error, STUDIO_SAVE_ATTEMPT_PROPERTY);
19998
- if (direct != null) return direct;
19999
- const cause = error.cause;
20000
- if (cause && cause !== error) return getStudioSaveAttempt(cause);
20001
- return void 0;
20002
- }
20003
- function isStudioSaveAbortError(error) {
20004
- return error instanceof Error && error.name === "AbortError";
20005
- }
20006
- function isRetryableStudioSaveError(error) {
20007
- if (isStudioSaveAbortError(error)) return false;
20008
- if (error instanceof StudioSaveNetworkError) return true;
20009
- const statusCode = getStudioSaveStatusCode(error);
20010
- if (statusCode == null) return false;
20011
- return statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
20012
- }
20013
- function buildStudioSaveFailureProperties(input) {
20014
- const statusCode = input.statusCode ?? getStudioSaveStatusCode(input.error) ?? null;
20015
- const attempt = input.attempt ?? getStudioSaveAttempt(input.error) ?? void 0;
20016
- return {
20017
- source: input.source,
20018
- error_message: getStudioSaveErrorMessage(input.error),
20019
- status_code: statusCode,
20020
- file_path: input.filePath ?? input.targetSourceFile ?? void 0,
20021
- mutation_type: input.mutationType ?? void 0,
20022
- attempt,
20023
- label: input.label ?? void 0,
20024
- target_id: input.targetId ?? void 0,
20025
- target_selector: input.targetSelector ?? void 0,
20026
- target_source_file: input.targetSourceFile ?? void 0,
20027
- block_detail: gsapBlockDetail(input.error)
20028
- };
20029
- }
20030
- function gsapBlockDetail(error) {
20031
- if (!(error instanceof Error) || error.name !== "GsapEditBlockedError") return void 0;
20032
- const detail = error.detail;
20033
- return typeof detail === "string" ? detail : void 0;
20034
- }
20035
- function trackStudioSaveFailure(input) {
20036
- trackStudioEvent("save_failure", buildStudioSaveFailureProperties(input));
20037
- }
20038
- function trackStudioEditBlocked(input) {
20039
- trackStudioEvent("edit_blocked", buildStudioSaveFailureProperties(input));
20040
- }
20041
- async function createStudioSaveHttpError(response, fallbackMessage, options = {}) {
20042
- let body = "";
20043
- try {
20044
- body = await response.text();
20045
- } catch {
20046
- body = "";
20047
- }
20048
- const detail = body.trim().slice(0, 300);
20049
- const message = detail ? `${fallbackMessage} (${response.status}): ${detail}` : `${fallbackMessage} (${response.status})`;
20050
- return new StudioSaveHttpError(message, response.status, options);
20051
- }
20052
- async function retryStudioSave(operation, options = {}) {
20053
- const retries = options.retries ?? 3;
20054
- const baseDelayMs = options.baseDelayMs ?? 500;
20055
- const maxDelayMs = options.maxDelayMs ?? 8e3;
20056
- const jitterRatio = options.jitterRatio ?? 0.25;
20057
- const random = options.random ?? Math.random;
20058
- const shouldRetry = options.shouldRetry ?? isRetryableStudioSaveError;
20059
- const sleep = options.sleep ?? ((delayMs, signal) => new Promise((resolve, reject) => {
20060
- throwIfStudioSaveAborted(signal);
20061
- const onAbort = () => {
20062
- globalThis.clearTimeout(timeout);
20063
- reject(createStudioSaveAbortError());
20064
- };
20065
- const timeout = globalThis.setTimeout(() => {
20066
- signal?.removeEventListener("abort", onAbort);
20067
- resolve();
20068
- }, delayMs);
20069
- signal?.addEventListener("abort", onAbort, { once: true });
20070
- }));
20071
- const maxAttempts = retries + 1;
20072
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
20073
- try {
20074
- throwIfStudioSaveAborted(options.signal);
20075
- return await operation(attempt);
20076
- } catch (error) {
20077
- const failure = attachStudioSaveAttempt(error, attempt);
20078
- if (attempt >= maxAttempts || !shouldRetry(failure, attempt)) throw failure;
20079
- const retryIndex = attempt - 1;
20080
- const exponentialDelay = Math.min(baseDelayMs * 2 ** retryIndex, maxDelayMs);
20081
- const jitterSpan = exponentialDelay * jitterRatio;
20082
- const jitteredDelay = Math.round(exponentialDelay + (random() * 2 - 1) * jitterSpan);
20083
- const delayMs = Math.max(0, Math.min(maxDelayMs, jitteredDelay));
20084
- await sleep(delayMs, options.signal);
20085
- }
20086
- }
20087
- throw new Error("Save retry loop exited unexpectedly");
20088
- }
20089
-
20090
20278
  // src/utils/studioFileHistory.ts
20091
20279
  async function readProjectFileContent(pid, path) {
20092
20280
  const response = await fetch(buildProjectApiPath(pid, `/files/${encodeURIComponent(path)}`));
@@ -20357,7 +20545,7 @@ function applySoftReloadFinalization(iframe, currentTime) {
20357
20545
  }
20358
20546
  }
20359
20547
  function applySoftReload(iframe, scriptText, options = {}) {
20360
- const { onAsyncFailure, currentTimeOverride, authoredHtml } = options;
20548
+ const { onAsyncFailure, currentTimeOverride, authoredHtml, reparse = [] } = options;
20361
20549
  if (!iframe || !scriptText) return "cannot-soft-reload";
20362
20550
  const win = iframe.contentWindow;
20363
20551
  const doc = iframe.contentDocument;
@@ -20391,7 +20579,7 @@ function applySoftReload(iframe, scriptText, options = {}) {
20391
20579
  const readAuthoredOpacity = (el) => findAuthoredStyle(el)?.opacity ?? readStampedAuthoredOpacity(el);
20392
20580
  const doReload = () => {
20393
20581
  const timelines = win.__timelines;
20394
- const allTargets = [];
20582
+ const allTargets = [...reparse];
20395
20583
  if (timelines) {
20396
20584
  for (const key2 of targetKeys) {
20397
20585
  const tl = timelines[key2];
@@ -24244,18 +24432,14 @@ var PLAYBACK_SHORTCUT_IGNORED_SELECTOR = [
24244
24432
  "a[href]",
24245
24433
  "[role='button']",
24246
24434
  "[role='checkbox']",
24247
- "[role='combobox']",
24248
24435
  "[role='menuitem']",
24249
24436
  // Base UI's menu radio item is a `<div>`, so `button` above no longer catches it.
24250
24437
  "[role='menuitemradio']",
24251
24438
  "[role='radio']",
24252
- "[role='slider']",
24253
- "[role='spinbutton']",
24254
- "[role='switch']",
24255
- "[role='textbox']"
24439
+ "[role='spinbutton']"
24256
24440
  ].join(",");
24257
24441
  function shouldIgnorePlaybackShortcutTarget(target) {
24258
- if (isTypingTarget(target)) return true;
24442
+ if (ownsPlainKeys(target)) return true;
24259
24443
  if (!target || typeof target !== "object") return false;
24260
24444
  const candidate = target;
24261
24445
  if (typeof candidate.closest !== "function") return false;
@@ -25729,6 +25913,7 @@ var STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP = "__hfStudioManualEditsPlaybackFram
25729
25913
  var STUDIO_ROTATION_TRANSFORM_ORIGIN = "center center";
25730
25914
 
25731
25915
  // src/components/editor/gsapAnimatesProperty.ts
25916
+ var gsapRendersTransform = (el) => !!el._gsap?.renderTransform;
25732
25917
  function gsapAnimatesProperty(el, ...props) {
25733
25918
  const win = el.ownerDocument.defaultView;
25734
25919
  if (!win?.__timelines) return false;
@@ -25785,11 +25970,136 @@ function splitTopLevelWhitespace(value) {
25785
25970
  return parts;
25786
25971
  }
25787
25972
 
25973
+ // src/components/editor/manualEditsDomPatches.ts
25974
+ function collectInlineStyleOps(element, properties, ops) {
25975
+ for (const prop of properties) {
25976
+ const val = element.style.getPropertyValue(prop);
25977
+ if (val) ops.push({ type: "inline-style", property: prop, value: val });
25978
+ }
25979
+ }
25980
+ function collectAttributeOps(element, attrNames, ops) {
25981
+ for (const attr of attrNames) {
25982
+ const val = element.getAttribute(attr);
25983
+ if (val !== null) ops.push({ type: "attribute", property: attr, value: val });
25984
+ }
25985
+ }
25986
+ function appendTransformDisplayOps(element, ops) {
25987
+ const val = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
25988
+ if (val !== null) {
25989
+ ops.push({ type: "inline-style", property: "display", value: val || null });
25990
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: null });
25991
+ }
25992
+ }
25993
+ function buildClearPathOffsetPatches(element) {
25994
+ const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR);
25995
+ const ops = [
25996
+ { type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: null },
25997
+ { type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: null },
25998
+ { type: "inline-style", property: "translate", value: originalInlineTranslate || null },
25999
+ { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: null },
26000
+ { type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: null },
26001
+ { type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: null }
26002
+ ];
26003
+ appendTransformDisplayOps(element, ops);
26004
+ return ops;
26005
+ }
26006
+ var BOX_SIZE_STYLE_PROPS = [
26007
+ "width",
26008
+ "height",
26009
+ "min-width",
26010
+ "min-height",
26011
+ "max-width",
26012
+ "max-height",
26013
+ "flex-basis",
26014
+ "flex-grow",
26015
+ "flex-shrink",
26016
+ "box-sizing",
26017
+ "scale",
26018
+ "transform-origin",
26019
+ "display"
26020
+ ];
26021
+ var BOX_SIZE_ORIG_ATTRS = [
26022
+ [STUDIO_ORIGINAL_WIDTH_ATTR, "width"],
26023
+ [STUDIO_ORIGINAL_HEIGHT_ATTR, "height"],
26024
+ // Records a measurement rather than a style, so it restores nothing.
26025
+ [STUDIO_ORIGINAL_BOX_WIDTH_ATTR, ""],
26026
+ [STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, ""],
26027
+ [STUDIO_ORIGINAL_MIN_WIDTH_ATTR, "min-width"],
26028
+ [STUDIO_ORIGINAL_MIN_HEIGHT_ATTR, "min-height"],
26029
+ [STUDIO_ORIGINAL_MAX_WIDTH_ATTR, "max-width"],
26030
+ [STUDIO_ORIGINAL_MAX_HEIGHT_ATTR, "max-height"],
26031
+ [STUDIO_ORIGINAL_FLEX_BASIS_ATTR, "flex-basis"],
26032
+ [STUDIO_ORIGINAL_FLEX_GROW_ATTR, "flex-grow"],
26033
+ [STUDIO_ORIGINAL_FLEX_SHRINK_ATTR, "flex-shrink"],
26034
+ [STUDIO_ORIGINAL_BOX_SIZING_ATTR, "box-sizing"],
26035
+ [STUDIO_ORIGINAL_SCALE_ATTR, "scale"],
26036
+ [STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR, "transform-origin"],
26037
+ [STUDIO_ORIGINAL_DISPLAY_ATTR, "display"],
26038
+ [STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, ""]
26039
+ ];
26040
+ function buildBoxSizePatches(element) {
26041
+ const ops = [];
26042
+ collectInlineStyleOps(element, [STUDIO_WIDTH_PROP, STUDIO_HEIGHT_PROP], ops);
26043
+ const withoutGsapScaleMask = gsapRendersTransform(element) ? BOX_SIZE_STYLE_PROPS.filter((prop) => prop !== "scale") : BOX_SIZE_STYLE_PROPS;
26044
+ collectInlineStyleOps(element, withoutGsapScaleMask, ops);
26045
+ ops.push({ type: "attribute", property: STUDIO_BOX_SIZE_ATTR, value: "true" });
26046
+ collectAttributeOps(
26047
+ element,
26048
+ BOX_SIZE_ORIG_ATTRS.map(([attr]) => attr),
26049
+ ops
26050
+ );
26051
+ return ops;
26052
+ }
26053
+ function buildClearBoxSizePatches(element) {
26054
+ const ops = [
26055
+ { type: "inline-style", property: STUDIO_WIDTH_PROP, value: null },
26056
+ { type: "inline-style", property: STUDIO_HEIGHT_PROP, value: null },
26057
+ { type: "attribute", property: STUDIO_BOX_SIZE_ATTR, value: null }
26058
+ ];
26059
+ for (const [attrName, styleProp] of BOX_SIZE_ORIG_ATTRS) {
26060
+ const origVal = element.getAttribute(attrName);
26061
+ if (origVal !== null && styleProp) {
26062
+ ops.push({ type: "inline-style", property: styleProp, value: origVal || null });
26063
+ }
26064
+ ops.push({ type: "attribute", property: attrName, value: null });
26065
+ }
26066
+ return ops;
26067
+ }
26068
+ function buildClearRotationPatches(element) {
26069
+ const origInlineRotate = element.getAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR);
26070
+ const origRotationTransformOrigin = element.getAttribute(
26071
+ STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR
26072
+ );
26073
+ const ops = [
26074
+ { type: "inline-style", property: STUDIO_ROTATION_PROP, value: null },
26075
+ { type: "inline-style", property: "rotate", value: origInlineRotate || null },
26076
+ {
26077
+ type: "inline-style",
26078
+ property: "transform-origin",
26079
+ value: origRotationTransformOrigin !== null ? origRotationTransformOrigin || null : null
26080
+ },
26081
+ { type: "attribute", property: STUDIO_ROTATION_ATTR, value: null },
26082
+ { type: "attribute", property: STUDIO_ROTATION_DRAFT_ATTR, value: null },
26083
+ { type: "attribute", property: STUDIO_ORIGINAL_ROTATE_ATTR, value: null },
26084
+ { type: "attribute", property: STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, value: null },
26085
+ { type: "attribute", property: STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR, value: null }
26086
+ ];
26087
+ appendTransformDisplayOps(element, ops);
26088
+ return ops;
26089
+ }
26090
+
25788
26091
  // src/components/editor/manualEditsDom.ts
25789
26092
  var studioManualEditGestureId = 0;
25790
- function beginStudioManualEditGesture(element) {
26093
+ var MOVE_DRAWS = ["translate", STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP];
26094
+ var GESTURE_DRAWS = {
26095
+ move: MOVE_DRAWS,
26096
+ resize: [...MOVE_DRAWS, STUDIO_WIDTH_PROP, STUDIO_HEIGHT_PROP, ...BOX_SIZE_STYLE_PROPS],
26097
+ rotate: ["rotate", "transform", "transform-origin", "display", STUDIO_ROTATION_PROP],
26098
+ edit: []
26099
+ };
26100
+ function beginStudioManualEditGesture(element, draws) {
25791
26101
  studioManualEditGestureId += 1;
25792
- const token = `gesture-${studioManualEditGestureId}`;
26102
+ const token = `gesture-${studioManualEditGestureId}:${draws}`;
25793
26103
  element.setAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR, token);
25794
26104
  return token;
25795
26105
  }
@@ -25818,9 +26128,18 @@ var gestureSaves = /* @__PURE__ */ new WeakMap();
25818
26128
  function studioManualEditSavesIn(doc) {
25819
26129
  return gestureSaves.get(doc) ?? 0;
25820
26130
  }
25821
- function noteStudioManualEditSave(element) {
26131
+ function countStudioManualEditSave(element, save) {
25822
26132
  const doc = element.ownerDocument;
25823
- gestureSaves.set(doc, studioManualEditSavesIn(doc) + 1);
26133
+ const count = () => void gestureSaves.set(doc, studioManualEditSavesIn(doc) + 1);
26134
+ count();
26135
+ const result = save();
26136
+ void Promise.resolve(result).then(count, count);
26137
+ return result;
26138
+ }
26139
+ function studioGestureDraws(element) {
26140
+ const token = element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
26141
+ if (token === null) return null;
26142
+ return GESTURE_DRAWS[token.split(":")[1]] ?? [];
25824
26143
  }
25825
26144
  function isStudioManualEditGestureActive(element) {
25826
26145
  return element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
@@ -26264,90 +26583,6 @@ function sceneSwapFor(iframe) {
26264
26583
  };
26265
26584
  }
26266
26585
 
26267
- // src/utils/studioPendingEdits.ts
26268
- var STUDIO_FLUSH_PENDING_EDITS_EVENT = "hf-studio-flush-pending-edits";
26269
- var pendingEditPromises = /* @__PURE__ */ new Set();
26270
- function waitForPostBlurEffects() {
26271
- return new Promise((resolve) => setTimeout(resolve, 0));
26272
- }
26273
- function inspectDrainFailures(results) {
26274
- let firstFailure;
26275
- for (const result of results) {
26276
- if (result.status !== "rejected") continue;
26277
- if (result.reason instanceof StudioFileConflictError) return { conflict: result.reason };
26278
- firstFailure ??= result;
26279
- }
26280
- return { firstFailure };
26281
- }
26282
- function focusedField() {
26283
- const active = document.activeElement;
26284
- return active instanceof HTMLElement && active.matches('input, textarea, select, [contenteditable="true"], [role="textbox"]') ? active : null;
26285
- }
26286
- function hasStudioPendingEdits() {
26287
- return pendingEditPromises.size > 0 || focusedField() !== null;
26288
- }
26289
- function isStudioEditSaving() {
26290
- return pendingEditPromises.size > 0;
26291
- }
26292
- function afterStudioPendingEdits(run) {
26293
- let waiting = true;
26294
- const check = () => {
26295
- if (!waiting) return;
26296
- if (isStudioEditSaving()) {
26297
- void Promise.allSettled([...pendingEditPromises]).then(check);
26298
- return;
26299
- }
26300
- waiting = false;
26301
- run();
26302
- };
26303
- check();
26304
- return () => {
26305
- waiting = false;
26306
- };
26307
- }
26308
- function trackStudioPendingEdit(result) {
26309
- if (!result) return void 0;
26310
- const promise = Promise.resolve(result);
26311
- pendingEditPromises.add(promise);
26312
- promise.then(
26313
- () => pendingEditPromises.delete(promise),
26314
- () => pendingEditPromises.delete(promise)
26315
- );
26316
- return promise;
26317
- }
26318
- async function flushStudioPendingEdits() {
26319
- const active = focusedField();
26320
- if (active) {
26321
- active.blur();
26322
- await Promise.resolve();
26323
- await waitForPostBlurEffects();
26324
- }
26325
- const detail = { promises: [] };
26326
- window.dispatchEvent(
26327
- new CustomEvent(STUDIO_FLUSH_PENDING_EDITS_EVENT, { detail })
26328
- );
26329
- let firstFailure;
26330
- while (detail.promises.length > 0 || pendingEditPromises.size > 0) {
26331
- const promises = [...detail.promises, ...pendingEditPromises];
26332
- detail.promises = [];
26333
- const results = await Promise.allSettled(promises);
26334
- const batchFailures = inspectDrainFailures(results);
26335
- if (batchFailures.conflict) return { status: "conflict", error: batchFailures.conflict };
26336
- firstFailure ??= batchFailures.firstFailure;
26337
- }
26338
- return firstFailure ? { status: "failed", error: firstFailure.reason } : { status: "clean" };
26339
- }
26340
- function addStudioPendingEditFlushListener(handler) {
26341
- const listener2 = (event) => {
26342
- const detail = event.detail;
26343
- if (!detail?.promises) return;
26344
- const promise = trackStudioPendingEdit(handler());
26345
- if (promise) detail.promises.push(promise);
26346
- };
26347
- window.addEventListener(STUDIO_FLUSH_PENDING_EDITS_EVENT, listener2);
26348
- return () => window.removeEventListener(STUDIO_FLUSH_PENDING_EDITS_EVENT, listener2);
26349
- }
26350
-
26351
26586
  // src/player/hooks/useShadowPreviewReload.ts
26352
26587
  var SHADOW_READY_TIMEOUT_MS = 15e3;
26353
26588
  var SHADOW_READY_BUDGETS = 3;
@@ -33045,125 +33280,6 @@ function computeDraggedGsapPosition(element, studioOffset, fallbackBase) {
33045
33280
 
33046
33281
  // src/hooks/gsapRuntimeKeyframes.ts
33047
33282
  import { buildArcPath } from "@hyperframes/core/gsap-parser-acorn";
33048
-
33049
- // src/components/editor/manualEditsDomPatches.ts
33050
- function collectInlineStyleOps(element, properties, ops) {
33051
- for (const prop of properties) {
33052
- const val = element.style.getPropertyValue(prop);
33053
- if (val) ops.push({ type: "inline-style", property: prop, value: val });
33054
- }
33055
- }
33056
- function collectAttributeOps(element, attrNames, ops) {
33057
- for (const attr of attrNames) {
33058
- const val = element.getAttribute(attr);
33059
- if (val !== null) ops.push({ type: "attribute", property: attr, value: val });
33060
- }
33061
- }
33062
- function appendTransformDisplayOps(element, ops) {
33063
- const val = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
33064
- if (val !== null) {
33065
- ops.push({ type: "inline-style", property: "display", value: val || null });
33066
- ops.push({ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: null });
33067
- }
33068
- }
33069
- function buildClearPathOffsetPatches(element) {
33070
- const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR);
33071
- const ops = [
33072
- { type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: null },
33073
- { type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: null },
33074
- { type: "inline-style", property: "translate", value: originalInlineTranslate || null },
33075
- { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: null },
33076
- { type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: null },
33077
- { type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: null }
33078
- ];
33079
- appendTransformDisplayOps(element, ops);
33080
- return ops;
33081
- }
33082
- var BOX_SIZE_STYLE_PROPS = [
33083
- "width",
33084
- "height",
33085
- "min-width",
33086
- "min-height",
33087
- "max-width",
33088
- "max-height",
33089
- "flex-basis",
33090
- "flex-grow",
33091
- "flex-shrink",
33092
- "box-sizing",
33093
- "scale",
33094
- "transform-origin",
33095
- "display"
33096
- ];
33097
- var BOX_SIZE_ORIG_ATTRS = [
33098
- [STUDIO_ORIGINAL_WIDTH_ATTR, "width"],
33099
- [STUDIO_ORIGINAL_HEIGHT_ATTR, "height"],
33100
- // Records a measurement rather than a style, so it restores nothing.
33101
- [STUDIO_ORIGINAL_BOX_WIDTH_ATTR, ""],
33102
- [STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, ""],
33103
- [STUDIO_ORIGINAL_MIN_WIDTH_ATTR, "min-width"],
33104
- [STUDIO_ORIGINAL_MIN_HEIGHT_ATTR, "min-height"],
33105
- [STUDIO_ORIGINAL_MAX_WIDTH_ATTR, "max-width"],
33106
- [STUDIO_ORIGINAL_MAX_HEIGHT_ATTR, "max-height"],
33107
- [STUDIO_ORIGINAL_FLEX_BASIS_ATTR, "flex-basis"],
33108
- [STUDIO_ORIGINAL_FLEX_GROW_ATTR, "flex-grow"],
33109
- [STUDIO_ORIGINAL_FLEX_SHRINK_ATTR, "flex-shrink"],
33110
- [STUDIO_ORIGINAL_BOX_SIZING_ATTR, "box-sizing"],
33111
- [STUDIO_ORIGINAL_SCALE_ATTR, "scale"],
33112
- [STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR, "transform-origin"],
33113
- [STUDIO_ORIGINAL_DISPLAY_ATTR, "display"],
33114
- [STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, ""]
33115
- ];
33116
- function buildBoxSizePatches(element) {
33117
- const ops = [];
33118
- collectInlineStyleOps(element, [STUDIO_WIDTH_PROP, STUDIO_HEIGHT_PROP], ops);
33119
- collectInlineStyleOps(element, BOX_SIZE_STYLE_PROPS, ops);
33120
- ops.push({ type: "attribute", property: STUDIO_BOX_SIZE_ATTR, value: "true" });
33121
- collectAttributeOps(
33122
- element,
33123
- BOX_SIZE_ORIG_ATTRS.map(([attr]) => attr),
33124
- ops
33125
- );
33126
- return ops;
33127
- }
33128
- function buildClearBoxSizePatches(element) {
33129
- const ops = [
33130
- { type: "inline-style", property: STUDIO_WIDTH_PROP, value: null },
33131
- { type: "inline-style", property: STUDIO_HEIGHT_PROP, value: null },
33132
- { type: "attribute", property: STUDIO_BOX_SIZE_ATTR, value: null }
33133
- ];
33134
- for (const [attrName, styleProp] of BOX_SIZE_ORIG_ATTRS) {
33135
- const origVal = element.getAttribute(attrName);
33136
- if (origVal !== null && styleProp) {
33137
- ops.push({ type: "inline-style", property: styleProp, value: origVal || null });
33138
- }
33139
- ops.push({ type: "attribute", property: attrName, value: null });
33140
- }
33141
- return ops;
33142
- }
33143
- function buildClearRotationPatches(element) {
33144
- const origInlineRotate = element.getAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR);
33145
- const origRotationTransformOrigin = element.getAttribute(
33146
- STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR
33147
- );
33148
- const ops = [
33149
- { type: "inline-style", property: STUDIO_ROTATION_PROP, value: null },
33150
- { type: "inline-style", property: "rotate", value: origInlineRotate || null },
33151
- {
33152
- type: "inline-style",
33153
- property: "transform-origin",
33154
- value: origRotationTransformOrigin !== null ? origRotationTransformOrigin || null : null
33155
- },
33156
- { type: "attribute", property: STUDIO_ROTATION_ATTR, value: null },
33157
- { type: "attribute", property: STUDIO_ROTATION_DRAFT_ATTR, value: null },
33158
- { type: "attribute", property: STUDIO_ORIGINAL_ROTATE_ATTR, value: null },
33159
- { type: "attribute", property: STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, value: null },
33160
- { type: "attribute", property: STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR, value: null }
33161
- ];
33162
- appendTransformDisplayOps(element, ops);
33163
- return ops;
33164
- }
33165
-
33166
- // src/hooks/gsapRuntimeKeyframes.ts
33167
33283
  var FLAT_SKIP_KEYS = /* @__PURE__ */ new Set([
33168
33284
  "ease",
33169
33285
  "duration",
@@ -33382,7 +33498,6 @@ var ROTATION_CHANNELS = [
33382
33498
  ...["rotation", "rotationX", "rotationY", "rotationZ"],
33383
33499
  ...["rotate", "rotateX", "rotateY", "rotateZ"]
33384
33500
  ];
33385
- var gsapRendersTransform = (el) => !!el._gsap?.renderTransform;
33386
33501
  function gsapWritesPosition(el) {
33387
33502
  return gsapRendersTransform(el) || gsapWritesChannels(el, MOVE_CHANNELS);
33388
33503
  }
@@ -33756,7 +33871,10 @@ function createManualOffsetDragMember(input) {
33756
33871
  }
33757
33872
  }
33758
33873
  const initialPathOffset = captureStudioPathOffset(input.element);
33759
- const gestureToken = beginStudioManualEditGesture(input.element);
33874
+ const gestureToken = beginStudioManualEditGesture(
33875
+ input.element,
33876
+ input.gesture === "resize" ? "resize" : "move"
33877
+ );
33760
33878
  const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset, {
33761
33879
  scaleX: input.rect.editScaleX,
33762
33880
  scaleY: input.rect.editScaleY,
@@ -34710,10 +34828,13 @@ function startGesture(kind, e, opts, options) {
34710
34828
  manualEditDragToken = result.member.gestureToken;
34711
34829
  if (result.member.plainTranslate) applyManualOffsetDragDraft(result.member, 0, 0);
34712
34830
  } else {
34713
- manualEditDragToken = beginStudioManualEditGesture(sel.element);
34831
+ manualEditDragToken = beginStudioManualEditGesture(sel.element, "resize");
34714
34832
  }
34715
34833
  } else {
34716
- manualEditDragToken = beginStudioManualEditGesture(sel.element);
34834
+ manualEditDragToken = beginStudioManualEditGesture(
34835
+ sel.element,
34836
+ kind === "rotate" ? "rotate" : "resize"
34837
+ );
34717
34838
  }
34718
34839
  }
34719
34840
  try {
@@ -35413,25 +35534,31 @@ function useDomEditNudge(params) {
35413
35534
  paramsRef.current = params;
35414
35535
  const commitSession = () => {
35415
35536
  const session = sessionRef.current;
35416
- if (!session) return;
35537
+ if (!session) return void 0;
35417
35538
  sessionRef.current = null;
35418
35539
  if (session.timer) clearTimeout(session.timer);
35419
- const updates = session.members.map((member) => ({
35420
- selection: member.selection,
35421
- next: applyManualOffsetNudgeCommit(member, session.accum),
35422
- plainTranslate: member.plainTranslate
35423
- }));
35424
35540
  const p = paramsRef.current;
35425
- const commit = session.isGroup ? p.onGroupPathOffsetCommitRef.current(updates) : p.onPathOffsetCommitRef.current(updates[0].selection, updates[0].next, {
35426
- plainTranslate: updates[0].plainTranslate
35427
- });
35428
- void Promise.resolve(commit).catch(() => {
35429
- for (const member of session.members) {
35430
- if (isStudioManualEditGestureCurrent(member.element, member.gestureToken)) {
35431
- restoreStudioPathOffset(member.element, member.initialPathOffset);
35541
+ let saved2;
35542
+ try {
35543
+ const updates = session.members.map((member) => ({
35544
+ selection: member.selection,
35545
+ next: applyManualOffsetNudgeCommit(member, session.accum),
35546
+ plainTranslate: member.plainTranslate
35547
+ }));
35548
+ const commit = session.isGroup ? p.onGroupPathOffsetCommitRef.current(updates) : p.onPathOffsetCommitRef.current(updates[0].selection, updates[0].next, {
35549
+ plainTranslate: updates[0].plainTranslate
35550
+ });
35551
+ saved2 = Promise.resolve(commit).catch(() => {
35552
+ for (const member of session.members) {
35553
+ if (isStudioManualEditGestureCurrent(member.element, member.gestureToken)) {
35554
+ restoreStudioPathOffset(member.element, member.initialPathOffset);
35555
+ }
35432
35556
  }
35433
- }
35434
- }).finally(() => endManualOffsetDragMembers(session.members));
35557
+ }).finally(() => endManualOffsetDragMembers(session.members));
35558
+ return saved2;
35559
+ } finally {
35560
+ session.endPendingEdit(saved2);
35561
+ }
35435
35562
  };
35436
35563
  const commitSessionRef = useRef64(commitSession);
35437
35564
  commitSessionRef.current = commitSession;
@@ -35442,6 +35569,7 @@ function useDomEditNudge(params) {
35442
35569
  if (session.timer) clearTimeout(session.timer);
35443
35570
  restoreManualOffsetDragMembers(session.members);
35444
35571
  endManualOffsetDragMembers(session.members);
35572
+ session.endPendingEdit();
35445
35573
  }, [params.allowCanvasMovement]);
35446
35574
  const beginSession = (event) => {
35447
35575
  const p = paramsRef.current;
@@ -35461,7 +35589,10 @@ function useDomEditNudge(params) {
35461
35589
  }
35462
35590
  if (members.length === 0) return null;
35463
35591
  p.onManualDragStartRef.current?.();
35464
- return { members, isGroup, accum: { x: 0, y: 0 }, timer: null };
35592
+ let endPendingEdit = () => {
35593
+ };
35594
+ trackStudioPendingEdit(new Promise((resolve) => endPendingEdit = resolve));
35595
+ return { members, isGroup, accum: { x: 0, y: 0 }, timer: null, endPendingEdit };
35465
35596
  };
35466
35597
  const handleKeyDown = (event) => {
35467
35598
  const p = paramsRef.current;
@@ -35483,14 +35614,18 @@ function useDomEditNudge(params) {
35483
35614
  useMountEffect(() => {
35484
35615
  const listener2 = (event) => handleKeyDownRef.current(event);
35485
35616
  window.addEventListener("keydown", listener2, true);
35617
+ const stopCommitOnUndoDrain = addStudioPendingEditFlushListener(
35618
+ () => commitSessionRef.current()
35619
+ );
35486
35620
  return () => {
35487
35621
  window.removeEventListener("keydown", listener2, true);
35622
+ stopCommitOnUndoDrain();
35488
35623
  commitSessionRef.current();
35489
35624
  };
35490
35625
  });
35491
35626
  const selectionKey = selectionIdentityKey(params.selection);
35492
35627
  const groupSelectionsKey = groupSelectionsIdentityKey(params.groupSelections);
35493
- useEffect44(() => () => commitSessionRef.current(), [selectionKey, groupSelectionsKey]);
35628
+ useEffect44(() => () => void commitSessionRef.current(), [selectionKey, groupSelectionsKey]);
35494
35629
  useEffect44(() => {
35495
35630
  const targets = params.groupSelections.length > 1 ? params.groupSelections : params.selection ? [params.selection] : [];
35496
35631
  if (!params.allowCanvasMovement || !canCanvasNudgeTargets(targets)) return;
@@ -37166,7 +37301,7 @@ function useInlineTextEdit({
37166
37301
  original: element.innerHTML,
37167
37302
  outline: element.style.outline,
37168
37303
  outlineOffset: element.style.outlineOffset,
37169
- gesture: beginStudioManualEditGesture(element)
37304
+ gesture: beginStudioManualEditGesture(element, "edit")
37170
37305
  };
37171
37306
  element.style.outline = EDITING_OUTLINE;
37172
37307
  element.style.outlineOffset = "2px";
@@ -37194,9 +37329,11 @@ function useInlineTextEdit({
37194
37329
  let save = () => {
37195
37330
  };
37196
37331
  trackStudioPendingEdit(new Promise((resolve) => save = resolve));
37197
- noteStudioManualEditSave(open.element);
37198
- teardown();
37199
- save(onCommit({ element: open.element, html: html2, previousHtml: open.original }));
37332
+ const commitText = () => {
37333
+ teardown();
37334
+ return onCommit({ element: open.element, html: html2, previousHtml: open.original });
37335
+ };
37336
+ save(countStudioManualEditSave(open.element, commitText));
37200
37337
  }, [onCommit, teardown]);
37201
37338
  const cancel = useCallback59(() => {
37202
37339
  const open = openRef.current;
@@ -37649,29 +37786,20 @@ var DomEditOverlay = memo26(function DomEditOverlay2({
37649
37786
  return () => onTextEditingChangeRef.current?.(false);
37650
37787
  }, [inlineText.editing]);
37651
37788
  const onPathOffsetCommitRef = useRef70(onPathOffsetCommit);
37652
- onPathOffsetCommitRef.current = (sel, ...rest) => {
37653
- noteStudioManualEditSave(sel.element);
37654
- return onPathOffsetCommit(sel, ...rest);
37655
- };
37789
+ onPathOffsetCommitRef.current = (sel, ...rest) => countStudioManualEditSave(sel.element, () => onPathOffsetCommit(sel, ...rest));
37656
37790
  const onGroupPathOffsetCommitRef = useRef70(onGroupPathOffsetCommit);
37657
37791
  onGroupPathOffsetCommitRef.current = (updates) => {
37658
- if (updates[0]) noteStudioManualEditSave(updates[0].selection.element);
37659
- return onGroupPathOffsetCommit(updates);
37792
+ const save = () => onGroupPathOffsetCommit(updates);
37793
+ return updates[0] ? countStudioManualEditSave(updates[0].selection.element, save) : save();
37660
37794
  };
37661
37795
  const onBoxSizeCommitRef = useRef70(onBoxSizeCommit);
37662
- onBoxSizeCommitRef.current = (sel, ...rest) => {
37663
- noteStudioManualEditSave(sel.element);
37664
- return onBoxSizeCommit(sel, ...rest);
37665
- };
37796
+ onBoxSizeCommitRef.current = (sel, ...rest) => countStudioManualEditSave(sel.element, () => onBoxSizeCommit(sel, ...rest));
37666
37797
  const onRotationCommitRef = useRef70(onRotationCommit);
37667
- onRotationCommitRef.current = (sel, next) => {
37668
- noteStudioManualEditSave(sel.element);
37669
- return onRotationCommit(sel, next);
37670
- };
37798
+ onRotationCommitRef.current = (sel, next) => countStudioManualEditSave(sel.element, () => onRotationCommit(sel, next));
37671
37799
  const onStyleCommitRef = useRef70(onStyleCommit);
37672
37800
  onStyleCommitRef.current = onStyleCommit && ((property, value) => {
37673
- if (selectionRef.current) noteStudioManualEditSave(selectionRef.current.element);
37674
- return onStyleCommit(property, value);
37801
+ const save = () => onStyleCommit(property, value);
37802
+ return selectionRef.current ? countStudioManualEditSave(selectionRef.current.element, save) : save();
37675
37803
  });
37676
37804
  const onBlockedMoveRef = useRef70(onBlockedMove);
37677
37805
  onBlockedMoveRef.current = onBlockedMove;
@@ -42444,10 +42572,8 @@ var SnapToolbar = memo28(function SnapToolbar2() {
42444
42572
  useEffect55(() => {
42445
42573
  const handleKeyDown = (e) => {
42446
42574
  if (e.defaultPrevented) return;
42447
- const t2 = e.target;
42448
- if (t2 instanceof HTMLInputElement || t2 instanceof HTMLTextAreaElement) return;
42449
- if (t2 instanceof HTMLElement && t2.isContentEditable) return;
42450
- if (t2 instanceof HTMLIFrameElement) return;
42575
+ if (ownsPlainKeys(e.target)) return;
42576
+ if (e.target instanceof HTMLIFrameElement) return;
42451
42577
  if (e.key === "s" && !e.metaKey && !e.ctrlKey && !e.altKey) {
42452
42578
  e.preventDefault();
42453
42579
  updatePrefs({ snapEnabled: !prefs.snapEnabled });
@@ -42824,6 +42950,9 @@ import { useCallback as useCallback67, useMemo as useMemo44 } from "react";
42824
42950
  // src/hooks/useGsapTweenCache.ts
42825
42951
  import { useEffect as useEffect57, useMemo as useMemo43, useRef as useRef77, useState as useState49, useCallback as useCallback66 } from "react";
42826
42952
 
42953
+ // src/hooks/gsapRuntimeBridge.ts
42954
+ import { isXYPositionWrite } from "@hyperframes/parsers/gsap-constants";
42955
+
42827
42956
  // src/hooks/gsapRuntimeReaders.ts
42828
42957
  import { classifyPropertyGroup as classifyPropertyGroup3 } from "@hyperframes/core/gsap-parser";
42829
42958
  import {
@@ -43005,6 +43134,7 @@ function readAllAnimatedProperties(iframe, selector, anim, group) {
43005
43134
  }
43006
43135
 
43007
43136
  // src/hooks/gsapDragPositionCommit.ts
43137
+ import { PROPERTY_GROUPS } from "@hyperframes/parsers/gsap-constants";
43008
43138
  function buildTemporalArcKeyframes(anim, percentage, properties) {
43009
43139
  return [
43010
43140
  ...(anim.keyframes?.keyframes ?? []).filter((keyframe) => Math.abs(keyframe.percentage - percentage) > KEYFRAME_PCT_MATCH).map((keyframe) => ({
@@ -43099,7 +43229,11 @@ async function commitFlatViaKeyframes(selection, anim, properties, callbacks, be
43099
43229
  if (runtime && ts !== null) {
43100
43230
  const { gsapLib, el, mainTl } = runtime;
43101
43231
  const draggedValues = {};
43102
- for (const key2 of Object.keys(properties)) {
43232
+ const channelsTheClearWipes = /* @__PURE__ */ new Set([
43233
+ ...Object.keys(properties),
43234
+ ...PROPERTY_GROUPS.position
43235
+ ]);
43236
+ for (const key2 of channelsTheClearWipes) {
43103
43237
  const v = Number(gsapLib.getProperty(el, key2));
43104
43238
  if (Number.isFinite(v)) draggedValues[key2] = v;
43105
43239
  }
@@ -43573,7 +43707,7 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
43573
43707
  if (!selector) return { status: "blocked", reason: "no-selector" };
43574
43708
  const commitMutation = oneUndoStep(gestureCommit);
43575
43709
  let workingAnimations = animations;
43576
- const isPosWrite = (a) => a.targetSelector === selector && a.propertyGroup === "position";
43710
+ const isPosWrite = (a) => a.targetSelector === selector && isXYPositionWrite(a);
43577
43711
  if (animations.filter(isPosWrite).length > 1 && fetchFallbackAnimations) {
43578
43712
  const fresh = await fetchFallbackAnimations();
43579
43713
  const dupes = fresh.filter(isPosWrite);
@@ -43627,7 +43761,7 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
43627
43761
  if (fetchFallbackAnimations) {
43628
43762
  const fresh = await fetchFallbackAnimations();
43629
43763
  const freshMatch = fresh.find(
43630
- (a) => a.targetSelector === posAnim.targetSelector && a.propertyGroup === posAnim.propertyGroup
43764
+ (a) => a.targetSelector === posAnim.targetSelector && a.propertyGroup === posAnim.propertyGroup && isXYPositionWrite(a) === isXYPositionWrite(posAnim)
43631
43765
  );
43632
43766
  if (freshMatch && freshMatch.id !== posAnim.id) {
43633
43767
  posAnim = freshMatch;
@@ -48839,14 +48973,199 @@ import {
48839
48973
  audioGainToFaderPosition as audioGainToFaderPosition2,
48840
48974
  audioGainToText as audioGainToText2
48841
48975
  } from "@hyperframes/core/audio-gain";
48976
+
48977
+ // src/components/editor/mediaAudioEdits.ts
48978
+ import {
48979
+ HF_AUDIO_AUTOMATION_ATTR as HF_AUDIO_AUTOMATION_ATTR4,
48980
+ RATE_TARGET,
48981
+ parseAutomation as parseAutomation3,
48982
+ serializeAutomation as serializeAutomation2
48983
+ } from "@hyperframes/core/audio-automation";
48984
+ import { HF_AUDIO_CARVE_ATTR as HF_AUDIO_CARVE_ATTR2 } from "@hyperframes/core/audio-carve";
48985
+ import { HF_AUDIO_FADE_IN_ATTR as HF_AUDIO_FADE_IN_ATTR2, HF_AUDIO_FADE_OUT_ATTR as HF_AUDIO_FADE_OUT_ATTR2 } from "@hyperframes/core/audio-fade";
48986
+ import { HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR4 } from "@hyperframes/core/audio-fx";
48987
+ import { HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR3 } from "@hyperframes/core/audio-groups";
48988
+ var MEDIA_LINK_ATTR = "data-link";
48989
+ var SOUND_KEPT_ON_LINKED_AUDIO_STAGE = "Background removed. Sound kept on a linked audio track.";
48990
+ var MOVED_SOUND_ATTRS = [
48991
+ "data-volume",
48992
+ HF_AUDIO_FADE_IN_ATTR2,
48993
+ HF_AUDIO_FADE_OUT_ATTR2,
48994
+ HF_AUDIO_FX_ATTR4,
48995
+ HF_AUDIO_CARVE_ATTR2,
48996
+ HF_AUDIO_GROUP_ATTR3
48997
+ ];
48998
+ var COPIED_TIMING_ATTRS = [
48999
+ "start",
49000
+ "end",
49001
+ "duration",
49002
+ "media-start",
49003
+ "playback-start",
49004
+ "playback-rate",
49005
+ "source-duration"
49006
+ ];
49007
+ var htmlAttr = (property, value) => ({
49008
+ type: "html-attribute",
49009
+ property,
49010
+ value
49011
+ });
49012
+ var dataAttr = (property, value) => ({
49013
+ type: "attribute",
49014
+ property,
49015
+ value
49016
+ });
49017
+ function stripDataPrefix(name) {
49018
+ return name.startsWith("data-") ? name.slice("data-".length) : name;
49019
+ }
49020
+ function probedHasAudioValue(probedHasAudio) {
49021
+ if (probedHasAudio === null) return null;
49022
+ return probedHasAudio ? "true" : "false";
49023
+ }
49024
+ function mutedToggleOps(input) {
49025
+ const muted = htmlAttr("muted", input.nextMuted ? "true" : null);
49026
+ if (!input.isVideo) return [muted];
49027
+ const hasAudio = input.nextMuted ? null : probedHasAudioValue(input.probedHasAudio);
49028
+ return [muted, dataAttr("has-audio", hasAudio)];
49029
+ }
49030
+ function hasAudioToggleOps(next) {
49031
+ return next ? [dataAttr("has-audio", "true"), htmlAttr("muted", null)] : [dataAttr("has-audio", null), htmlAttr("muted", "true")];
49032
+ }
49033
+ function cutoutOps(input) {
49034
+ const src = htmlAttr("src", input.cutoutSrc);
49035
+ return input.isVideo ? [src, htmlAttr("muted", "true"), dataAttr("has-audio", null)] : [src];
49036
+ }
49037
+ function firstFreeName(base2, taken) {
49038
+ if (!taken.has(base2)) return base2;
49039
+ let n = 2;
49040
+ while (taken.has(`${base2}-${n}`)) n += 1;
49041
+ return `${base2}-${n}`;
49042
+ }
49043
+ function mintLinkId(doc) {
49044
+ const taken = /* @__PURE__ */ new Set();
49045
+ for (const el of Array.from(doc.querySelectorAll(`[id], [${MEDIA_LINK_ATTR}]`))) {
49046
+ if (el.id) taken.add(el.id);
49047
+ const link = el.getAttribute(MEDIA_LINK_ATTR);
49048
+ if (link) taken.add(link);
49049
+ }
49050
+ return firstFreeName("link", taken);
49051
+ }
49052
+ function firstFreeTrackIndex(source) {
49053
+ const used = new Set(
49054
+ Array.from(source.matchAll(/\bdata-track-index=["'](-?\d+)["']/g), (m) => Number(m[1]))
49055
+ );
49056
+ let track2 = 0;
49057
+ while (used.has(track2)) track2 += 1;
49058
+ return track2;
49059
+ }
49060
+ function splitAutomation(raw) {
49061
+ let lanes;
49062
+ try {
49063
+ lanes = parseAutomation3(raw).lanes;
49064
+ } catch {
49065
+ return { videoKeeps: null, audioTakes: raw };
49066
+ }
49067
+ const rateLanes = lanes.filter((lane) => lane.target === RATE_TARGET);
49068
+ const soundLanes = lanes.filter((lane) => lane.target !== RATE_TARGET);
49069
+ return {
49070
+ videoKeeps: rateLanes.length > 0 ? serializeAutomation2({ version: 1, lanes: rateLanes }) : null,
49071
+ audioTakes: serializeAutomation2({ version: 1, lanes: [...soundLanes, ...rateLanes] })
49072
+ };
49073
+ }
49074
+ function formatAttrs(attrs) {
49075
+ return attrs.map(([name, value]) => `${name}="${escapeHtmlAttribute(value)}"`).join(" ");
49076
+ }
49077
+ function insertBeforeTarget(source, target, markup) {
49078
+ const match = findTagByTarget(source, target);
49079
+ if (!match) return source;
49080
+ const lineStart = source.lastIndexOf("\n", match.start) + 1;
49081
+ const indent = /^[ \t]*$/.test(source.slice(lineStart, match.start)) ? source.slice(lineStart, match.start) : "";
49082
+ const separator = indent ? `
49083
+ ${indent}` : "";
49084
+ return `${source.slice(0, match.start)}${markup}${separator}${source.slice(match.start)}`;
49085
+ }
49086
+ function buildKeepSoundCutoutEdit(input) {
49087
+ const { video, target } = input;
49088
+ const linkId = mintLinkId(video.ownerDocument);
49089
+ const originalSrc = video.getAttribute("src") ?? "";
49090
+ const movedSound = [];
49091
+ const ops = [...cutoutOps({ isVideo: true, cutoutSrc: input.cutoutSrc })];
49092
+ for (const name of MOVED_SOUND_ATTRS) {
49093
+ const value = video.getAttribute(name);
49094
+ if (value === null) continue;
49095
+ movedSound.push([name, value]);
49096
+ ops.push(dataAttr(stripDataPrefix(name), null));
49097
+ }
49098
+ const automation = video.getAttribute(HF_AUDIO_AUTOMATION_ATTR4);
49099
+ if (automation !== null) {
49100
+ const { videoKeeps, audioTakes } = splitAutomation(automation);
49101
+ movedSound.push([HF_AUDIO_AUTOMATION_ATTR4, audioTakes]);
49102
+ ops.push(dataAttr(stripDataPrefix(HF_AUDIO_AUTOMATION_ATTR4), videoKeeps));
49103
+ }
49104
+ ops.push(dataAttr(stripDataPrefix(MEDIA_LINK_ATTR), linkId));
49105
+ const prepareContent = (source) => {
49106
+ const audioId = firstFreeName(
49107
+ `${input.videoId || "video"}-audio`,
49108
+ new Set(collectHtmlIds(source))
49109
+ );
49110
+ const timing = [];
49111
+ for (const name of COPIED_TIMING_ATTRS) {
49112
+ const value = readAttributeByTarget(source, target, name);
49113
+ if (value !== void 0) timing.push([`data-${name}`, value]);
49114
+ }
49115
+ const attrs = [
49116
+ ["id", audioId],
49117
+ ["data-hf-id", `hf-${generateId()}`],
49118
+ ["class", "clip"],
49119
+ ["src", originalSrc],
49120
+ ...timing,
49121
+ ["data-track-index", String(firstFreeTrackIndex(source))],
49122
+ [MEDIA_LINK_ATTR, linkId],
49123
+ ...movedSound
49124
+ ];
49125
+ return insertBeforeTarget(source, target, `<audio ${formatAttrs(attrs)}></audio>`);
49126
+ };
49127
+ return { ops, prepareContent };
49128
+ }
49129
+ var isVideoSelection = (element) => element.tagName === "video";
49130
+ async function commitMutedToggle(ctx3, nextMuted) {
49131
+ const isVideo = isVideoSelection(ctx3.element);
49132
+ const probedHasAudio = isVideo && !nextMuted && ctx3.projectId && ctx3.projectSrc ? await resolveAssetHasAudio(ctx3.projectId, ctx3.projectSrc) : null;
49133
+ await ctx3.commit(ctx3.element, mutedToggleOps({ isVideo, nextMuted, probedHasAudio }), {
49134
+ label: "Edit muted"
49135
+ });
49136
+ }
49137
+ async function commitHasAudioToggle(ctx3, next) {
49138
+ await ctx3.commit(ctx3.element, hasAudioToggleOps(next), { label: "Edit has audio" });
49139
+ }
49140
+ async function commitCutout(ctx3, cutoutSrc, keepSound) {
49141
+ const isVideo = isVideoSelection(ctx3.element);
49142
+ const label = "Remove background";
49143
+ const keep = isVideo && keepSound;
49144
+ const landed = keep ? await commitKeepSoundCutout(ctx3, cutoutSrc, label) : await ctx3.commit(ctx3.element, cutoutOps({ isVideo, cutoutSrc }), { label });
49145
+ if (!landed) throw new Error("Couldn't apply the cutout");
49146
+ return keep ? SOUND_KEPT_ON_LINKED_AUDIO_STAGE : "Applied cutout";
49147
+ }
49148
+ function commitKeepSoundCutout(ctx3, cutoutSrc, label) {
49149
+ const edit = buildKeepSoundCutoutEdit({
49150
+ video: ctx3.element.element,
49151
+ videoId: ctx3.element.id ?? null,
49152
+ target: buildDomEditPatchTarget(ctx3.element),
49153
+ cutoutSrc
49154
+ });
49155
+ return ctx3.commit(ctx3.element, edit.ops, { label, prepareContent: edit.prepareContent });
49156
+ }
49157
+
49158
+ // src/components/editor/propertyPanelMediaSection.tsx
48842
49159
  import { Fragment as Fragment31, jsx as jsx110, jsxs as jsxs77 } from "react/jsx-runtime";
48843
49160
  function MediaSection({
49161
+ projectId = null,
48844
49162
  projectDir,
48845
49163
  element,
48846
49164
  styles,
48847
49165
  onSetStyle,
48848
49166
  onSetAttribute,
48849
49167
  onSetHtmlAttribute,
49168
+ onSetAttributeBatch,
48850
49169
  onRemoveBackground
48851
49170
  }) {
48852
49171
  const track2 = useTrackDesignInput();
@@ -48860,7 +49179,11 @@ function MediaSection({
48860
49179
  const { mediaStart, mediaStartAttr } = readClipInPoint(element.dataAttributes);
48861
49180
  const hasLoop = el.hasAttribute("loop");
48862
49181
  const hasMuted = el.hasAttribute("muted");
48863
- const hasAudio = element.dataAttributes["has-audio"] === "true";
49182
+ const hasAudio = isVideoAudible({
49183
+ tag: el.tagName,
49184
+ hasAudioAttr: element.dataAttributes["has-audio"],
49185
+ muted: el.hasAttribute("muted")
49186
+ });
48864
49187
  const playbackRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1;
48865
49188
  const objectFit = styles["object-fit"] || "contain";
48866
49189
  const objectPosition = styles["object-position"] || "center";
@@ -48880,13 +49203,7 @@ function MediaSection({
48880
49203
  setRemoveProgress(null);
48881
49204
  setCreatePlate(false);
48882
49205
  }, [srcAttr]);
48883
- const applyCutoutResult = async (result) => {
48884
- await onSetHtmlAttribute("src", result.outputPath);
48885
- if (isVideo) {
48886
- await onSetAttribute("has-audio", "");
48887
- await onSetHtmlAttribute("muted", "true");
48888
- }
48889
- };
49206
+ const mediaEdit = { element, projectId, projectSrc, commit: onSetAttributeBatch };
48890
49207
  const runBackgroundRemoval = async () => {
48891
49208
  if (!onRemoveBackground || !projectSrc || removeBusy) return;
48892
49209
  track2("button", "Remove background");
@@ -48898,13 +49215,8 @@ function MediaSection({
48898
49215
  quality,
48899
49216
  onProgress: setRemoveProgress
48900
49217
  });
48901
- await applyCutoutResult(result);
48902
- setRemoveProgress({
48903
- status: "complete",
48904
- progress: 100,
48905
- stage: "Applied cutout",
48906
- ...result
48907
- });
49218
+ const stage = await commitCutout(mediaEdit, result.outputPath, hasAudio);
49219
+ setRemoveProgress({ status: "complete", progress: 100, stage, ...result });
48908
49220
  } catch (error) {
48909
49221
  setRemoveProgress({
48910
49222
  status: "failed",
@@ -49109,9 +49421,7 @@ function MediaSection({
49109
49421
  {
49110
49422
  trackName: "Muted",
49111
49423
  value: hasMuted ? "on" : "off",
49112
- onChange: (next) => {
49113
- void onSetHtmlAttribute("muted", next === "on" ? "true" : null);
49114
- },
49424
+ onChange: (next) => void commitMutedToggle(mediaEdit, next === "on"),
49115
49425
  options: [
49116
49426
  { label: "On", value: "on" },
49117
49427
  { label: "Off", value: "off" }
@@ -49127,15 +49437,7 @@ function MediaSection({
49127
49437
  {
49128
49438
  trackName: "Has audio track",
49129
49439
  value: hasAudio ? "yes" : "no",
49130
- onChange: (next) => {
49131
- if (next === "yes") {
49132
- void onSetAttribute("has-audio", "true");
49133
- void onSetHtmlAttribute("muted", null);
49134
- } else {
49135
- void onSetAttribute("has-audio", "");
49136
- void onSetHtmlAttribute("muted", "true");
49137
- }
49138
- },
49440
+ onChange: (next) => void commitHasAudioToggle(mediaEdit, next === "yes"),
49139
49441
  options: [
49140
49442
  { label: "Yes", value: "yes" },
49141
49443
  { label: "No", value: "no" }
@@ -58136,14 +58438,14 @@ function motionSectionLabel(args) {
58136
58438
  // src/components/editor/propertyPanelAudioFxGroup.tsx
58137
58439
  import { useMemo as useMemo58, useState as useState81 } from "react";
58138
58440
  import {
58139
- HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR6,
58441
+ HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR7,
58140
58442
  HF_AUDIO_FX_DATA_KEY as HF_AUDIO_FX_DATA_KEY2,
58141
58443
  parseAudioFxChain as parseAudioFxChain4,
58142
58444
  serializeAudioFxChain as serializeAudioFxChain5
58143
58445
  } from "@hyperframes/core/audio-fx";
58144
58446
  import {
58145
58447
  classifyAudioName as classifyAudioName4,
58146
- HF_AUDIO_CARVE_ATTR as HF_AUDIO_CARVE_ATTR3,
58448
+ HF_AUDIO_CARVE_ATTR as HF_AUDIO_CARVE_ATTR4,
58147
58449
  normalizeCarveSettings as normalizeCarveSettings2
58148
58450
  } from "@hyperframes/core/audio-carve";
58149
58451
  import {
@@ -60464,13 +60766,13 @@ function useFxChainObserved(element, chain, carve, automation, onSetAttributeQui
60464
60766
  // src/components/editor/useFxCarve.ts
60465
60767
  import { useEffect as useEffect86, useRef as useRef101 } from "react";
60466
60768
  import {
60467
- HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR4,
60769
+ HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR5,
60468
60770
  serializeAudioFxChain as serializeAudioFxChain3
60469
60771
  } from "@hyperframes/core/audio-fx";
60470
60772
  import {
60471
60773
  clipsOverlap,
60472
60774
  DEFAULT_CARVE as DEFAULT_CARVE3,
60473
- HF_AUDIO_CARVE_ATTR as HF_AUDIO_CARVE_ATTR2
60775
+ HF_AUDIO_CARVE_ATTR as HF_AUDIO_CARVE_ATTR3
60474
60776
  } from "@hyperframes/core/audio-carve";
60475
60777
  import { resolveAudioGroups as resolveAudioGroups3, resolveCarveSourceIds as resolveCarveSourceIds2 } from "@hyperframes/core/audio-groups";
60476
60778
 
@@ -60637,7 +60939,7 @@ async function dropCarveOutput(chain, automation, onSetAttributeQuiet) {
60637
60939
  const kept = chain.nodes.filter((n) => !n.fromCarve);
60638
60940
  if (kept.length !== chain.nodes.length) {
60639
60941
  await onSetAttributeQuiet(
60640
- HF_AUDIO_FX_ATTR4,
60942
+ HF_AUDIO_FX_ATTR5,
60641
60943
  kept.length ? serializeAudioFxChain3({ version: 1, nodes: kept }) : null
60642
60944
  );
60643
60945
  }
@@ -60690,7 +60992,7 @@ function useFxCarve(element, chain, carve, automation, onSetAttributeQuiet, writ
60690
60992
  });
60691
60993
  const generatedOutputStands = Boolean(next?.enabled) && (next?.sources.length ?? 0) > 0;
60692
60994
  if (!generatedOutputStands) await dropCarveOutput(chain, automation, onSetAttributeQuiet);
60693
- await onSetAttributeQuiet(HF_AUDIO_CARVE_ATTR2, next ? JSON.stringify(next) : null);
60995
+ await onSetAttributeQuiet(HF_AUDIO_CARVE_ATTR3, next ? JSON.stringify(next) : null);
60694
60996
  if (carveNeedsReanalysis(carve, next)) await analyse(next);
60695
60997
  };
60696
60998
  const autoCarveInFlight = useRef101(false);
@@ -60703,7 +61005,7 @@ function useFxCarve(element, chain, carve, automation, onSetAttributeQuiet, writ
60703
61005
  const persistMeasuredCarve = async (measured) => {
60704
61006
  const { bands, carved, duck, voiceMix } = measured;
60705
61007
  const { next, carvedNodes, duckNode } = mintCarveNodes(chain, carved, duck);
60706
- await onSetAttributeQuiet(HF_AUDIO_FX_ATTR4, serializeAudioFxChain3(next));
61008
+ await onSetAttributeQuiet(HF_AUDIO_FX_ATTR5, serializeAudioFxChain3(next));
60707
61009
  const lanes = carveLanes(carvedNodes, duckNode, duck, voiceMix, bands);
60708
61010
  const carriedOver = withoutCarveLanes(automation, chain);
60709
61011
  if (lanes.length > 0 || carriedOver.lanes.length !== automation.lanes.length) {
@@ -60763,7 +61065,7 @@ function useFxCarve(element, chain, carve, automation, onSetAttributeQuiet, writ
60763
61065
 
60764
61066
  // src/components/editor/propertyPanelAudioFxGroup.tsx
60765
61067
  import {
60766
- HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR3,
61068
+ HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR4,
60767
61069
  HF_AUDIO_GROUP_TAG as HF_AUDIO_GROUP_TAG4,
60768
61070
  resolveAudioGroups as resolveAudioGroups4
60769
61071
  } from "@hyperframes/core/audio-groups";
@@ -60771,7 +61073,7 @@ import {
60771
61073
  // src/components/editor/useFxLevelling.ts
60772
61074
  import { useRef as useRef102, useState as useState80 } from "react";
60773
61075
  import {
60774
- HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR5,
61076
+ HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR6,
60775
61077
  serializeAudioFxChain as serializeAudioFxChain4
60776
61078
  } from "@hyperframes/core/audio-fx";
60777
61079
  import { levellingResult, removeLevelling } from "@hyperframes/core/audio-leveller";
@@ -60816,7 +61118,7 @@ function useFxLevelling(element, chain, automation, onSetAttributeQuiet, onSetAt
60816
61118
  const result = measureLevelling(audio);
60817
61119
  if (!result) return;
60818
61120
  trackLeveller("run");
60819
- await onSetAttributeQuiet(HF_AUDIO_FX_ATTR5, serializeAudioFxChain4(result.chain));
61121
+ await onSetAttributeQuiet(HF_AUDIO_FX_ATTR6, serializeAudioFxChain4(result.chain));
60820
61122
  const lane = result.lane;
60821
61123
  if (lane) {
60822
61124
  void onSetAttributeQuiet(
@@ -60835,7 +61137,7 @@ function useFxLevelling(element, chain, automation, onSetAttributeQuiet, onSetAt
60835
61137
  const stopLevelAudition = () => {
60836
61138
  setAuditioningLevel(false);
60837
61139
  void onSetAttributeLive(
60838
- HF_AUDIO_FX_ATTR5,
61140
+ HF_AUDIO_FX_ATTR6,
60839
61141
  chain.nodes.length ? serializeAudioFxChain4(chain) : null
60840
61142
  );
60841
61143
  void onSetAttributeLive(HF_AUDIO_AUTOMATION_ATTR, automationAttrValue(automation) || null);
@@ -60847,7 +61149,7 @@ function useFxLevelling(element, chain, automation, onSetAttributeQuiet, onSetAt
60847
61149
  if (!audio || run !== auditionRun.current) return;
60848
61150
  const result = measureLevelling(audio);
60849
61151
  if (!result || run !== auditionRun.current) return;
60850
- void onSetAttributeLive(HF_AUDIO_FX_ATTR5, serializeAudioFxChain4(result.chain));
61152
+ void onSetAttributeLive(HF_AUDIO_FX_ATTR6, serializeAudioFxChain4(result.chain));
60851
61153
  const lane = result.lane;
60852
61154
  if (lane) {
60853
61155
  void onSetAttributeLive(
@@ -60868,7 +61170,7 @@ function useFxLevelling(element, chain, automation, onSetAttributeQuiet, onSetAt
60868
61170
  const removeLeveller = () => {
60869
61171
  trackLeveller("removed");
60870
61172
  const { chain: next, removedTarget } = removeLevelling(chain);
60871
- void onSetAttributeQuiet(HF_AUDIO_FX_ATTR5, serializeAudioFxChain4(next));
61173
+ void onSetAttributeQuiet(HF_AUDIO_FX_ATTR6, serializeAudioFxChain4(next));
60872
61174
  if (removedTarget) {
60873
61175
  void onSetAttributeQuiet(
60874
61176
  HF_AUDIO_AUTOMATION_ATTR,
@@ -60892,7 +61194,7 @@ function auditionSpansFor(element) {
60892
61194
  if (element.tagName?.toLowerCase() !== HF_AUDIO_GROUP_TAG4 || !element.id) return [];
60893
61195
  const doc = element.element?.ownerDocument;
60894
61196
  if (!doc) return [];
60895
- return [...doc.querySelectorAll(`audio[${HF_AUDIO_GROUP_ATTR3}]`)].filter((member) => member.getAttribute(HF_AUDIO_GROUP_ATTR3) === element.id).flatMap((member) => {
61197
+ return [...doc.querySelectorAll(`audio[${HF_AUDIO_GROUP_ATTR4}]`)].filter((member) => member.getAttribute(HF_AUDIO_GROUP_ATTR4) === element.id).flatMap((member) => {
60896
61198
  const span = auditionSpan(
60897
61199
  member.getAttribute("data-start") ?? void 0,
60898
61200
  member.getAttribute("data-duration") ?? void 0
@@ -61038,7 +61340,7 @@ function AudioFxGroup({
61038
61340
  // bypassing an effect is applied to the running graph, so a reload would
61039
61341
  // only interrupt the audio to reach the same state.
61040
61342
  onSetAttributeQuiet(
61041
- HF_AUDIO_FX_ATTR6,
61343
+ HF_AUDIO_FX_ATTR7,
61042
61344
  next.nodes.length ? serializeAudioFxChain5(next) : null
61043
61345
  )
61044
61346
  ),
@@ -61048,11 +61350,11 @@ function AudioFxGroup({
61048
61350
  // Live writes skip the preview refresh entirely, so dragging a knob no
61049
61351
  // longer reloads the composition and restarts playback on every pixel.
61050
61352
  // The gesture-end write above is the one that resyncs.
61051
- onSetAttributeLive(HF_AUDIO_FX_ATTR6, next.nodes.length ? serializeAudioFxChain5(next) : null)
61353
+ onSetAttributeLive(HF_AUDIO_FX_ATTR7, next.nodes.length ? serializeAudioFxChain5(next) : null)
61052
61354
  ),
61053
61355
  carve,
61054
61356
  onCarveChange: (next) => void setCarve(next),
61055
- onCarvePreview: (next) => onSetAttributeLive(HF_AUDIO_CARVE_ATTR3, JSON.stringify(next)),
61357
+ onCarvePreview: (next) => onSetAttributeLive(HF_AUDIO_CARVE_ATTR4, JSON.stringify(next)),
61056
61358
  sourceOptions,
61057
61359
  trackKind: classifyAudioName4(element.id, element.element?.getAttribute("src")),
61058
61360
  onLevel: () => void runLeveller(),
@@ -61072,7 +61374,7 @@ function AudioFxGroup({
61072
61374
  // src/components/editor/useVolumeAutomation.ts
61073
61375
  import {
61074
61376
  HF_AUDIO_AUTOMATION_DATA_KEY as HF_AUDIO_AUTOMATION_DATA_KEY2,
61075
- RATE_TARGET,
61377
+ RATE_TARGET as RATE_TARGET2,
61076
61378
  resolveAutomationRange as resolveAutomationRange6,
61077
61379
  sampleAutomationLane as sampleAutomationLane9,
61078
61380
  VOLUME_TARGET as VOLUME_TARGET2
@@ -61105,7 +61407,7 @@ function useVolumeAutomation(element, currentTime, onSetAttributeQuiet) {
61105
61407
  const current = Number.isFinite(parsed) ? parsed : 1;
61106
61408
  const volume = laneBinding(VOLUME_TARGET2, current);
61107
61409
  const rateAttr = Number.parseFloat(element.dataAttributes?.["playback-rate"] ?? "");
61108
- const rate = laneBinding(RATE_TARGET, Number.isFinite(rateAttr) && rateAttr > 0 ? rateAttr : 1);
61410
+ const rate = laneBinding(RATE_TARGET2, Number.isFinite(rateAttr) && rateAttr > 0 ? rateAttr : 1);
61109
61411
  return {
61110
61412
  volumeAutomated: volume.automated,
61111
61413
  automatedVolumeValue: volume.automatedValue,
@@ -61260,12 +61562,14 @@ function finiteSeconds(seconds) {
61260
61562
  // src/components/editor/propertyPanelFlatMediaSection.tsx
61261
61563
  import { Fragment as Fragment43, jsx as jsx160, jsxs as jsxs122 } from "react/jsx-runtime";
61262
61564
  function FlatMediaSection({
61565
+ projectId = null,
61263
61566
  projectDir,
61264
61567
  element,
61265
61568
  styles,
61266
61569
  onSetStyle,
61267
61570
  onSetAttribute,
61268
61571
  onSetHtmlAttribute,
61572
+ onSetAttributeBatch,
61269
61573
  onRemoveBackground,
61270
61574
  volumeAutomated,
61271
61575
  onAutomateVolume,
@@ -61293,7 +61597,11 @@ function FlatMediaSection({
61293
61597
  const fadeMax = clipDuration2 > 0 ? clipDuration2 : 10;
61294
61598
  const hasLoop = el.hasAttribute("loop");
61295
61599
  const hasMuted = el.hasAttribute("muted");
61296
- const hasAudio = element.dataAttributes["has-audio"] === "true";
61600
+ const hasAudio = isVideoAudible({
61601
+ tag: el.tagName,
61602
+ hasAudioAttr: element.dataAttributes["has-audio"],
61603
+ muted: el.hasAttribute("muted")
61604
+ });
61297
61605
  const objectFit = styles["object-fit"] || "contain";
61298
61606
  const objectPosition = styles["object-position"] || "center";
61299
61607
  const srcAttr = el.getAttribute("src") ?? "";
@@ -61309,13 +61617,7 @@ function FlatMediaSection({
61309
61617
  setRemoveProgress(null);
61310
61618
  setCreatePlate(false);
61311
61619
  }, [srcAttr]);
61312
- const applyCutoutResult = async (result) => {
61313
- await onSetHtmlAttribute("src", result.outputPath);
61314
- if (isVideo) {
61315
- await onSetAttribute("has-audio", "");
61316
- await onSetHtmlAttribute("muted", "true");
61317
- }
61318
- };
61620
+ const mediaEdit = { element, projectId, projectSrc, commit: onSetAttributeBatch };
61319
61621
  const runBackgroundRemoval = async () => {
61320
61622
  if (!onRemoveBackground || !projectSrc || removeBusy) return;
61321
61623
  track2("button", "Remove background");
@@ -61327,8 +61629,8 @@ function FlatMediaSection({
61327
61629
  quality,
61328
61630
  onProgress: setRemoveProgress
61329
61631
  });
61330
- await applyCutoutResult(result);
61331
- setRemoveProgress({ status: "complete", progress: 100, stage: "Applied cutout", ...result });
61632
+ const stage = await commitCutout(mediaEdit, result.outputPath, hasAudio);
61633
+ setRemoveProgress({ status: "complete", progress: 100, stage, ...result });
61332
61634
  } catch (error) {
61333
61635
  setRemoveProgress({
61334
61636
  status: "failed",
@@ -61551,7 +61853,7 @@ function FlatMediaSection({
61551
61853
  {
61552
61854
  label: "Muted",
61553
61855
  checked: hasMuted,
61554
- onChange: (next) => void onSetHtmlAttribute("muted", next ? "true" : null)
61856
+ onChange: (next) => void commitMutedToggle(mediaEdit, next)
61555
61857
  }
61556
61858
  ),
61557
61859
  isVideo && /* @__PURE__ */ jsx160(
@@ -61559,15 +61861,7 @@ function FlatMediaSection({
61559
61861
  {
61560
61862
  label: "Has audio track",
61561
61863
  checked: hasAudio,
61562
- onChange: (next) => {
61563
- if (next) {
61564
- void onSetAttribute("has-audio", "true");
61565
- void onSetHtmlAttribute("muted", null);
61566
- } else {
61567
- void onSetAttribute("has-audio", "");
61568
- void onSetHtmlAttribute("muted", "true");
61569
- }
61570
- }
61864
+ onChange: (next) => void commitHasAudioToggle(mediaEdit, next)
61571
61865
  }
61572
61866
  )
61573
61867
  ] }),
@@ -65069,7 +65363,6 @@ function PropertyPanelFlat({
65069
65363
  clipboardCopied,
65070
65364
  onCopyElementInfo,
65071
65365
  projectId,
65072
- projectDir,
65073
65366
  assets,
65074
65367
  previewIframeRef,
65075
65368
  onClearSelection,
@@ -65081,8 +65374,6 @@ function PropertyPanelFlat({
65081
65374
  onSetAttributeLive,
65082
65375
  onSetAttributeQuiet,
65083
65376
  onApplyColorGradingScope,
65084
- onSetHtmlAttribute,
65085
- onRemoveBackground,
65086
65377
  onSetText,
65087
65378
  onSetTextFieldStyle,
65088
65379
  onPreviewTextFieldStyle,
@@ -65137,7 +65428,8 @@ function PropertyPanelFlat({
65137
65428
  onUnroll,
65138
65429
  onUpdateKeyframeEase,
65139
65430
  onUpdateSegmentEase,
65140
- onSetAllKeyframeEases
65431
+ onSetAllKeyframeEases,
65432
+ ...forwardedProps
65141
65433
  }) {
65142
65434
  const [openGroupId, setOpenGroupId] = useState93(
65143
65435
  () => isTextEditableSelection(element) ? "text" : showEditableSections ? "style" : sections.media ? "media" : (
@@ -65446,13 +65738,12 @@ function PropertyPanelFlat({
65446
65738
  content: /* @__PURE__ */ jsx172(
65447
65739
  FlatMediaSection,
65448
65740
  {
65449
- projectDir,
65741
+ ...forwardedProps,
65742
+ projectId,
65450
65743
  element,
65451
65744
  styles,
65452
65745
  onSetStyle,
65453
65746
  onSetAttribute,
65454
- onSetHtmlAttribute,
65455
- onRemoveBackground,
65456
65747
  ...volumeAutomation
65457
65748
  }
65458
65749
  )
@@ -65730,7 +66021,6 @@ import { createElement as createElement3 } from "react";
65730
66021
  var PropertyPanel = memo36(function PropertyPanel2(props) {
65731
66022
  const {
65732
66023
  projectId,
65733
- projectDir,
65734
66024
  assets,
65735
66025
  element,
65736
66026
  multiSelectCount = 0,
@@ -65744,8 +66034,6 @@ var PropertyPanel = memo36(function PropertyPanel2(props) {
65744
66034
  onSetAttribute,
65745
66035
  onSetAttributeLive,
65746
66036
  onApplyColorGradingScope,
65747
- onSetHtmlAttribute,
65748
- onRemoveBackground,
65749
66037
  onSetManualOffset,
65750
66038
  onSetManualSize,
65751
66039
  onSetManualRotation,
@@ -65984,18 +66272,7 @@ var PropertyPanel = memo36(function PropertyPanel2(props) {
65984
66272
  },
65985
66273
  selectionIdentityKey2(element)
65986
66274
  ),
65987
- sections.media && /* @__PURE__ */ jsx175(
65988
- MediaSection,
65989
- {
65990
- projectDir,
65991
- element,
65992
- styles,
65993
- onSetStyle,
65994
- onSetAttribute,
65995
- onSetHtmlAttribute,
65996
- onRemoveBackground
65997
- }
65998
- ),
66275
+ sections.media && /* @__PURE__ */ jsx175(MediaSection, { ...props, element, styles }),
65999
66276
  sections.layout && /* @__PURE__ */ jsxs136(Section, { title: "Layout", icon: /* @__PURE__ */ jsx175(Move, { size: 15 }), children: [
66000
66277
  /* @__PURE__ */ jsxs136("div", { className: RESPONSIVE_GRID, children: [
66001
66278
  /* @__PURE__ */ jsxs136("div", { className: "flex items-center gap-1", children: [
@@ -69990,20 +70267,43 @@ function diffRestoreDocs(prevDoc, nextDoc) {
69990
70267
  });
69991
70268
  return prevRest === nextRest ? changedElementKeys : null;
69992
70269
  }
69993
- function syncElementAttributes(target, source) {
70270
+ function syncElementAttributes(target, source, base2) {
70271
+ const draws = base2 && studioGestureDraws(target);
70272
+ const merged = draws ? keepDrawn(target, source, base2, draws) : source;
69994
70273
  for (const name of [...target.getAttributeNames()]) {
69995
- if (!source.hasAttribute(name)) target.removeAttribute(name);
70274
+ if (!merged.hasAttribute(name)) target.removeAttribute(name);
69996
70275
  }
69997
- for (const name of source.getAttributeNames()) {
69998
- target.setAttribute(name, source.getAttribute(name) ?? "");
70276
+ for (const name of merged.getAttributeNames()) {
70277
+ target.setAttribute(name, merged.getAttribute(name) ?? "");
69999
70278
  }
70000
70279
  }
70001
- function syncStaleEditMarks(doc, restored) {
70002
- const restoredDoc = parseSavedSource(restored);
70280
+ function keepDrawn(live, source, base2, draws) {
70281
+ const merged = source.cloneNode(false);
70282
+ const keepsMark = draws.includes("translate");
70283
+ for (const name of live.getAttributeNames().filter((n) => n !== "style")) {
70284
+ const value = live.getAttribute(name);
70285
+ const mark = keepsMark && name === STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR;
70286
+ if (mark || value !== base2.getAttribute(name)) merged.setAttribute(name, value);
70287
+ }
70288
+ keepDrawnStyle(live, base2, merged, draws);
70289
+ return merged;
70290
+ }
70291
+ function keepDrawnStyle(live, base2, merged, draws) {
70292
+ const value = (prop) => live.style.getPropertyValue(prop);
70293
+ const drawn = (prop) => draws.includes(prop) || value(prop) !== base2.style.getPropertyValue(prop);
70294
+ const props = /* @__PURE__ */ new Set([...Array.from(live.style), ...Array.from(base2.style), ...draws]);
70295
+ for (const prop of [...props].filter(drawn)) {
70296
+ if (value(prop))
70297
+ merged.style.setProperty(prop, value(prop), live.style.getPropertyPriority(prop));
70298
+ else merged.style.removeProperty(prop);
70299
+ }
70300
+ }
70301
+ function syncStaleEditMarks(doc, file) {
70302
+ const [restoredDoc, previousDoc] = [file.restored, file.previous].map(parseSavedSource);
70003
70303
  for (const live of doc.querySelectorAll(STUDIO_EDIT_ATTRS.map((attr) => `[${attr}]`).join())) {
70004
70304
  const source = findAuthoredElement(restoredDoc, live);
70005
70305
  if (source && STUDIO_EDIT_ATTRS.some((a) => live.getAttribute(a) !== source.getAttribute(a))) {
70006
- syncElementAttributes(live, source);
70306
+ syncElementAttributes(live, source, findAuthoredElement(previousDoc, live) ?? void 0);
70007
70307
  }
70008
70308
  }
70009
70309
  }
@@ -70023,7 +70323,7 @@ function liveScopes(doc, path, isActive) {
70023
70323
  if (isActive) return [doc];
70024
70324
  return Array.from(doc.querySelectorAll(`[data-composition-file="${CSS.escape(path)}"]`));
70025
70325
  }
70026
- function scopeTargets(scope, keys, restoredByKey) {
70326
+ function scopeTargets(scope, keys, restoredByKey, previousByKey) {
70027
70327
  const liveByKey = identityElementMap(scope);
70028
70328
  if (!liveByKey) return null;
70029
70329
  const targets = [];
@@ -70031,7 +70331,7 @@ function scopeTargets(scope, keys, restoredByKey) {
70031
70331
  const live = liveByKey.get(key2);
70032
70332
  const restored = restoredByKey.get(key2);
70033
70333
  if (!live || !restored || live.hasAttribute("data-hf-inner-root")) return null;
70034
- targets.push({ live, restored });
70334
+ targets.push({ live, restored, previous: previousByKey.get(key2) });
70035
70335
  }
70036
70336
  return targets;
70037
70337
  }
@@ -70042,9 +70342,10 @@ function fileTargets(doc, path, isActive, file) {
70042
70342
  if (scripted === null) return null;
70043
70343
  const keys = diffRestoreDocs(prevDoc, nextDoc);
70044
70344
  const restoredByKey = identityElementMap(nextDoc);
70045
- if (!keys || !restoredByKey) return null;
70345
+ const previousByKey = identityElementMap(prevDoc);
70346
+ if (!keys || !restoredByKey || !previousByKey) return null;
70046
70347
  const found = liveScopes(doc, path, isActive).map(
70047
- (scope) => scopeTargets(scope, keys, restoredByKey)
70348
+ (scope) => scopeTargets(scope, keys, restoredByKey, previousByKey)
70048
70349
  );
70049
70350
  if (!found.length || found.includes(null)) return null;
70050
70351
  return { targets: found.flat(), scripted };
@@ -70074,11 +70375,14 @@ function showRestoreInPlace(iframe, activeCompPath, files, currentTime) {
70074
70375
  const win = iframe?.contentWindow;
70075
70376
  const plan = doc && win ? planRestoreTargets(doc, activeCompPath ?? "index.html", files) : null;
70076
70377
  if (!iframe || !win || !plan || plan.scripted) return null;
70077
- const before = plan.targets.map(({ live }) => [live, live.cloneNode(false)]);
70078
- for (const { live, restored } of plan.targets) syncElementAttributes(live, restored);
70378
+ const before = plan.targets.map(
70379
+ ({ live, restored }) => [live, live.cloneNode(false), restored]
70380
+ );
70381
+ for (const { live, restored, previous } of plan.targets)
70382
+ syncElementAttributes(live, restored, previous);
70079
70383
  const putBack = (time) => {
70080
70384
  if (iframe.contentDocument !== doc) return false;
70081
- for (const [live, attributes] of before) syncElementAttributes(live, attributes);
70385
+ for (const [live, attributes, shown2] of before) syncElementAttributes(live, attributes, shown2);
70082
70386
  return finalizeInPlace(iframe, win, time);
70083
70387
  };
70084
70388
  if (finalizeInPlace(iframe, win, currentTime)) return putBack;
@@ -70099,16 +70403,23 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
70099
70403
  reloadPreview();
70100
70404
  return "full";
70101
70405
  }
70102
- for (const target of plan.targets) syncElementAttributes(target.live, target.restored);
70406
+ for (const target of plan.targets)
70407
+ syncElementAttributes(target.live, target.restored, target.previous);
70103
70408
  const active = files[activeDocPath];
70104
70409
  const restoredScript = active ? extractGsapScriptText(active.restored) : null;
70105
70410
  const previousScript = active ? extractGsapScriptText(active.previous) : null;
70106
- if (restoredScript && restoredScript !== previousScript) {
70107
- syncStaleEditMarks(doc, active.restored);
70411
+ const reparse = plan.scripted ? plan.targets.map(({ live }) => live).filter((el) => "_gsap" in el) : [];
70412
+ if (reparse.length && !restoredScript) {
70413
+ reloadPreview();
70414
+ return "full";
70415
+ }
70416
+ if (restoredScript && (restoredScript !== previousScript || reparse.length)) {
70417
+ syncStaleEditMarks(doc, active);
70108
70418
  const result = applySoftReload(iframe, restoredScript, {
70109
70419
  onAsyncFailure: reloadPreview,
70110
70420
  currentTimeOverride: currentTime,
70111
- authoredHtml: active.restored
70421
+ authoredHtml: active.restored,
70422
+ reparse
70112
70423
  });
70113
70424
  if (result === "cannot-soft-reload") {
70114
70425
  reloadPreview();
@@ -70122,11 +70433,11 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
70122
70433
  }
70123
70434
 
70124
70435
  // src/player/lib/automationStoreSync.ts
70125
- import { HF_AUDIO_AUTOMATION_ATTR as HF_AUDIO_AUTOMATION_ATTR4 } from "@hyperframes/core/audio-automation";
70126
- import { HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR7 } from "@hyperframes/core/audio-fx";
70436
+ import { HF_AUDIO_AUTOMATION_ATTR as HF_AUDIO_AUTOMATION_ATTR5 } from "@hyperframes/core/audio-automation";
70437
+ import { HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR8 } from "@hyperframes/core/audio-fx";
70127
70438
  import {
70128
- HF_AUDIO_FADE_IN_ATTR as HF_AUDIO_FADE_IN_ATTR2,
70129
- HF_AUDIO_FADE_OUT_ATTR as HF_AUDIO_FADE_OUT_ATTR2,
70439
+ HF_AUDIO_FADE_IN_ATTR as HF_AUDIO_FADE_IN_ATTR3,
70440
+ HF_AUDIO_FADE_OUT_ATTR as HF_AUDIO_FADE_OUT_ATTR3,
70130
70441
  readElementFades,
70131
70442
  readFadeSeconds as readFadeSeconds2
70132
70443
  } from "@hyperframes/core/audio-fade";
@@ -70134,8 +70445,8 @@ function syncedFields(doc, element, node) {
70134
70445
  const group = element.audioGroup ? groupInfoFor(doc, element.audioGroup) : null;
70135
70446
  const fades = readElementFades(node);
70136
70447
  return {
70137
- automation: node.getAttribute(HF_AUDIO_AUTOMATION_ATTR4) ?? void 0,
70138
- fxChain: node.getAttribute(HF_AUDIO_FX_ATTR7) ?? void 0,
70448
+ automation: node.getAttribute(HF_AUDIO_AUTOMATION_ATTR5) ?? void 0,
70449
+ fxChain: node.getAttribute(HF_AUDIO_FX_ATTR8) ?? void 0,
70139
70450
  fadeIn: storedFade(fades.fadeIn),
70140
70451
  fadeOut: storedFade(fades.fadeOut),
70141
70452
  audioGroupAutomation: group?.automation,
@@ -70161,10 +70472,10 @@ function syncStoredAutomationFromPreview(doc) {
70161
70472
  }
70162
70473
  var storedFade = (seconds) => seconds > 0 ? seconds : void 0;
70163
70474
  var STORED_FIELD = {
70164
- [HF_AUDIO_AUTOMATION_ATTR4]: (value) => ({ automation: value ?? void 0 }),
70165
- [HF_AUDIO_FX_ATTR7]: (value) => ({ fxChain: value ?? void 0 }),
70166
- [HF_AUDIO_FADE_IN_ATTR2]: (value) => ({ fadeIn: storedFade(readFadeSeconds2(value)) }),
70167
- [HF_AUDIO_FADE_OUT_ATTR2]: (value) => ({ fadeOut: storedFade(readFadeSeconds2(value)) })
70475
+ [HF_AUDIO_AUTOMATION_ATTR5]: (value) => ({ automation: value ?? void 0 }),
70476
+ [HF_AUDIO_FX_ATTR8]: (value) => ({ fxChain: value ?? void 0 }),
70477
+ [HF_AUDIO_FADE_IN_ATTR3]: (value) => ({ fadeIn: storedFade(readFadeSeconds2(value)) }),
70478
+ [HF_AUDIO_FADE_OUT_ATTR3]: (value) => ({ fadeOut: storedFade(readFadeSeconds2(value)) })
70168
70479
  };
70169
70480
  function syncStoredElementAttribute(target, attr, value) {
70170
70481
  const fields = STORED_FIELD[attr]?.(value);
@@ -70272,6 +70583,10 @@ function usePreviewPersistence({
70272
70583
  const result = await drainPendingDomEditSaves();
70273
70584
  if (result.status !== "clean") throw result.error;
70274
70585
  }, [drainPendingDomEditSaves]);
70586
+ const settlePendingEdits = useCallback104(async () => {
70587
+ await flushStudioPendingEdits();
70588
+ await domEditSaveQueueRef.current?.waitForIdle();
70589
+ }, []);
70275
70590
  const resetDomEditSaveQueueBreaker = useCallback104(() => {
70276
70591
  domEditSaveQueueRef.current?.reset();
70277
70592
  setDomEditSaveQueuePaused(null);
@@ -70335,6 +70650,7 @@ function usePreviewPersistence({
70335
70650
  queueDomEditSave,
70336
70651
  drainPendingDomEditSaves,
70337
70652
  waitForPendingDomEditSaves,
70653
+ settlePendingEdits,
70338
70654
  domEditSaveQueuePaused,
70339
70655
  resetDomEditSaveQueueBreaker,
70340
70656
  applyCurrentStudioManualEditsToPreview,
@@ -71024,8 +71340,8 @@ function useTimelineEditGate(canEdit, showToast) {
71024
71340
  }
71025
71341
 
71026
71342
  // src/hooks/timelineAudioGroupVolume.ts
71027
- import { HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR8 } from "@hyperframes/core/audio-fx";
71028
- import { HF_AUDIO_AUTOMATION_ATTR as HF_AUDIO_AUTOMATION_ATTR5 } from "@hyperframes/core/audio-automation";
71343
+ import { HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR9 } from "@hyperframes/core/audio-fx";
71344
+ import { HF_AUDIO_AUTOMATION_ATTR as HF_AUDIO_AUTOMATION_ATTR6 } from "@hyperframes/core/audio-automation";
71029
71345
 
71030
71346
  // src/hooks/liveLanes.ts
71031
71347
  import { useRef as useRef132, useState as useState109 } from "react";
@@ -71164,8 +71480,8 @@ var GROUP_ATTR_TO_MIRROR = {
71164
71480
  "data-hidden": (value) => ({ audioGroupHidden: value !== null }),
71165
71481
  "data-volume": (value) => ({ audioGroupVolume: mirroredGroupVolume(value) }),
71166
71482
  "data-label": (value, groupId) => ({ audioGroupLabel: value ?? groupId }),
71167
- [HF_AUDIO_FX_ATTR8]: (value) => ({ audioGroupFxChain: value ?? void 0 }),
71168
- [HF_AUDIO_AUTOMATION_ATTR5]: (value) => ({ audioGroupAutomation: value ?? void 0 })
71483
+ [HF_AUDIO_FX_ATTR9]: (value) => ({ audioGroupFxChain: value ?? void 0 }),
71484
+ [HF_AUDIO_AUTOMATION_ATTR6]: (value) => ({ audioGroupAutomation: value ?? void 0 })
71169
71485
  };
71170
71486
  function syncStoredGroupAttribute(groupId, attr, value) {
71171
71487
  const toPatch = GROUP_ATTR_TO_MIRROR[attr];
@@ -71653,11 +71969,7 @@ function useTrackPendingTimelineEdit() {
71653
71969
  const key2 = fn;
71654
71970
  const cached5 = wrappedRef.current.get(key2);
71655
71971
  if (cached5) return cached5;
71656
- const wrapped = (...args) => {
71657
- const result = fn(...args);
71658
- trackStudioPendingEdit(result);
71659
- return result;
71660
- };
71972
+ const wrapped = trackedStudioEdit(fn);
71661
71973
  wrappedRef.current.set(key2, wrapped);
71662
71974
  return wrapped;
71663
71975
  }, []);
@@ -71665,7 +71977,7 @@ function useTrackPendingTimelineEdit() {
71665
71977
 
71666
71978
  // src/hooks/timelineAudioGroupCreate.ts
71667
71979
  import { useCallback as useCallback114 } from "react";
71668
- import { HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR4, HF_AUDIO_GROUP_TAG as HF_AUDIO_GROUP_TAG5 } from "@hyperframes/core/audio-groups";
71980
+ import { HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR5, HF_AUDIO_GROUP_TAG as HF_AUDIO_GROUP_TAG5 } from "@hyperframes/core/audio-groups";
71669
71981
 
71670
71982
  // src/hooks/timelineTrackVisibility.ts
71671
71983
  import { useCallback as useCallback113 } from "react";
@@ -71929,8 +72241,8 @@ function patchLiveAudioGroupState(iframe, elements, groupId, activeCompPath, res
71929
72241
  const target = findTimelineElementInIframe(iframe, element, activeCompPath);
71930
72242
  if (!target) continue;
71931
72243
  const next = restore ? restore.get(element) ?? null : groupId;
71932
- if (next) target.setAttribute(HF_AUDIO_GROUP_ATTR4, next);
71933
- else target.removeAttribute(HF_AUDIO_GROUP_ATTR4);
72244
+ if (next) target.setAttribute(HF_AUDIO_GROUP_ATTR5, next);
72245
+ else target.removeAttribute(HF_AUDIO_GROUP_ATTR5);
71934
72246
  }
71935
72247
  invalidateGroupInfoCache(iframe?.contentDocument);
71936
72248
  }
@@ -71938,7 +72250,7 @@ function captureAudioGroupState(iframe, elements, activeCompPath) {
71938
72250
  const prior = /* @__PURE__ */ new Map();
71939
72251
  for (const element of elements) {
71940
72252
  const target = findTimelineElementInIframe(iframe, element, activeCompPath);
71941
- prior.set(element, target?.getAttribute(HF_AUDIO_GROUP_ATTR4) ?? null);
72253
+ prior.set(element, target?.getAttribute(HF_AUDIO_GROUP_ATTR5) ?? null);
71942
72254
  }
71943
72255
  return prior;
71944
72256
  }
@@ -71994,7 +72306,7 @@ async function createAudioGroupAndAssignMembers({
71994
72306
  reseekPreviewRuntime(previewIframe);
71995
72307
  const groupOperation = {
71996
72308
  type: "attribute",
71997
- property: HF_AUDIO_GROUP_ATTR4,
72309
+ property: HF_AUDIO_GROUP_ATTR5,
71998
72310
  value: groupId
71999
72311
  };
72000
72312
  const groupPath = activeCompPath || "index.html";
@@ -73863,30 +74175,8 @@ function injectPreviewImportedFont(doc, asset) {
73863
74175
  style.textContent = importedFontFaceCss(asset);
73864
74176
  doc.head.appendChild(style);
73865
74177
  }
73866
- function ensureImportedFontFace(html2, asset, sourceFile) {
73867
- const css2 = importedFontFaceCss(asset, toRelativeProjectAssetPath(sourceFile, asset.path));
73868
- if (html2.includes(css2)) return html2;
73869
- const styleRe = /<style\b[^>]*data-hf-studio-fonts=(["'])true\1[^>]*>([\s\S]*?)<\/style>/i;
73870
- const styleMatch = styleRe.exec(html2);
73871
- if (styleMatch) {
73872
- const nextCss = `${styleMatch[2].trim()}
73873
- ${css2}`.trim();
73874
- return html2.replace(
73875
- styleMatch[0],
73876
- () => `<style data-hf-studio-fonts="true">
73877
- ${nextCss}
73878
- </style>`
73879
- );
73880
- }
73881
- const styleTag = `<style data-hf-studio-fonts="true">
73882
- ${css2}
73883
- </style>`;
73884
- if (/<\/head>/i.test(html2)) {
73885
- return html2.replace(/<\/head>/i, () => ` ${styleTag}
73886
- </head>`);
73887
- }
73888
- return `${styleTag}
73889
- ${html2}`;
74178
+ function importedFontFaceCssFor(asset, sourceFile) {
74179
+ return importedFontFaceCss(asset, toRelativeProjectAssetPath(sourceFile, asset.path));
73890
74180
  }
73891
74181
 
73892
74182
  // src/hooks/domEditPersistFailure.ts
@@ -73904,6 +74194,13 @@ var DomEditPersistUnsafeValueError = class extends Error {
73904
74194
  this.alreadyToasted = options.alreadyToasted ?? false;
73905
74195
  }
73906
74196
  };
74197
+ var DomEditPersistPreparedWriteError = class extends Error {
74198
+ alreadyToasted = true;
74199
+ constructor(targetPath) {
74200
+ super(`The edit saved but its follow-up write to ${targetPath} did not`);
74201
+ this.name = "DomEditPersistPreparedWriteError";
74202
+ }
74203
+ };
73907
74204
  function summarizeOperations(operations) {
73908
74205
  return operations.map((op) => `${op.type}:${op.property}`).join(", ");
73909
74206
  }
@@ -73923,7 +74220,7 @@ function getSelectionLabel(selection) {
73923
74220
  return selection.label || selection.selector || selection.id || "this element";
73924
74221
  }
73925
74222
  function wasAlreadyToasted(error) {
73926
- return (error instanceof DomEditPersistUnsafeValueError || error instanceof StudioSaveHttpError) && error.alreadyToasted;
74223
+ return (error instanceof DomEditPersistUnsafeValueError || error instanceof DomEditPersistPreparedWriteError || error instanceof StudioSaveHttpError) && error.alreadyToasted;
73927
74224
  }
73928
74225
  function reportDomEditPersistFailure(selection, operations, error, showToast) {
73929
74226
  const detail = getErrorDetail(error);
@@ -74154,16 +74451,16 @@ async function postPatchElement(projectId, targetPath, body, showToast) {
74154
74451
  }
74155
74452
  async function writePreparedContent(targetPath, patchedContent, prepare, writeProjectFile, showToast) {
74156
74453
  const preparedContent = prepare(patchedContent, targetPath);
74157
- if (preparedContent === patchedContent) return patchedContent;
74454
+ if (preparedContent === patchedContent) return { content: patchedContent, failed: false };
74158
74455
  try {
74159
74456
  await writeProjectFile(targetPath, preparedContent, patchedContent);
74160
- return preparedContent;
74457
+ return { content: preparedContent, failed: false };
74161
74458
  } catch (error) {
74162
74459
  showToast(
74163
74460
  `Saved, but couldn't finish updating ${targetPath}: ${getErrorDetail2(error)}`,
74164
74461
  "error"
74165
74462
  );
74166
- return patchedContent;
74463
+ return { content: patchedContent, failed: true };
74167
74464
  }
74168
74465
  }
74169
74466
 
@@ -74191,7 +74488,8 @@ function useDomEditPersist({
74191
74488
  if (options?.shouldSave && !options.shouldSave()) return;
74192
74489
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
74193
74490
  const completePersistence = (result, changed) => {
74194
- if (options?.skipRefresh && changed) reseekPreviewRuntime(previewIframeRef.current);
74491
+ if (options?.skipRefresh && changed && !options.deferRender)
74492
+ reseekPreviewRuntime(previewIframeRef.current);
74195
74493
  return result;
74196
74494
  };
74197
74495
  const readTarget = async () => {
@@ -74202,7 +74500,12 @@ function useDomEditPersist({
74202
74500
  return options?.shouldSave && !options.shouldSave() ? null : content;
74203
74501
  };
74204
74502
  const patchTarget = buildDomEditPatchTarget(selection);
74205
- const patchBody = { target: patchTarget, operations };
74503
+ const font = options?.importedFont;
74504
+ const patchBody = {
74505
+ target: patchTarget,
74506
+ operations,
74507
+ ...font ? { fontFaceCss: importedFontFaceCssFor(font, targetPath) } : {}
74508
+ };
74206
74509
  const unsafeFields = findUnsafeDomPatchValues2(patchBody);
74207
74510
  if (unsafeFields.length > 0) {
74208
74511
  const fields = formatUnsafeFieldList(unsafeFields);
@@ -74211,7 +74514,7 @@ function useDomEditPersist({
74211
74514
  alreadyToasted: true
74212
74515
  });
74213
74516
  }
74214
- if (onTrySdkPersist && !options?.prepareContent) {
74517
+ if (onTrySdkPersist && !font && !options?.prepareContent) {
74215
74518
  const originalContent = await readTarget();
74216
74519
  if (originalContent === null) return;
74217
74520
  const cutover = await onTrySdkPersist(selection, operations, originalContent, targetPath, {
@@ -74233,19 +74536,22 @@ function useDomEditPersist({
74233
74536
  coalesceMs: options?.coalesceMs
74234
74537
  };
74235
74538
  const prepare = options?.prepareContent;
74539
+ let preparedWriteFailed = false;
74236
74540
  const saved2 = await serializeStudioFileMutations(writeProjectFile, [targetPath], async () => {
74237
74541
  const originalContent = await readTarget();
74238
74542
  if (originalContent === null) return null;
74239
74543
  const patchData2 = await postPatchElement(pid, targetPath, patchBody, showToast);
74240
74544
  if (!patchData2.changed) return { patchData: patchData2, patchedContent: null, finalContent: null };
74241
74545
  const patchedContent2 = typeof patchData2.content === "string" ? patchData2.content : originalContent;
74242
- const finalContent2 = prepare ? await writePreparedContent(
74546
+ const prepared = prepare ? await writePreparedContent(
74243
74547
  targetPath,
74244
74548
  patchedContent2,
74245
74549
  prepare,
74246
74550
  writeProjectFile,
74247
74551
  showToast
74248
- ) : patchedContent2;
74552
+ ) : { content: patchedContent2, failed: false };
74553
+ preparedWriteFailed = prepared.failed;
74554
+ const finalContent2 = prepared.content;
74249
74555
  await editHistory.recordEdit({
74250
74556
  ...history2,
74251
74557
  files: { [targetPath]: { before: originalContent, after: finalContent2 } }
@@ -74278,10 +74584,12 @@ function useDomEditPersist({
74278
74584
  if (!options?.skipRefresh) {
74279
74585
  reloadPreview();
74280
74586
  }
74281
- return completePersistence(
74587
+ const outcome = completePersistence(
74282
74588
  finalContent === patchedContent && typeof patchData.path === "string" && typeof patchData.version === "string" ? { sourceFile: patchData.path, version: patchData.version, changed: true } : void 0,
74283
74589
  true
74284
74590
  );
74591
+ if (preparedWriteFailed) throw new DomEditPersistPreparedWriteError(targetPath);
74592
+ return outcome;
74285
74593
  },
74286
74594
  [
74287
74595
  activeCompPath,
@@ -74299,9 +74607,10 @@ function useDomEditPersist({
74299
74607
  (selection, operations, options) => {
74300
74608
  const expectedProjectId = projectIdRef.current;
74301
74609
  if (!expectedProjectId) return Promise.reject(new Error("No active project"));
74302
- return queueDomEditSave(
74610
+ const save = () => queueDomEditSave(
74303
74611
  () => performPersistDomEditOperations(selection, operations, options, expectedProjectId)
74304
74612
  );
74613
+ return selection.element ? countStudioManualEditSave(selection.element, save) : save();
74305
74614
  },
74306
74615
  [performPersistDomEditOperations, projectIdRef, queueDomEditSave]
74307
74616
  );
@@ -74321,7 +74630,8 @@ function useDomEditPositionPatchCommit({
74321
74630
  label: options.label,
74322
74631
  coalesceKey: options.coalesceKey,
74323
74632
  coalesceMs: options.coalesceMs,
74324
- skipRefresh: options.skipRefresh ?? true
74633
+ skipRefresh: options.skipRefresh ?? true,
74634
+ deferRender: options.deferRender
74325
74635
  }).then(() => void 0).catch((error) => {
74326
74636
  if (error instanceof DomEditSaveQueueOpenError) throw error;
74327
74637
  if (!wasAlreadyToasted(error)) {
@@ -74457,7 +74767,7 @@ async function commitDomStyles(context, selection, styles) {
74457
74767
  label: "Edit layer style",
74458
74768
  // Inline styles are already live, so a reload would only blank the preview.
74459
74769
  skipRefresh: true,
74460
- prepareContent: importedFont ? (html2, sourceFile) => ensureImportedFontFace(html2, importedFont, sourceFile) : void 0
74770
+ importedFont: importedFont ?? void 0
74461
74771
  }),
74462
74772
  shouldRevert: ownsAny,
74463
74773
  revert: () => {
@@ -74479,7 +74789,7 @@ async function commitDomStyles(context, selection, styles) {
74479
74789
 
74480
74790
  // src/hooks/useDomEditAttributeCommits.ts
74481
74791
  import { useCallback as useCallback123, useRef as useRef141 } from "react";
74482
- import { HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR5, HF_AUDIO_GROUP_TAG as HF_AUDIO_GROUP_TAG7 } from "@hyperframes/core/audio-groups";
74792
+ import { HF_AUDIO_GROUP_ATTR as HF_AUDIO_GROUP_ATTR6, HF_AUDIO_GROUP_TAG as HF_AUDIO_GROUP_TAG7 } from "@hyperframes/core/audio-groups";
74483
74793
  function resolveFullAttrName(attr, prefixData) {
74484
74794
  return prefixData && !attr.startsWith("data-") ? `data-${attr}` : attr;
74485
74795
  }
@@ -74487,13 +74797,20 @@ function resolveOptimisticAttributeValue(attr, value) {
74487
74797
  if (value === null) return null;
74488
74798
  return value === "false" && HTML_BOOLEAN_ATTRIBUTES.has(attr) ? null : value;
74489
74799
  }
74800
+ function previewAttributeName(op) {
74801
+ return op.type === "attribute" ? resolveFullAttrName(op.property, true) : op.property;
74802
+ }
74803
+ function previewAttributeValue(op) {
74804
+ if (op.type === "html-attribute") return resolveOptimisticAttributeValue(op.property, op.value);
74805
+ return op.value === "" ? null : op.value;
74806
+ }
74490
74807
  function setOrRemovePreviewAttribute(el, fullAttr, value) {
74491
74808
  if (value === null) {
74492
74809
  el.removeAttribute(fullAttr);
74493
74810
  } else {
74494
74811
  el.setAttribute(fullAttr, value);
74495
74812
  }
74496
- if (el.tagName.toLowerCase() === HF_AUDIO_GROUP_TAG7 || fullAttr === HF_AUDIO_GROUP_ATTR5) {
74813
+ if (el.tagName.toLowerCase() === HF_AUDIO_GROUP_TAG7 || fullAttr === HF_AUDIO_GROUP_ATTR6) {
74497
74814
  invalidateGroupInfoCache(el.ownerDocument);
74498
74815
  }
74499
74816
  }
@@ -74583,25 +74900,20 @@ function useDomEditAttributeCommits({
74583
74900
  previewIframeRef
74584
74901
  ]
74585
74902
  );
74586
- const commitDataAttributes = useCallback123(
74587
- async (selection, attrs, options) => {
74903
+ const commitAttributeOps = useCallback123(
74904
+ async (selection, ops, options) => {
74588
74905
  const iframe = previewIframeRef.current;
74589
- const entries = Object.entries(attrs).map(([attr, value]) => ({
74590
- attr,
74591
- fullAttr: resolveFullAttrName(attr, true),
74592
- value
74906
+ const entries = ops.map((op) => ({
74907
+ fullAttr: previewAttributeName(op),
74908
+ value: previewAttributeValue(op)
74593
74909
  }));
74594
- const commitKey = `${options.coalescePrefix}:${entries.map((entry2) => entry2.attr).sort().join(",")}:${getDomEditTargetKey(selection)}`;
74910
+ const commitKey = `${options.coalescePrefix}:${ops.map((op) => op.property).sort().join(",")}:${getDomEditTargetKey(selection)}`;
74595
74911
  const isLatestCommit = bumpDomEditCommitMapVersion(
74596
74912
  domAttributeCommitVersionRef.current,
74597
74913
  commitKey
74598
74914
  );
74599
- const ops = entries.map((entry2) => ({
74600
- type: "attribute",
74601
- property: entry2.attr,
74602
- value: entry2.value
74603
- }));
74604
74915
  let captured = null;
74916
+ let landed = false;
74605
74917
  await runDomEditCommit({
74606
74918
  capture: () => {
74607
74919
  captured = captureMultiAttributeElement(
@@ -74614,16 +74926,16 @@ function useDomEditAttributeCommits({
74614
74926
  apply: () => {
74615
74927
  if (!captured) return;
74616
74928
  for (const entry2 of entries) {
74617
- const nextValue = entry2.value === null || entry2.value === "" ? null : entry2.value;
74618
- setOrRemovePreviewAttribute(captured.element, entry2.fullAttr, nextValue);
74929
+ setOrRemovePreviewAttribute(captured.element, entry2.fullAttr, entry2.value);
74619
74930
  }
74620
74931
  },
74621
74932
  persist: () => persistDomEditOperations(selection, ops, {
74622
74933
  label: options.label,
74623
74934
  coalesceKey: commitKey,
74624
- skipRefresh: options.skipRefresh
74935
+ skipRefresh: options.skipRefresh,
74936
+ prepareContent: options.prepareContent
74625
74937
  }),
74626
- shouldRevert: () => isLatestCommit(),
74938
+ shouldRevert: (error) => isLatestCommit() && !(error instanceof DomEditPersistPreparedWriteError),
74627
74939
  revert: () => {
74628
74940
  if (!captured) return;
74629
74941
  for (const entry2 of entries) {
@@ -74636,10 +74948,19 @@ function useDomEditAttributeCommits({
74636
74948
  },
74637
74949
  onError: (error) => reportDomEditPersistFailure(selection, ops, error, showToast),
74638
74950
  shouldResync: () => isLatestCommit() && !!options.refreshAfter,
74639
- resync: () => refreshDomEditSelectionFromPreview(selection),
74640
- onSettled: options.onSettled,
74951
+ resync: () => {
74952
+ refreshDomEditSelectionFromPreview(selection);
74953
+ if (options.syncAutomation) {
74954
+ syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null);
74955
+ }
74956
+ },
74957
+ onSettled: (ok) => {
74958
+ landed = ok;
74959
+ options.onSettled?.(ok);
74960
+ },
74641
74961
  onFinally: isLatestCommit.release
74642
74962
  });
74963
+ return landed;
74643
74964
  },
74644
74965
  [
74645
74966
  activeCompPath,
@@ -74651,14 +74972,33 @@ function useDomEditAttributeCommits({
74651
74972
  );
74652
74973
  const handleDomAttributesCommit = useCallback123(
74653
74974
  async (selection, attrs) => {
74654
- await commitDataAttributes(selection, attrs, {
74655
- label: "Edit timing",
74656
- coalescePrefix: "attrs",
74657
- skipRefresh: false,
74658
- refreshAfter: true
74659
- });
74975
+ await commitAttributeOps(
74976
+ selection,
74977
+ Object.entries(attrs).map(([property, value]) => ({
74978
+ type: "attribute",
74979
+ property,
74980
+ value
74981
+ })),
74982
+ {
74983
+ label: "Edit timing",
74984
+ coalescePrefix: "attrs",
74985
+ skipRefresh: false,
74986
+ refreshAfter: true
74987
+ }
74988
+ );
74660
74989
  },
74661
- [commitDataAttributes]
74990
+ [commitAttributeOps]
74991
+ );
74992
+ const handleDomAttributeBatchCommit = useCallback123(
74993
+ (selection, ops, options) => commitAttributeOps(selection, ops, {
74994
+ label: options.label,
74995
+ coalescePrefix: "attr-batch",
74996
+ skipRefresh: false,
74997
+ refreshAfter: true,
74998
+ syncAutomation: true,
74999
+ prepareContent: options.prepareContent
75000
+ }),
75001
+ [commitAttributeOps]
74662
75002
  );
74663
75003
  const handleDomAttributeCommit = useCallback123(
74664
75004
  async (attr, value) => {
@@ -74759,7 +75099,8 @@ function useDomEditAttributeCommits({
74759
75099
  handleDomAttributeLiveCommit,
74760
75100
  handleDomAttributeQuietCommit,
74761
75101
  handleDomHtmlAttributeCommit,
74762
- handleDomAttributesCommit
75102
+ handleDomAttributesCommit,
75103
+ handleDomAttributeBatchCommit
74763
75104
  };
74764
75105
  }
74765
75106
 
@@ -74799,7 +75140,8 @@ function useDomEditTextCommits({
74799
75140
  handleDomAttributeLiveCommit,
74800
75141
  handleDomAttributeQuietCommit,
74801
75142
  handleDomHtmlAttributeCommit,
74802
- handleDomAttributesCommit
75143
+ handleDomAttributesCommit,
75144
+ handleDomAttributeBatchCommit
74803
75145
  } = useDomEditAttributeCommits({
74804
75146
  activeCompPath,
74805
75147
  previewIframeRef,
@@ -75010,7 +75352,7 @@ function useDomEditTextCommits({
75010
75352
  await persistDomEditOperations(selection, textCommit.operations, {
75011
75353
  label: "Edit text",
75012
75354
  skipRefresh: true,
75013
- prepareContent: importedFont ? (html2, sourceFile) => ensureImportedFontFace(html2, importedFont, sourceFile) : void 0
75355
+ importedFont: importedFont ?? void 0
75014
75356
  });
75015
75357
  },
75016
75358
  shouldRevert: () => isLatestTextCommit(),
@@ -75122,6 +75464,7 @@ function useDomEditTextCommits({
75122
75464
  handleDomAttributeQuietCommit,
75123
75465
  handleDomHtmlAttributeCommit,
75124
75466
  handleDomAttributesCommit,
75467
+ handleDomAttributeBatchCommit,
75125
75468
  handleDomTextCommit,
75126
75469
  handleDomTextCommitForSelection,
75127
75470
  handleDomRichTextCommit,
@@ -75283,7 +75626,7 @@ function useDomGeometryCommits({
75283
75626
  [commitPositionPatchToHtml, readOnlyPreview, showToast]
75284
75627
  );
75285
75628
  const handleDomBoxSizeCommit = useCallback125(
75286
- (selection, next, offset, restore) => {
75629
+ (selection, next, offset, restore, undoKey) => {
75287
75630
  if (readOnlyPreview) return Promise.resolve();
75288
75631
  const element = selection.element;
75289
75632
  const beforeSize = captureStudioBoxSize(element);
@@ -75296,7 +75639,7 @@ function useDomGeometryCommits({
75296
75639
  if (offset) patches.push(...writePlainMove(element, offset));
75297
75640
  return commitPositionPatchToHtml(selection, patches, {
75298
75641
  label: "Resize layer box",
75299
- coalesceKey: `box-size:${getDomEditTargetKey(selection)}`
75642
+ ...undoKey ? { coalesceKey: undoKey, coalesceMs: Number.POSITIVE_INFINITY, deferRender: true } : { coalesceKey: `box-size:${getDomEditTargetKey(selection)}` }
75300
75643
  }).catch((error) => {
75301
75644
  restoreStudioBoxSize(element, beforeSize);
75302
75645
  if (offset) restoreStudioPathOffset(element, beforeOffset);
@@ -75492,6 +75835,7 @@ function useDomEditCommits({
75492
75835
  handleDomAttributeQuietCommit,
75493
75836
  handleDomHtmlAttributeCommit,
75494
75837
  handleDomAttributesCommit,
75838
+ handleDomAttributeBatchCommit,
75495
75839
  handleDomTextCommit,
75496
75840
  handleDomTextCommitForSelection,
75497
75841
  handleDomRichTextCommit,
@@ -75548,6 +75892,7 @@ function useDomEditCommits({
75548
75892
  handleDomAttributeQuietCommit,
75549
75893
  handleDomHtmlAttributeCommit,
75550
75894
  handleDomAttributesCommit,
75895
+ handleDomAttributeBatchCommit,
75551
75896
  handleDomTextCommit,
75552
75897
  handleDomTextCommitForSelection,
75553
75898
  handleDomRichTextCommit,
@@ -77037,7 +77382,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
77037
77382
  }
77038
77383
  return run();
77039
77384
  };
77040
- const commit = (selection, mutation, options) => {
77385
+ const commit = trackedStudioEdit((selection, mutation, options) => {
77041
77386
  if (!activeProjectId) return Promise.resolve();
77042
77387
  const file = selection.sourceFile || activeCompPath || "index.html";
77043
77388
  return serializeCommit(
@@ -77045,8 +77390,8 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
77045
77390
  options.serializeKey,
77046
77391
  () => runCommit(activeProjectId, activeCompPath, file, selection, mutation, options)
77047
77392
  );
77048
- };
77049
- commit.batch = (calls, options) => {
77393
+ });
77394
+ commit.batch = trackedStudioEdit((calls, options) => {
77050
77395
  if (!activeProjectId) return Promise.resolve();
77051
77396
  const file = calls[0]?.selection.sourceFile || activeCompPath || "index.html";
77052
77397
  return serializeCommit(
@@ -77054,7 +77399,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
77054
77399
  options.serializeKey,
77055
77400
  () => runBatchCommit(activeProjectId, activeCompPath, file, calls, options)
77056
77401
  );
77057
- };
77402
+ });
77058
77403
  return commit;
77059
77404
  }, [runCommit, runBatchCommit, activeCompPath, activeProjectId, writeProjectFile]);
77060
77405
  const trackGsapSaveFailure = useGsapSaveFailureTelemetry(activeCompPath);
@@ -77803,6 +78148,7 @@ function preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnim
77803
78148
 
77804
78149
  // src/hooks/gsapResizeIntercept.ts
77805
78150
  var IDENTITY_ONE_PROPS = /* @__PURE__ */ new Set(["opacity", "autoAlpha", "scale", "scaleX", "scaleY"]);
78151
+ var SIZE_PROPS = /* @__PURE__ */ new Set(["width", "height"]);
77806
78152
  function synthesizeIdentityProps(source) {
77807
78153
  const id = {};
77808
78154
  for (const [k, v] of Object.entries(source)) {
@@ -77830,7 +78176,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
77830
78176
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
77831
78177
  const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
77832
78178
  if (outcome.status === "blocked") return outcome;
77833
- const { resizeGroup, resizeProperties, workingAnimations } = resizeRoute(
78179
+ const { allKnownAnimations, resizeGroup, resizeProperties, workingAnimations } = resizeRoute(
77834
78180
  animations,
77835
78181
  fetchedAnimations
77836
78182
  );
@@ -77851,6 +78197,10 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
77851
78197
  size
77852
78198
  });
77853
78199
  if (!anim || isInstantHold(anim)) {
78200
+ const scriptWritesSize = allKnownAnimations.some(
78201
+ (a) => animationWritesAnyProperty(a, SIZE_PROPS)
78202
+ );
78203
+ if (!scriptWritesSize) return { status: "element-size" };
77854
78204
  const sel = selectorFromSelection(selection) ?? writeTargetSelector(selection);
77855
78205
  if (!sel) return { status: "blocked", reason: "no-selector" };
77856
78206
  const sizeSet = resizeGroup === "size" ? anim ?? findSizeSetAnimation(workingAnimations, sel, selection.element) : findSizeSetAnimation(workingAnimations, sel, selection.element);
@@ -78080,7 +78430,6 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
78080
78430
  );
78081
78431
  return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
78082
78432
  }
78083
- const SIZE_PROPS = /* @__PURE__ */ new Set(["width", "height"]);
78084
78433
  const backfillDefaults = {};
78085
78434
  for (const k of Object.keys(runtimeProps)) {
78086
78435
  if (SIZE_PROPS.has(k)) continue;
@@ -78692,7 +79041,9 @@ function useGsapAwareEditing({
78692
79041
  makeFetchFallback(selection)
78693
79042
  );
78694
79043
  assertGsapEditPersisted(outcome);
78695
- cropUndoKey = coalesceKey;
79044
+ if (outcome.status === "element-size")
79045
+ await handleDomBoxSizeCommit(selection, next, void 0, void 0, coalesceKey);
79046
+ else cropUndoKey = coalesceKey;
78696
79047
  const ownsDragOffset = outcome.status === "persisted" && outcome.ownsDragOffset === true;
78697
79048
  logResize("intercept-handled", {
78698
79049
  scaleRoute,
@@ -79306,6 +79657,7 @@ function useDomEditSession({
79306
79657
  handleDomAttributeQuietCommit,
79307
79658
  handleDomHtmlAttributeCommit,
79308
79659
  handleDomAttributesCommit,
79660
+ handleDomAttributeBatchCommit,
79309
79661
  handleDomTextCommit,
79310
79662
  handleDomTextCommitForSelection,
79311
79663
  handleDomRichTextCommit,
@@ -79570,6 +79922,7 @@ function useDomEditSession({
79570
79922
  handleDomAttributeQuietCommit,
79571
79923
  handleDomHtmlAttributeCommit,
79572
79924
  handleDomAttributesCommit,
79925
+ handleDomAttributeBatchCommit,
79573
79926
  handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
79574
79927
  handleDomGroupPathOffsetCommit: handleGsapAwareGroupPathOffsetCommit,
79575
79928
  handleDomZIndexReorderCommit,
@@ -81103,7 +81456,7 @@ function useAppHotkeys({
81103
81456
  showToast,
81104
81457
  syncHistoryPreviewAfterApply,
81105
81458
  showHistoryRestoreNow,
81106
- waitForPendingDomEditSaves,
81459
+ settlePendingEdits,
81107
81460
  handleCopy: handleCopy2,
81108
81461
  handlePaste: handlePaste2,
81109
81462
  handleCut,
@@ -81127,7 +81480,7 @@ function useAppHotkeys({
81127
81480
  showToast,
81128
81481
  syncHistoryPreviewAfterApply,
81129
81482
  showHistoryRestoreNow,
81130
- waitForPendingDomEditSaves,
81483
+ waitForPendingDomEditSaves: settlePendingEdits,
81131
81484
  onAfterUndoRedo,
81132
81485
  activeCompPath,
81133
81486
  forceReloadSdkSession
@@ -89472,6 +89825,10 @@ function makeSlideshowNotesController() {
89472
89825
  }
89473
89826
  };
89474
89827
  }
89828
+ function isPanelUndoKey(e) {
89829
+ if (!(e.metaKey || e.ctrlKey) || e.shiftKey || e.key.toLowerCase() !== "z") return false;
89830
+ return !isTypingTarget(e.target);
89831
+ }
89475
89832
  function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
89476
89833
  const { editingFile } = useFileManagerContext();
89477
89834
  const compHtml = editingFile?.content ?? null;
@@ -89677,13 +90034,10 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
89677
90034
  {
89678
90035
  className: "flex flex-col h-full overflow-y-auto text-text-0",
89679
90036
  onKeyDown: (e) => {
89680
- if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z") {
89681
- const target = e.target instanceof HTMLElement ? e.target.tagName : "";
89682
- if (target === "TEXTAREA" || target === "INPUT") return;
89683
- e.preventDefault();
89684
- e.stopPropagation();
89685
- handleUndo();
89686
- }
90037
+ if (!isPanelUndoKey(e)) return;
90038
+ e.preventDefault();
90039
+ e.stopPropagation();
90040
+ handleUndo();
89687
90041
  },
89688
90042
  children: [
89689
90043
  persistError && /* @__PURE__ */ jsxs173(
@@ -91235,6 +91589,7 @@ function StudioRightPanels({
91235
91589
  handleDomAttributeQuietCommit,
91236
91590
  handleDomHtmlAttributeCommit,
91237
91591
  handleDomAttributesCommit,
91592
+ handleDomAttributeBatchCommit,
91238
91593
  handleDomPathOffsetCommit,
91239
91594
  handleDomBoxSizeCommit,
91240
91595
  handleDomRotationCommit,
@@ -91357,6 +91712,7 @@ function StudioRightPanels({
91357
91712
  onSetStyle: handleDomStyleCommit,
91358
91713
  onSetAttribute: handleDomAttributeCommit,
91359
91714
  onSetAttributes: handleDomAttributesCommit,
91715
+ onSetAttributeBatch: handleDomAttributeBatchCommit,
91360
91716
  onSetAttributeLive: setAttributeWhileDragging,
91361
91717
  onSetAttributeQuiet: handleDomAttributeQuietCommit,
91362
91718
  onApplyColorGradingScope: handleApplyColorGradingScope,
@@ -91800,17 +92156,11 @@ function useEnableKeyframes(sessionRef) {
91800
92156
 
91801
92157
  // src/hooks/useKeyframeKeyboard.ts
91802
92158
  import { useEffect as useEffect147, useCallback as useCallback183 } from "react";
91803
- function isTextInput2(el) {
91804
- if (!el) return false;
91805
- const tag = el.tagName;
91806
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
91807
- return el.isContentEditable === true;
91808
- }
91809
92159
  function useKeyframeKeyboard({ enabled, onAddKeyframe }) {
91810
92160
  const handler = useCallback183(
91811
92161
  (e) => {
91812
92162
  if (!enabled || !onAddKeyframe) return;
91813
- if (isTextInput2(document.activeElement)) return;
92163
+ if (isTypingTarget(document.activeElement)) return;
91814
92164
  if (e.metaKey || e.ctrlKey) return;
91815
92165
  if (e.key.toLowerCase() !== STUDIO_PLAIN_KEYS.addKeyframe) return;
91816
92166
  e.preventDefault();
@@ -92099,10 +92449,7 @@ function TimelineToolbar({
92099
92449
  const onKeyDown = (e) => {
92100
92450
  if (e.key !== "n" && e.key !== "N") return;
92101
92451
  if (e.metaKey || e.ctrlKey || e.altKey) return;
92102
- const target = e.target instanceof HTMLElement ? e.target : null;
92103
- if (target?.isContentEditable) return;
92104
- const tag = target?.tagName?.toLowerCase() ?? "";
92105
- if (tag === "input" || tag === "textarea" || tag === "select") return;
92452
+ if (isTypingTarget(e.target)) return;
92106
92453
  const store = usePlayerStore.getState();
92107
92454
  store.setTimelineSnapEnabled(!store.timelineSnapEnabled);
92108
92455
  };
@@ -92841,7 +93188,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
92841
93188
  showToast,
92842
93189
  syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
92843
93190
  showHistoryRestoreNow: previewPersistence.showHistoryRestoreNow,
92844
- waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
93191
+ settlePendingEdits: previewPersistence.settlePendingEdits,
92845
93192
  handleCopy: handleCopy2,
92846
93193
  handlePaste: handlePaste2,
92847
93194
  handleCut,
@@ -92900,9 +93247,10 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
92900
93247
  readOnlyPreview
92901
93248
  });
92902
93249
  domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
92903
- handleDomZIndexReorderCommitRef.current = domEditSession.handleDomZIndexReorderCommit;
93250
+ const { handleDomZIndexReorderCommit: zCommit, handleDomEditElementDelete: del } = domEditSession;
93251
+ handleDomZIndexReorderCommitRef.current = trackedStudioEdit(zCommit);
92904
93252
  clearDomSelectionRef.current = domEditSession.clearDomSelection;
92905
- handleDomEditElementDeleteRef.current = domEditSession.handleDomEditElementDelete;
93253
+ handleDomEditElementDeleteRef.current = trackedStudioEdit(del);
92906
93254
  resetKeyframesRef.current = domEditSession.handleResetSelectedElementKeyframes;
92907
93255
  invalidateGsapCacheRef.current = domEditSession.invalidateGsapCache;
92908
93256
  deleteSelectedKeyframesRef.current = () => deleteSelectedKeyframes(domEditSession);
@@ -93794,11 +94142,13 @@ export {
93794
94142
  buildGridSnapEdges,
93795
94143
  buildPickerAgentContextPreview,
93796
94144
  buildPickerAgentPrompt,
94145
+ buildStudioTools,
93797
94146
  buttonBase,
93798
94147
  buttonSizes,
93799
94148
  buttonVariants,
93800
94149
  cn,
93801
94150
  collectSnapContext,
94151
+ collectStudioLookScene,
93802
94152
  extractSnapTargets,
93803
94153
  fieldBase,
93804
94154
  fieldText,