@hyperframes/studio 0.6.5 → 0.6.7

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 (57) hide show
  1. package/dist/assets/{hyperframes-player-CzwFysqv.js → hyperframes-player-D0Yi3xMP.js} +2 -2
  2. package/dist/assets/index-Ckqo37Co.css +1 -0
  3. package/dist/assets/index-Yvtxngdi.js +116 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +54 -31
  7. package/src/components/StudioGlobalDragOverlay.tsx +26 -0
  8. package/src/components/StudioHeader.tsx +128 -3
  9. package/src/components/StudioRightPanel.tsx +0 -2
  10. package/src/components/editor/DomEditOverlay.test.ts +1 -0
  11. package/src/components/editor/DomEditOverlay.tsx +2 -1
  12. package/src/components/editor/PropertyPanel.tsx +27 -36
  13. package/src/components/editor/domEditingElement.ts +1 -0
  14. package/src/components/editor/manualEdits.test.ts +39 -466
  15. package/src/components/editor/manualEdits.ts +6 -168
  16. package/src/components/editor/manualEditsDom.ts +361 -1
  17. package/src/components/editor/manualEditsParsing.ts +2 -240
  18. package/src/components/editor/manualEditsTypes.ts +1 -40
  19. package/src/components/editor/useDomEditOverlayGestures.ts +25 -8
  20. package/src/components/nle/NLEPreview.tsx +1 -1
  21. package/src/components/sidebar/CompositionsTab.tsx +9 -3
  22. package/src/contexts/DomEditContext.tsx +3 -0
  23. package/src/contexts/FileManagerContext.tsx +3 -0
  24. package/src/hooks/useAppHotkeys.ts +1 -4
  25. package/src/hooks/useDomEditCommits.ts +82 -77
  26. package/src/hooks/useDomEditSession.ts +4 -16
  27. package/src/hooks/useFileManager.ts +10 -1
  28. package/src/hooks/useManifestPersistence.ts +51 -187
  29. package/src/hooks/usePanelLayout.ts +10 -3
  30. package/src/hooks/usePreviewInteraction.ts +0 -1
  31. package/src/hooks/useStudioUrlState.ts +188 -0
  32. package/src/player/components/Player.tsx +15 -1
  33. package/src/player/components/PlayerControls.test.ts +17 -0
  34. package/src/player/components/PlayerControls.tsx +347 -56
  35. package/src/player/hooks/usePlaybackKeyboard.test.ts +174 -0
  36. package/src/player/hooks/usePlaybackKeyboard.ts +37 -10
  37. package/src/player/hooks/useTimelinePlayer.seek.test.ts +329 -0
  38. package/src/player/hooks/useTimelinePlayer.ts +97 -28
  39. package/src/player/hooks/useTimelineSyncCallbacks.ts +10 -4
  40. package/src/player/lib/playbackAdapter.test.ts +50 -0
  41. package/src/player/lib/playbackAdapter.ts +2 -2
  42. package/src/player/lib/playbackTypes.ts +1 -1
  43. package/src/player/lib/timelineDOM.ts +4 -2
  44. package/src/player/lib/timelineIframeHelpers.ts +63 -7
  45. package/src/player/store/playerStore.test.ts +105 -1
  46. package/src/player/store/playerStore.ts +39 -1
  47. package/src/utils/projectRouting.test.ts +15 -0
  48. package/src/utils/projectRouting.ts +46 -9
  49. package/src/utils/sourcePatcher.ts +50 -14
  50. package/src/utils/studioPreviewHelpers.test.ts +56 -0
  51. package/src/utils/studioPreviewHelpers.ts +51 -13
  52. package/src/utils/studioUiPreferences.test.ts +3 -0
  53. package/src/utils/studioUiPreferences.ts +4 -0
  54. package/src/utils/studioUrlState.test.ts +249 -0
  55. package/src/utils/studioUrlState.ts +135 -0
  56. package/dist/assets/index-Bs6NmE0o.js +0 -117
  57. package/dist/assets/index-Dswa2GJ2.css +0 -1
@@ -1,34 +1,17 @@
1
1
  // Public re-exports — consumers import from this file as before.
2
2
  export {
3
- STUDIO_MANUAL_EDITS_PATH,
4
3
  STUDIO_OFFSET_X_PROP,
5
4
  STUDIO_OFFSET_Y_PROP,
6
5
  STUDIO_WIDTH_PROP,
7
6
  STUDIO_HEIGHT_PROP,
8
7
  STUDIO_ROTATION_PROP,
9
- type StudioManualEditTarget,
10
- type StudioPathOffsetEdit,
11
- type StudioBoxSizeEdit,
12
- type StudioRotationEdit,
13
- type StudioManualEdit,
14
- type StudioManualEditManifest,
15
8
  type StudioManualEditSeekWindow,
16
9
  type StudioBoxSizeSnapshot,
17
10
  type StudioRotationSnapshot,
18
11
  type StudioPathOffsetSnapshot,
19
12
  } from "./manualEditsTypes";
20
13
 
21
- export {
22
- emptyStudioManualEditManifest,
23
- parseStudioManualEditManifest,
24
- serializeStudioManualEditManifest,
25
- readStudioFileChangePath,
26
- isStudioManualEditManifestPath,
27
- upsertStudioPathOffsetEdit,
28
- upsertStudioBoxSizeEdit,
29
- upsertStudioRotationEdit,
30
- removeStudioManualEditsForSelection,
31
- } from "./manualEditsParsing";
14
+ export { readStudioFileChangePath } from "./manualEditsParsing";
32
15
 
33
16
  export {
34
17
  beginStudioManualEditGesture,
@@ -46,6 +29,7 @@ export {
46
29
  clearStudioPathOffset,
47
30
  clearStudioRotation,
48
31
  clearStudioBoxSize,
32
+ reapplyPositionEditsAfterSeek,
49
33
  } from "./manualEditsDom";
50
34
 
51
35
  export {
@@ -57,27 +41,14 @@ export {
57
41
  restoreStudioPathOffset,
58
42
  } from "./manualEditsSnapshot";
59
43
 
60
- import type {
61
- StudioManualEdit,
62
- StudioManualEditManifest,
63
- StudioManualEditSeekWindow,
64
- } from "./manualEditsTypes";
44
+ import type { StudioManualEditSeekWindow } from "./manualEditsTypes";
65
45
  import {
66
46
  STUDIO_MANUAL_EDITS_APPLY_PROP,
67
47
  STUDIO_MANUAL_EDITS_WRAPPED_PROP,
68
48
  STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP,
69
49
  } from "./manualEditsTypes";
70
50
  import { finiteNumber } from "./manualEditsParsing";
71
- import {
72
- applyStudioPathOffset,
73
- applyStudioBoxSize,
74
- applyStudioRotation,
75
- isStudioManualEditGestureActive,
76
- clearStudioPathOffset,
77
- clearStudioBoxSize,
78
- clearStudioRotation,
79
- } from "./manualEditsDom";
80
- import { collectStudioManualEditElements } from "./manualEditsSnapshot";
51
+ import { isStudioManualEditGestureActive } from "./manualEditsDom";
81
52
 
82
53
  /* ── Seek/play reapply wrappers ───────────────────────────────────── */
83
54
  function markWrapped(fn: (...args: unknown[]) => unknown): void {
@@ -307,138 +278,5 @@ export function installStudioManualEditSeekReapply(win: Window, apply: () => voi
307
278
  );
308
279
  }
309
280
 
310
- /* ── DOM target resolution ────────────────────────────────────────── */
311
- function getManualEditSourceFileForElement(
312
- el: HTMLElement,
313
- activeCompositionPath: string | null,
314
- ): string {
315
- let current: HTMLElement | null = el;
316
- while (current) {
317
- const sourceFile =
318
- current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
319
- if (sourceFile) return sourceFile;
320
- current = current.parentElement;
321
- }
322
- return activeCompositionPath ?? "index.html";
323
- }
324
-
325
- function elementMatchesManualEditSourceFile(
326
- element: HTMLElement,
327
- sourceFile: string,
328
- activeCompositionPath: string | null,
329
- ): boolean {
330
- return getManualEditSourceFileForElement(element, activeCompositionPath) === sourceFile;
331
- }
332
-
333
- function queryManualEditSelectorCandidates(
334
- doc: Document,
335
- selector: string,
336
- htmlElement: typeof HTMLElement,
337
- ): HTMLElement[] {
338
- const isCandidate = (element: Element): element is HTMLElement => element instanceof htmlElement;
339
-
340
- const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
341
- if (className) {
342
- return Array.from(doc.getElementsByTagName("*")).filter(
343
- (element): element is HTMLElement =>
344
- isCandidate(element) && element.classList.contains(className),
345
- );
346
- }
347
-
348
- if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
349
- return Array.from(doc.getElementsByTagName(selector)).filter(isCandidate);
350
- }
351
-
352
- return Array.from(doc.querySelectorAll(selector)).filter(isCandidate);
353
- }
354
-
355
- function resolveManualEditTarget(
356
- doc: Document,
357
- edit: StudioManualEdit,
358
- activeCompositionPath: string | null,
359
- ): HTMLElement | null {
360
- const htmlElement = doc.defaultView?.HTMLElement;
361
- if (!htmlElement) return null;
362
-
363
- if (edit.target.id) {
364
- const byId = doc.getElementById(edit.target.id);
365
- if (
366
- byId instanceof htmlElement &&
367
- elementMatchesManualEditSourceFile(byId, edit.target.sourceFile, activeCompositionPath)
368
- ) {
369
- return byId;
370
- }
371
-
372
- const matchesById = [doc.documentElement, ...Array.from(doc.getElementsByTagName("*"))].filter(
373
- (element): element is HTMLElement =>
374
- element instanceof htmlElement &&
375
- element.id === edit.target.id &&
376
- elementMatchesManualEditSourceFile(element, edit.target.sourceFile, activeCompositionPath),
377
- );
378
- if (matchesById[0]) return matchesById[0];
379
- }
380
-
381
- if (!edit.target.selector) return null;
382
- try {
383
- const matches = queryManualEditSelectorCandidates(
384
- doc,
385
- edit.target.selector,
386
- htmlElement,
387
- ).filter((element) =>
388
- elementMatchesManualEditSourceFile(element, edit.target.sourceFile, activeCompositionPath),
389
- );
390
- return matches[edit.target.selectorIndex ?? 0] ?? null;
391
- } catch {
392
- return null;
393
- }
394
- }
395
-
396
- /* ── Manifest application ─────────────────────────────────────────── */
397
- export function applyStudioManualEditManifest(
398
- doc: Document,
399
- manifest: StudioManualEditManifest,
400
- activeCompositionPath: string | null,
401
- ): number {
402
- const resolvedEdits: Array<{ edit: StudioManualEdit; element: HTMLElement }> = [];
403
- const pathOffsetTargets = new Set<HTMLElement>();
404
- const boxSizeTargets = new Set<HTMLElement>();
405
- const rotationTargets = new Set<HTMLElement>();
406
-
407
- for (const edit of manifest.edits) {
408
- const element = resolveManualEditTarget(doc, edit, activeCompositionPath);
409
- if (!element) continue;
410
- if (isStudioManualEditGestureActive(element)) {
411
- continue;
412
- }
413
- resolvedEdits.push({ edit, element });
414
- if (edit.kind === "path-offset") pathOffsetTargets.add(element);
415
- if (edit.kind === "box-size") boxSizeTargets.add(element);
416
- if (edit.kind === "rotation") rotationTargets.add(element);
417
- }
418
-
419
- for (const element of collectStudioManualEditElements(doc)) {
420
- if (isStudioManualEditGestureActive(element)) continue;
421
- if (!pathOffsetTargets.has(element)) {
422
- clearStudioPathOffset(element);
423
- }
424
- if (!boxSizeTargets.has(element)) {
425
- clearStudioBoxSize(element);
426
- }
427
- if (!rotationTargets.has(element)) {
428
- clearStudioRotation(element);
429
- }
430
- }
431
-
432
- let applied = 0;
433
- for (const { edit, element } of resolvedEdits) {
434
- if (edit.kind === "path-offset") {
435
- applyStudioPathOffset(element, { x: edit.x, y: edit.y });
436
- } else if (edit.kind === "box-size") {
437
- applyStudioBoxSize(element, { width: edit.width, height: edit.height });
438
- } else {
439
- applyStudioRotation(element, { angle: edit.angle });
440
- }
441
- applied += 1;
442
- }
443
- return applied;
444
- }
281
+ // Re-export for internal use (seek hooks need this)
282
+ export { isStudioManualEditGestureActive };
@@ -209,12 +209,39 @@ function writeStudioPathOffsetVars(
209
209
  }
210
210
 
211
211
  /* ── Path offset apply ────────────────────────────────────────────── */
212
+
213
+ // GSAP 3.x reads the resolved CSS `translate` individual property at initialization and bakes it
214
+ // into element.style.transform (as a matrix) on every seek. When the studio's reapply hook also
215
+ // writes `translate`, both properties compose additively, doubling the visual offset. This helper
216
+ // zeroes out only the translate component (m41/m42) so the `translate` prop isn't double-counted.
217
+ function stripGsapTranslateFromTransform(element: HTMLElement): void {
218
+ const transform = element.style.getPropertyValue("transform");
219
+ if (!transform || transform === "none") return;
220
+ const win = element.ownerDocument.defaultView as (Window & typeof globalThis) | null;
221
+ const DOMMatrixCtor = (win as unknown as { DOMMatrix?: typeof DOMMatrix })?.DOMMatrix;
222
+ if (!DOMMatrixCtor) return;
223
+ try {
224
+ const m = new DOMMatrixCtor(transform);
225
+ if (m.m41 === 0 && m.m42 === 0) return;
226
+ m.m41 = 0;
227
+ m.m42 = 0;
228
+ if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) {
229
+ element.style.removeProperty("transform");
230
+ } else {
231
+ element.style.setProperty("transform", m.toString());
232
+ }
233
+ } catch {
234
+ // non-parseable transform or DOMMatrix unavailable — leave as-is
235
+ }
236
+ }
237
+
212
238
  export function applyStudioPathOffset(
213
239
  element: HTMLElement,
214
240
  offset: { x: number; y: number },
241
+ options: { updateBase?: boolean } = {},
215
242
  ): void {
216
243
  promoteInlineForTransform(element);
217
- writeStudioPathOffsetVars(element, offset);
244
+ writeStudioPathOffsetVars(element, offset, { updateBase: options.updateBase ?? true });
218
245
  element.style.setProperty(
219
246
  "translate",
220
247
  composeTranslateValue(
@@ -223,6 +250,7 @@ export function applyStudioPathOffset(
223
250
  `var(${STUDIO_OFFSET_Y_PROP}, 0px)`,
224
251
  ),
225
252
  );
253
+ stripGsapTranslateFromTransform(element);
226
254
  }
227
255
 
228
256
  export function applyStudioPathOffsetDraft(
@@ -235,6 +263,7 @@ export function applyStudioPathOffsetDraft(
235
263
  "translate",
236
264
  composeTranslateValue(element, `${Math.round(offset.x)}px`, `${Math.round(offset.y)}px`),
237
265
  );
266
+ stripGsapTranslateFromTransform(element);
238
267
  }
239
268
 
240
269
  /* ── Box size apply ───────────────────────────────────────────────── */
@@ -434,3 +463,334 @@ export {
434
463
  clearStudioRotation,
435
464
  clearStudioBoxSize,
436
465
  } from "./manualEditsSnapshot";
466
+
467
+ /* ── HTML patch builders ──────────────────────────────────────────── */
468
+ import type { PatchOperation } from "../../utils/sourcePatcher";
469
+
470
+ export function buildPathOffsetPatches(element: HTMLElement): PatchOperation[] {
471
+ const x = element.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
472
+ const y = element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
473
+ const translate = element.style.getPropertyValue("translate");
474
+ const originalTranslate = element.getAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR);
475
+ const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR);
476
+ const displayVal = element.style.getPropertyValue("display");
477
+ const transformDisplayAttr = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
478
+ const ops: PatchOperation[] = [];
479
+ if (x) ops.push({ type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: x });
480
+ if (y) ops.push({ type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: y });
481
+ if (translate) ops.push({ type: "inline-style", property: "translate", value: translate });
482
+ ops.push({ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" });
483
+ if (originalTranslate !== null)
484
+ ops.push({
485
+ type: "attribute",
486
+ property: STUDIO_ORIGINAL_TRANSLATE_ATTR,
487
+ value: originalTranslate,
488
+ });
489
+ if (originalInlineTranslate !== null)
490
+ ops.push({
491
+ type: "attribute",
492
+ property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
493
+ value: originalInlineTranslate,
494
+ });
495
+ if (displayVal) ops.push({ type: "inline-style", property: "display", value: displayVal });
496
+ if (transformDisplayAttr !== null)
497
+ ops.push({
498
+ type: "attribute",
499
+ property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
500
+ value: transformDisplayAttr,
501
+ });
502
+ return ops;
503
+ }
504
+
505
+ export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperation[] {
506
+ const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR);
507
+ const ops: PatchOperation[] = [
508
+ { type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: null },
509
+ { type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: null },
510
+ {
511
+ type: "inline-style",
512
+ property: "translate",
513
+ value: originalInlineTranslate || null,
514
+ },
515
+ { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: null },
516
+ { type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: null },
517
+ { type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: null },
518
+ ];
519
+ const origDisplay = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
520
+ if (origDisplay !== null) {
521
+ ops.push({ type: "inline-style", property: "display", value: origDisplay || null });
522
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: null });
523
+ }
524
+ return ops;
525
+ }
526
+
527
+ export function buildBoxSizePatches(element: HTMLElement): PatchOperation[] {
528
+ const ops: PatchOperation[] = [];
529
+
530
+ const studioWidth = element.style.getPropertyValue(STUDIO_WIDTH_PROP);
531
+ const studioHeight = element.style.getPropertyValue(STUDIO_HEIGHT_PROP);
532
+ if (studioWidth)
533
+ ops.push({ type: "inline-style", property: STUDIO_WIDTH_PROP, value: studioWidth });
534
+ if (studioHeight)
535
+ ops.push({ type: "inline-style", property: STUDIO_HEIGHT_PROP, value: studioHeight });
536
+
537
+ const width = element.style.getPropertyValue("width");
538
+ const height = element.style.getPropertyValue("height");
539
+ const minWidth = element.style.getPropertyValue("min-width");
540
+ const minHeight = element.style.getPropertyValue("min-height");
541
+ const maxWidth = element.style.getPropertyValue("max-width");
542
+ const maxHeight = element.style.getPropertyValue("max-height");
543
+ const flexBasis = element.style.getPropertyValue("flex-basis");
544
+ const flexGrow = element.style.getPropertyValue("flex-grow");
545
+ const flexShrink = element.style.getPropertyValue("flex-shrink");
546
+ const boxSizing = element.style.getPropertyValue("box-sizing");
547
+ const scale = element.style.getPropertyValue("scale");
548
+ const transformOrigin = element.style.getPropertyValue("transform-origin");
549
+ const displayVal = element.style.getPropertyValue("display");
550
+
551
+ if (width) ops.push({ type: "inline-style", property: "width", value: width });
552
+ if (height) ops.push({ type: "inline-style", property: "height", value: height });
553
+ if (minWidth) ops.push({ type: "inline-style", property: "min-width", value: minWidth });
554
+ if (minHeight) ops.push({ type: "inline-style", property: "min-height", value: minHeight });
555
+ if (maxWidth) ops.push({ type: "inline-style", property: "max-width", value: maxWidth });
556
+ if (maxHeight) ops.push({ type: "inline-style", property: "max-height", value: maxHeight });
557
+ if (flexBasis) ops.push({ type: "inline-style", property: "flex-basis", value: flexBasis });
558
+ if (flexGrow) ops.push({ type: "inline-style", property: "flex-grow", value: flexGrow });
559
+ if (flexShrink) ops.push({ type: "inline-style", property: "flex-shrink", value: flexShrink });
560
+ if (boxSizing) ops.push({ type: "inline-style", property: "box-sizing", value: boxSizing });
561
+ if (scale) ops.push({ type: "inline-style", property: "scale", value: scale });
562
+ if (transformOrigin)
563
+ ops.push({ type: "inline-style", property: "transform-origin", value: transformOrigin });
564
+ if (displayVal) ops.push({ type: "inline-style", property: "display", value: displayVal });
565
+
566
+ ops.push({ type: "attribute", property: STUDIO_BOX_SIZE_ATTR, value: "true" });
567
+
568
+ const origWidth = element.getAttribute(STUDIO_ORIGINAL_WIDTH_ATTR);
569
+ const origHeight = element.getAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR);
570
+ const origMinWidth = element.getAttribute(STUDIO_ORIGINAL_MIN_WIDTH_ATTR);
571
+ const origMinHeight = element.getAttribute(STUDIO_ORIGINAL_MIN_HEIGHT_ATTR);
572
+ const origMaxWidth = element.getAttribute(STUDIO_ORIGINAL_MAX_WIDTH_ATTR);
573
+ const origMaxHeight = element.getAttribute(STUDIO_ORIGINAL_MAX_HEIGHT_ATTR);
574
+ const origFlexBasis = element.getAttribute(STUDIO_ORIGINAL_FLEX_BASIS_ATTR);
575
+ const origFlexGrow = element.getAttribute(STUDIO_ORIGINAL_FLEX_GROW_ATTR);
576
+ const origFlexShrink = element.getAttribute(STUDIO_ORIGINAL_FLEX_SHRINK_ATTR);
577
+ const origBoxSizing = element.getAttribute(STUDIO_ORIGINAL_BOX_SIZING_ATTR);
578
+ const origScale = element.getAttribute(STUDIO_ORIGINAL_SCALE_ATTR);
579
+ const origTransformOrigin = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR);
580
+ const origDisplay = element.getAttribute(STUDIO_ORIGINAL_DISPLAY_ATTR);
581
+ const origTransformDisplay = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
582
+
583
+ if (origWidth !== null)
584
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_WIDTH_ATTR, value: origWidth });
585
+ if (origHeight !== null)
586
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_HEIGHT_ATTR, value: origHeight });
587
+ if (origMinWidth !== null)
588
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_MIN_WIDTH_ATTR, value: origMinWidth });
589
+ if (origMinHeight !== null)
590
+ ops.push({
591
+ type: "attribute",
592
+ property: STUDIO_ORIGINAL_MIN_HEIGHT_ATTR,
593
+ value: origMinHeight,
594
+ });
595
+ if (origMaxWidth !== null)
596
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_MAX_WIDTH_ATTR, value: origMaxWidth });
597
+ if (origMaxHeight !== null)
598
+ ops.push({
599
+ type: "attribute",
600
+ property: STUDIO_ORIGINAL_MAX_HEIGHT_ATTR,
601
+ value: origMaxHeight,
602
+ });
603
+ if (origFlexBasis !== null)
604
+ ops.push({
605
+ type: "attribute",
606
+ property: STUDIO_ORIGINAL_FLEX_BASIS_ATTR,
607
+ value: origFlexBasis,
608
+ });
609
+ if (origFlexGrow !== null)
610
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_FLEX_GROW_ATTR, value: origFlexGrow });
611
+ if (origFlexShrink !== null)
612
+ ops.push({
613
+ type: "attribute",
614
+ property: STUDIO_ORIGINAL_FLEX_SHRINK_ATTR,
615
+ value: origFlexShrink,
616
+ });
617
+ if (origBoxSizing !== null)
618
+ ops.push({
619
+ type: "attribute",
620
+ property: STUDIO_ORIGINAL_BOX_SIZING_ATTR,
621
+ value: origBoxSizing,
622
+ });
623
+ if (origScale !== null)
624
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_SCALE_ATTR, value: origScale });
625
+ if (origTransformOrigin !== null)
626
+ ops.push({
627
+ type: "attribute",
628
+ property: STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR,
629
+ value: origTransformOrigin,
630
+ });
631
+ if (origDisplay !== null)
632
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_DISPLAY_ATTR, value: origDisplay });
633
+ if (origTransformDisplay !== null)
634
+ ops.push({
635
+ type: "attribute",
636
+ property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
637
+ value: origTransformDisplay,
638
+ });
639
+
640
+ return ops;
641
+ }
642
+
643
+ export function buildClearBoxSizePatches(element: HTMLElement): PatchOperation[] {
644
+ const ops: PatchOperation[] = [
645
+ { type: "inline-style", property: STUDIO_WIDTH_PROP, value: null },
646
+ { type: "inline-style", property: STUDIO_HEIGHT_PROP, value: null },
647
+ { type: "attribute", property: STUDIO_BOX_SIZE_ATTR, value: null },
648
+ ];
649
+
650
+ const origAttrs: Array<[string, string]> = [
651
+ [STUDIO_ORIGINAL_WIDTH_ATTR, "width"],
652
+ [STUDIO_ORIGINAL_HEIGHT_ATTR, "height"],
653
+ [STUDIO_ORIGINAL_MIN_WIDTH_ATTR, "min-width"],
654
+ [STUDIO_ORIGINAL_MIN_HEIGHT_ATTR, "min-height"],
655
+ [STUDIO_ORIGINAL_MAX_WIDTH_ATTR, "max-width"],
656
+ [STUDIO_ORIGINAL_MAX_HEIGHT_ATTR, "max-height"],
657
+ [STUDIO_ORIGINAL_FLEX_BASIS_ATTR, "flex-basis"],
658
+ [STUDIO_ORIGINAL_FLEX_GROW_ATTR, "flex-grow"],
659
+ [STUDIO_ORIGINAL_FLEX_SHRINK_ATTR, "flex-shrink"],
660
+ [STUDIO_ORIGINAL_BOX_SIZING_ATTR, "box-sizing"],
661
+ [STUDIO_ORIGINAL_SCALE_ATTR, "scale"],
662
+ [STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR, "transform-origin"],
663
+ [STUDIO_ORIGINAL_DISPLAY_ATTR, "display"],
664
+ ];
665
+
666
+ for (const [attrName, styleProp] of origAttrs) {
667
+ const origVal = element.getAttribute(attrName);
668
+ if (origVal !== null) {
669
+ ops.push({ type: "inline-style", property: styleProp, value: origVal || null });
670
+ }
671
+ ops.push({ type: "attribute", property: attrName, value: null });
672
+ }
673
+
674
+ const origTransformDisplay = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
675
+ if (origTransformDisplay !== null) {
676
+ ops.push({ type: "inline-style", property: "display", value: origTransformDisplay || null });
677
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: null });
678
+ }
679
+
680
+ return ops;
681
+ }
682
+
683
+ export function buildRotationPatches(element: HTMLElement): PatchOperation[] {
684
+ const ops: PatchOperation[] = [];
685
+
686
+ const studioRotation = element.style.getPropertyValue(STUDIO_ROTATION_PROP);
687
+ const rotate = element.style.getPropertyValue("rotate");
688
+ const transformOrigin = element.style.getPropertyValue("transform-origin");
689
+ const displayVal = element.style.getPropertyValue("display");
690
+
691
+ if (studioRotation)
692
+ ops.push({ type: "inline-style", property: STUDIO_ROTATION_PROP, value: studioRotation });
693
+ if (rotate) ops.push({ type: "inline-style", property: "rotate", value: rotate });
694
+ if (transformOrigin)
695
+ ops.push({ type: "inline-style", property: "transform-origin", value: transformOrigin });
696
+ if (displayVal) ops.push({ type: "inline-style", property: "display", value: displayVal });
697
+
698
+ ops.push({ type: "attribute", property: STUDIO_ROTATION_ATTR, value: "true" });
699
+
700
+ const origRotate = element.getAttribute(STUDIO_ORIGINAL_ROTATE_ATTR);
701
+ const origInlineRotate = element.getAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR);
702
+ const origRotationTransformOrigin = element.getAttribute(
703
+ STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
704
+ );
705
+ const origTransformDisplay = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
706
+
707
+ if (origRotate !== null)
708
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_ROTATE_ATTR, value: origRotate });
709
+ if (origInlineRotate !== null)
710
+ ops.push({
711
+ type: "attribute",
712
+ property: STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
713
+ value: origInlineRotate,
714
+ });
715
+ if (origRotationTransformOrigin !== null)
716
+ ops.push({
717
+ type: "attribute",
718
+ property: STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
719
+ value: origRotationTransformOrigin,
720
+ });
721
+ if (origTransformDisplay !== null)
722
+ ops.push({
723
+ type: "attribute",
724
+ property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
725
+ value: origTransformDisplay,
726
+ });
727
+
728
+ return ops;
729
+ }
730
+
731
+ export function buildClearRotationPatches(element: HTMLElement): PatchOperation[] {
732
+ const origInlineRotate = element.getAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR);
733
+ const origRotationTransformOrigin = element.getAttribute(
734
+ STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
735
+ );
736
+ const ops: PatchOperation[] = [
737
+ { type: "inline-style", property: STUDIO_ROTATION_PROP, value: null },
738
+ { type: "inline-style", property: "rotate", value: origInlineRotate || null },
739
+ {
740
+ type: "inline-style",
741
+ property: "transform-origin",
742
+ value: origRotationTransformOrigin !== null ? origRotationTransformOrigin || null : null,
743
+ },
744
+ { type: "attribute", property: STUDIO_ROTATION_ATTR, value: null },
745
+ { type: "attribute", property: STUDIO_ROTATION_DRAFT_ATTR, value: null },
746
+ { type: "attribute", property: STUDIO_ORIGINAL_ROTATE_ATTR, value: null },
747
+ { type: "attribute", property: STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, value: null },
748
+ { type: "attribute", property: STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR, value: null },
749
+ ];
750
+ const origTransformDisplay = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
751
+ if (origTransformDisplay !== null) {
752
+ ops.push({ type: "inline-style", property: "display", value: origTransformDisplay || null });
753
+ ops.push({ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: null });
754
+ }
755
+ return ops;
756
+ }
757
+
758
+ export function reapplyPositionEditsAfterSeek(doc: Document): void {
759
+ const htmlElement = doc.defaultView?.HTMLElement;
760
+ if (!htmlElement) return;
761
+
762
+ const offsetEls = Array.from(doc.querySelectorAll(`[${STUDIO_PATH_OFFSET_ATTR}="true"]`)).filter(
763
+ (el): el is HTMLElement => el instanceof htmlElement,
764
+ );
765
+ for (const el of offsetEls) {
766
+ const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
767
+ const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
768
+ if (x || y) {
769
+ applyStudioPathOffset(el, {
770
+ x: Number.parseFloat(x) || 0,
771
+ y: Number.parseFloat(y) || 0,
772
+ });
773
+ }
774
+ }
775
+
776
+ const boxSizeEls = Array.from(doc.querySelectorAll(`[${STUDIO_BOX_SIZE_ATTR}="true"]`)).filter(
777
+ (el): el is HTMLElement => el instanceof htmlElement,
778
+ );
779
+ for (const el of boxSizeEls) {
780
+ const w = Number.parseFloat(el.style.getPropertyValue(STUDIO_WIDTH_PROP));
781
+ const h = Number.parseFloat(el.style.getPropertyValue(STUDIO_HEIGHT_PROP));
782
+ if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
783
+ applyStudioBoxSize(el, { width: w, height: h });
784
+ }
785
+ }
786
+
787
+ const rotationEls = Array.from(doc.querySelectorAll(`[${STUDIO_ROTATION_ATTR}="true"]`)).filter(
788
+ (el): el is HTMLElement => el instanceof htmlElement,
789
+ );
790
+ for (const el of rotationEls) {
791
+ const angle = Number.parseFloat(el.style.getPropertyValue(STUDIO_ROTATION_PROP));
792
+ if (Number.isFinite(angle)) {
793
+ applyStudioRotation(el, { angle });
794
+ }
795
+ }
796
+ }