@hyperframes/studio 0.6.6 → 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 (55) hide show
  1. package/dist/assets/{hyperframes-player-T-ME1rqL.js → hyperframes-player-D0Yi3xMP.js} +2 -2
  2. package/dist/assets/{index-Bne9FFeo.css → index-Ckqo37Co.css} +1 -1
  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/StudioRightPanel.tsx +0 -2
  9. package/src/components/editor/DomEditOverlay.test.ts +1 -0
  10. package/src/components/editor/DomEditOverlay.tsx +2 -1
  11. package/src/components/editor/PropertyPanel.tsx +27 -36
  12. package/src/components/editor/domEditingElement.ts +1 -0
  13. package/src/components/editor/manualEdits.test.ts +39 -466
  14. package/src/components/editor/manualEdits.ts +6 -168
  15. package/src/components/editor/manualEditsDom.ts +361 -1
  16. package/src/components/editor/manualEditsParsing.ts +2 -240
  17. package/src/components/editor/manualEditsTypes.ts +1 -40
  18. package/src/components/editor/useDomEditOverlayGestures.ts +25 -8
  19. package/src/components/nle/NLEPreview.tsx +1 -1
  20. package/src/components/sidebar/CompositionsTab.tsx +9 -3
  21. package/src/contexts/DomEditContext.tsx +3 -0
  22. package/src/contexts/FileManagerContext.tsx +3 -0
  23. package/src/hooks/useAppHotkeys.ts +1 -4
  24. package/src/hooks/useDomEditCommits.ts +82 -77
  25. package/src/hooks/useDomEditSession.ts +4 -16
  26. package/src/hooks/useFileManager.ts +10 -1
  27. package/src/hooks/useManifestPersistence.ts +51 -187
  28. package/src/hooks/usePanelLayout.ts +10 -3
  29. package/src/hooks/usePreviewInteraction.ts +0 -1
  30. package/src/hooks/useStudioUrlState.ts +188 -0
  31. package/src/player/components/Player.tsx +15 -1
  32. package/src/player/components/PlayerControls.test.ts +17 -0
  33. package/src/player/components/PlayerControls.tsx +61 -0
  34. package/src/player/hooks/usePlaybackKeyboard.test.ts +174 -0
  35. package/src/player/hooks/usePlaybackKeyboard.ts +18 -15
  36. package/src/player/hooks/useTimelinePlayer.seek.test.ts +329 -0
  37. package/src/player/hooks/useTimelinePlayer.ts +76 -18
  38. package/src/player/hooks/useTimelineSyncCallbacks.ts +10 -4
  39. package/src/player/lib/playbackAdapter.test.ts +50 -0
  40. package/src/player/lib/playbackAdapter.ts +2 -2
  41. package/src/player/lib/playbackTypes.ts +1 -1
  42. package/src/player/lib/timelineDOM.ts +4 -2
  43. package/src/player/lib/timelineIframeHelpers.ts +63 -7
  44. package/src/player/store/playerStore.test.ts +105 -1
  45. package/src/player/store/playerStore.ts +12 -1
  46. package/src/utils/projectRouting.test.ts +15 -0
  47. package/src/utils/projectRouting.ts +46 -9
  48. package/src/utils/sourcePatcher.ts +50 -14
  49. package/src/utils/studioPreviewHelpers.test.ts +56 -0
  50. package/src/utils/studioPreviewHelpers.ts +51 -13
  51. package/src/utils/studioUiPreferences.test.ts +3 -0
  52. package/src/utils/studioUiPreferences.ts +4 -0
  53. package/src/utils/studioUrlState.test.ts +249 -0
  54. package/src/utils/studioUrlState.ts +135 -0
  55. package/dist/assets/index-DYqqzECY.js +0 -117
@@ -1,6 +1,5 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { Window } from "happy-dom";
3
- import type { DomEditSelection } from "./domEditing";
4
3
  import {
5
4
  STUDIO_OFFSET_X_PROP,
6
5
  STUDIO_OFFSET_Y_PROP,
@@ -8,7 +7,6 @@ import {
8
7
  STUDIO_WIDTH_PROP,
9
8
  applyStudioBoxSize,
10
9
  applyStudioBoxSizeDraft,
11
- applyStudioManualEditManifest,
12
10
  applyStudioPathOffset,
13
11
  applyStudioPathOffsetDraft,
14
12
  applyStudioRotation,
@@ -16,22 +14,17 @@ import {
16
14
  beginStudioManualEditGesture,
17
15
  captureStudioBoxSize,
18
16
  captureStudioRotation,
19
- emptyStudioManualEditManifest,
17
+ clearStudioBoxSize,
18
+ clearStudioPathOffset,
19
+ clearStudioRotation,
20
20
  endStudioManualEditGesture,
21
21
  installStudioManualEditSeekReapply,
22
- isStudioManualEditManifestPath,
23
- parseStudioManualEditManifest,
24
- readStudioFileChangePath,
25
22
  readStudioBoxSize,
23
+ readStudioFileChangePath,
26
24
  readStudioPathOffset,
27
25
  readStudioRotation,
28
- removeStudioManualEditsForSelection,
29
26
  restoreStudioBoxSize,
30
27
  restoreStudioRotation,
31
- serializeStudioManualEditManifest,
32
- upsertStudioBoxSizeEdit,
33
- upsertStudioPathOffsetEdit,
34
- upsertStudioRotationEdit,
35
28
  } from "./manualEdits";
36
29
 
37
30
  function createDocument(markup: string): Document {
@@ -40,36 +33,6 @@ function createDocument(markup: string): Document {
40
33
  return window.document;
41
34
  }
42
35
 
43
- function createSelection(): DomEditSelection {
44
- return {
45
- element: {} as HTMLElement,
46
- id: "card",
47
- selector: "#card",
48
- selectorIndex: undefined,
49
- sourceFile: "index.html",
50
- compositionPath: "index.html",
51
- compositionSrc: undefined,
52
- isCompositionHost: false,
53
- label: "Card",
54
- tagName: "div",
55
- boundingBox: { x: 0, y: 0, width: 100, height: 100 },
56
- textContent: null,
57
- dataAttributes: {},
58
- inlineStyles: {},
59
- computedStyles: {},
60
- textFields: [],
61
- capabilities: {
62
- canSelect: true,
63
- canEditStyles: true,
64
- canMove: false,
65
- canResize: false,
66
- canApplyManualOffset: true,
67
- canApplyManualSize: true,
68
- canApplyManualRotation: true,
69
- },
70
- };
71
- }
72
-
73
36
  function mockBoundingRect(element: HTMLElement, width: number, height: number): void {
74
37
  element.getBoundingClientRect = () =>
75
38
  ({
@@ -95,149 +58,13 @@ function mockComputedStyle(element: HTMLElement, values: Record<string, string>)
95
58
  }
96
59
 
97
60
  describe("studio manual edits", () => {
98
- it("upserts path offsets by stable target", () => {
99
- const manifest = upsertStudioPathOffsetEdit(
100
- emptyStudioManualEditManifest(),
101
- createSelection(),
102
- {
103
- x: 12.4,
104
- y: 30.6,
105
- },
106
- );
107
- const updated = upsertStudioPathOffsetEdit(manifest, createSelection(), {
108
- x: 20,
109
- y: 42,
110
- });
111
-
112
- expect(updated.edits).toHaveLength(1);
113
- expect(updated.edits[0]).toMatchObject({
114
- kind: "path-offset",
115
- target: { sourceFile: "index.html", selector: "#card", id: "card" },
116
- x: 20,
117
- y: 42,
118
- });
119
- });
120
-
121
- it("upserts box sizes without replacing path offsets for the same target", () => {
122
- const selection = createSelection();
123
- const manifest = upsertStudioPathOffsetEdit(emptyStudioManualEditManifest(), selection, {
124
- x: 12,
125
- y: 30,
126
- });
127
- const updated = upsertStudioBoxSizeEdit(manifest, selection, {
128
- width: 240.4,
129
- height: 120.6,
130
- });
131
- const resized = upsertStudioBoxSizeEdit(updated, selection, {
132
- width: 260,
133
- height: 140,
134
- });
135
-
136
- expect(resized.edits).toHaveLength(2);
137
- expect(resized.edits).toEqual(
138
- expect.arrayContaining([
139
- expect.objectContaining({ kind: "path-offset", x: 12, y: 30 }),
140
- expect.objectContaining({ kind: "box-size", width: 260, height: 140 }),
141
- ]),
142
- );
143
- });
144
-
145
- it("upserts rotations without replacing other manual edits for the same target", () => {
146
- const selection = createSelection();
147
- const manifest = upsertStudioPathOffsetEdit(emptyStudioManualEditManifest(), selection, {
148
- x: 12,
149
- y: 30,
150
- });
151
- const resized = upsertStudioBoxSizeEdit(manifest, selection, {
152
- width: 240,
153
- height: 120,
154
- });
155
- const rotated = upsertStudioRotationEdit(resized, selection, { angle: 32.34 });
156
- const updated = upsertStudioRotationEdit(rotated, selection, { angle: -14.96 });
157
-
158
- expect(updated.edits).toHaveLength(3);
159
- expect(updated.edits).toEqual(
160
- expect.arrayContaining([
161
- expect.objectContaining({ kind: "path-offset", x: 12, y: 30 }),
162
- expect.objectContaining({ kind: "box-size", width: 240, height: 120 }),
163
- expect.objectContaining({ kind: "rotation", angle: -15 }),
164
- ]),
165
- );
166
- });
167
-
168
- it("removes all manual edits for the selected target", () => {
169
- const selection = createSelection();
170
- const otherSelection = {
171
- ...createSelection(),
172
- id: "other-card",
173
- selector: "#other-card",
174
- label: "Other card",
175
- };
176
- const moved = upsertStudioPathOffsetEdit(emptyStudioManualEditManifest(), selection, {
177
- x: 12,
178
- y: 30,
179
- });
180
- const resized = upsertStudioBoxSizeEdit(moved, selection, {
181
- width: 240,
182
- height: 120,
183
- });
184
- const rotated = upsertStudioRotationEdit(resized, selection, { angle: 32 });
185
- const manifest = upsertStudioPathOffsetEdit(rotated, otherSelection, { x: 4, y: 8 });
186
-
187
- const updated = removeStudioManualEditsForSelection(manifest, selection);
188
-
189
- expect(updated.edits).toHaveLength(1);
190
- expect(updated.edits[0]).toMatchObject({
191
- kind: "path-offset",
192
- target: { id: "other-card", selector: "#other-card" },
193
- x: 4,
194
- y: 8,
195
- });
196
- });
197
-
198
- it("round-trips valid manifest entries and drops invalid entries", () => {
199
- const content = serializeStudioManualEditManifest({
200
- version: 1,
201
- edits: [
202
- {
203
- kind: "path-offset",
204
- target: { sourceFile: "index.html", selector: "#card", id: "card" },
205
- x: 10,
206
- y: 20,
207
- },
208
- {
209
- kind: "box-size",
210
- target: { sourceFile: "index.html", selector: "#card", id: "card" },
211
- width: 320,
212
- height: 180,
213
- },
214
- {
215
- kind: "rotation",
216
- target: { sourceFile: "index.html", selector: "#card", id: "card" },
217
- angle: 22.5,
218
- },
219
- ],
220
- });
221
-
222
- expect(parseStudioManualEditManifest(content).edits).toHaveLength(3);
223
- expect(parseStudioManualEditManifest('{ "edits": [{ "kind": "path-offset" }] }').edits).toEqual(
224
- [],
225
- );
226
- });
227
-
228
- it("recognizes manual edit manifest file-change payloads", () => {
61
+ it("recognizes studio file-change payloads", () => {
229
62
  expect(readStudioFileChangePath({ path: ".hyperframes/studio-manual-edits.json" })).toBe(
230
63
  ".hyperframes/studio-manual-edits.json",
231
64
  );
232
65
  expect(readStudioFileChangePath({ data: '{"path":"nested/file.html"}' })).toBe(
233
66
  "nested/file.html",
234
67
  );
235
- expect(
236
- isStudioManualEditManifestPath(
237
- "/Users/example/project/.hyperframes/studio-manual-edits.json",
238
- ),
239
- ).toBe(true);
240
- expect(isStudioManualEditManifestPath("index.html")).toBe(false);
241
68
  });
242
69
 
243
70
  it("applies offsets through CSS translate longhand", () => {
@@ -293,9 +120,8 @@ describe("studio manual edits", () => {
293
120
  applyStudioPathOffset(card, { x: 14, y: -8 });
294
121
  applyStudioRotation(card, { angle: 12 });
295
122
 
296
- expect(
297
- applyStudioManualEditManifest(document, emptyStudioManualEditManifest(), "index.html"),
298
- ).toBe(0);
123
+ clearStudioPathOffset(card);
124
+ clearStudioRotation(card);
299
125
 
300
126
  expect(card.style.getPropertyValue("translate")).toBe("");
301
127
  expect(card.style.getPropertyValue("rotate")).toBe("");
@@ -383,54 +209,6 @@ describe("studio manual edits", () => {
383
209
  expect(card.style.getPropertyValue("transform-origin")).toBe("center center");
384
210
  });
385
211
 
386
- it("does not recapture a studio rotation draft as the authored base", () => {
387
- const document = createDocument(`<div id="card" style="rotate: 8deg"></div>`);
388
- const card = document.getElementById("card") as HTMLElement;
389
- const manifest = parseStudioManualEditManifest(`{
390
- "version": 1,
391
- "edits": [
392
- {
393
- "kind": "rotation",
394
- "target": { "sourceFile": "index.html", "selector": "#card", "id": "card" },
395
- "angle": 35
396
- }
397
- ]
398
- }`);
399
-
400
- applyStudioRotation(card, { angle: 12 });
401
- applyStudioRotationDraft(card, { angle: 35 });
402
- expect(card.style.getPropertyValue("rotate")).toBe("calc(8deg + 35deg)");
403
-
404
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(1);
405
-
406
- expect(card.style.getPropertyValue("rotate")).toBe(
407
- `calc(8deg + var(${STUDIO_ROTATION_PROP}, 0deg))`,
408
- );
409
- });
410
-
411
- it("does not treat a base-free studio rotation draft as authored rotation", () => {
412
- const document = createDocument(`<div id="card"></div>`);
413
- const card = document.getElementById("card") as HTMLElement;
414
- const manifest = parseStudioManualEditManifest(`{
415
- "version": 1,
416
- "edits": [
417
- {
418
- "kind": "rotation",
419
- "target": { "sourceFile": "index.html", "selector": "#card", "id": "card" },
420
- "angle": 35
421
- }
422
- ]
423
- }`);
424
-
425
- applyStudioRotation(card, { angle: 12 });
426
- applyStudioRotationDraft(card, { angle: 35 });
427
- expect(card.style.getPropertyValue("rotate")).toBe("35deg");
428
-
429
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(1);
430
-
431
- expect(card.style.getPropertyValue("rotate")).toBe(`var(${STUDIO_ROTATION_PROP}, 0deg)`);
432
- });
433
-
434
212
  it("uses height for flex-basis inside column flex containers", () => {
435
213
  const document = createDocument(`
436
214
  <div style="display: flex; flex-direction: column">
@@ -523,9 +301,7 @@ describe("studio manual edits", () => {
523
301
  expect(tween._startAt.vars).toEqual({ x: -240, y: -20 });
524
302
  expect(card.style.getPropertyValue("translate")).toContain(STUDIO_OFFSET_X_PROP);
525
303
 
526
- expect(
527
- applyStudioManualEditManifest(document, emptyStudioManualEditManifest(), "index.html"),
528
- ).toBe(0);
304
+ clearStudioPathOffset(card);
529
305
  expect(tween.vars).toMatchObject({
530
306
  x: 0,
531
307
  y: 10,
@@ -537,197 +313,48 @@ describe("studio manual edits", () => {
537
313
  expect(card.style.getPropertyValue("translate")).toBe("");
538
314
  });
539
315
 
540
- it("applies manifest offsets to matching preview elements", () => {
541
- const document = createDocument(`<div id="card"></div>`);
542
- const manifest = parseStudioManualEditManifest(`{
543
- "version": 1,
544
- "edits": [
545
- {
546
- "kind": "path-offset",
547
- "target": { "sourceFile": "index.html", "selector": "#card", "id": "card" },
548
- "x": 32,
549
- "y": 18
550
- }
551
- ]
552
- }`);
553
-
554
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(1);
555
- expect(readStudioPathOffset(document.getElementById("card") as HTMLElement)).toEqual({
556
- x: 32,
557
- y: 18,
558
- });
559
- });
316
+ it("clears path offsets and restores authored inline translate", () => {
317
+ const document = createDocument(`<div id="card" style="translate: 10px 20px"></div>`);
318
+ const card = document.getElementById("card") as HTMLElement;
560
319
 
561
- it("resolves manifest targets within the matching source file", () => {
562
- const document = createDocument(`
563
- <div data-composition-id="root">
564
- <div id="card" class="tile"></div>
565
- <div data-composition-id="nested" data-composition-file="scenes/nested.html">
566
- <div id="card" class="tile"></div>
567
- <div class="tile"></div>
568
- </div>
569
- </div>
570
- `);
571
- const htmlElement = document.defaultView?.HTMLElement;
572
- if (!htmlElement) throw new Error("HTMLElement fixture missing");
573
- const cards = Array.from(document.getElementsByTagName("*")).filter(
574
- (element): element is HTMLElement => element instanceof htmlElement && element.id === "card",
575
- );
576
- const rootCard = cards[0];
577
- const nestedCard = cards[1];
578
- const tiles = Array.from(document.getElementsByTagName("*")).filter(
579
- (element): element is HTMLElement =>
580
- element instanceof htmlElement && element.classList.contains("tile"),
581
- );
582
- const nestedSecondTile = tiles[2];
583
- if (!rootCard || !nestedCard || !nestedSecondTile) {
584
- throw new Error("source-scoped fixture missing");
585
- }
586
-
587
- const manifest = parseStudioManualEditManifest(`{
588
- "version": 1,
589
- "edits": [
590
- {
591
- "kind": "path-offset",
592
- "target": {
593
- "sourceFile": "scenes/nested.html",
594
- "selector": "#card",
595
- "id": "card"
596
- },
597
- "x": 48,
598
- "y": 16
599
- },
600
- {
601
- "kind": "box-size",
602
- "target": {
603
- "sourceFile": "scenes/nested.html",
604
- "selector": ".tile",
605
- "selectorIndex": 1
606
- },
607
- "width": 220,
608
- "height": 80
609
- }
610
- ]
611
- }`);
612
-
613
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(2);
614
- expect(readStudioPathOffset(rootCard)).toEqual({ x: 0, y: 0 });
615
- expect(readStudioPathOffset(nestedCard)).toEqual({ x: 48, y: 16 });
616
- expect(readStudioBoxSize(nestedSecondTile)).toEqual({ width: 220, height: 80 });
617
- });
320
+ applyStudioPathOffset(card, { x: 24, y: 12 });
321
+ expect(card.style.getPropertyValue("translate")).toContain(STUDIO_OFFSET_X_PROP);
618
322
 
619
- it("resolves manifest targets inside composition-file hosts without composition ids", () => {
620
- const document = createDocument(`
621
- <div data-composition-id="root">
622
- <div id="card"></div>
623
- <div data-composition-file="scenes/anonymous.html">
624
- <div id="card"></div>
625
- </div>
626
- </div>
627
- `);
628
- const htmlElement = document.defaultView?.HTMLElement;
629
- if (!htmlElement) throw new Error("HTMLElement fixture missing");
630
- const cards = Array.from(document.getElementsByTagName("*")).filter(
631
- (element): element is HTMLElement => element instanceof htmlElement && element.id === "card",
632
- );
633
- const rootCard = cards[0];
634
- const nestedCard = cards[1];
635
- if (!rootCard || !nestedCard) {
636
- throw new Error("anonymous composition fixture missing");
637
- }
638
-
639
- const manifest = parseStudioManualEditManifest(`{
640
- "version": 1,
641
- "edits": [
642
- {
643
- "kind": "path-offset",
644
- "target": {
645
- "sourceFile": "scenes/anonymous.html",
646
- "selector": "#card",
647
- "id": "card"
648
- },
649
- "x": 24,
650
- "y": 12
651
- }
652
- ]
653
- }`);
654
-
655
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(1);
656
- expect(readStudioPathOffset(rootCard)).toEqual({ x: 0, y: 0 });
657
- expect(readStudioPathOffset(nestedCard)).toEqual({ x: 24, y: 12 });
323
+ clearStudioPathOffset(card);
324
+
325
+ expect(card.style.getPropertyValue("translate")).toBe("10px 20px");
658
326
  });
659
327
 
660
- it("applies nested source edits while previewing a non-index parent composition", () => {
661
- const document = createDocument(`
662
- <div data-composition-id="parent">
663
- <div id="parent-card"></div>
664
- <div data-composition-file="scenes/child.html">
665
- <div id="child-card"></div>
666
- </div>
667
- </div>
668
- `);
669
- const parentCard = document.getElementById("parent-card") as HTMLElement;
670
- const childCard = document.getElementById("child-card") as HTMLElement;
671
- const manifest = parseStudioManualEditManifest(`{
672
- "version": 1,
673
- "edits": [
674
- {
675
- "kind": "path-offset",
676
- "target": {
677
- "sourceFile": "scenes/parent.html",
678
- "selector": "#parent-card",
679
- "id": "parent-card"
680
- },
681
- "x": 12,
682
- "y": 8
683
- },
684
- {
685
- "kind": "path-offset",
686
- "target": {
687
- "sourceFile": "scenes/child.html",
688
- "selector": "#child-card",
689
- "id": "child-card"
690
- },
691
- "x": 36,
692
- "y": 18
693
- }
694
- ]
695
- }`);
696
-
697
- expect(applyStudioManualEditManifest(document, manifest, "scenes/parent.html")).toBe(2);
698
- expect(readStudioPathOffset(parentCard)).toEqual({ x: 12, y: 8 });
699
- expect(readStudioPathOffset(childCard)).toEqual({ x: 36, y: 18 });
328
+ it("clears stale offsets applied directly to the DOM", () => {
329
+ const document = createDocument(`<div id="card"></div>`);
330
+ const card = document.getElementById("card") as HTMLElement;
331
+
332
+ applyStudioPathOffset(card, { x: 24, y: 12 });
333
+ expect(readStudioPathOffset(card)).toEqual({ x: 24, y: 12 });
334
+
335
+ clearStudioPathOffset(card);
336
+
337
+ expect(readStudioPathOffset(card)).toEqual({ x: 0, y: 0 });
338
+ expect(card.style.getPropertyValue(STUDIO_OFFSET_X_PROP)).toBe("");
339
+ expect(card.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)).toBe("");
340
+ expect(card.style.getPropertyValue("translate")).toBe("");
700
341
  });
701
342
 
702
- it("applies and clears manifest box sizes while restoring authored inline size", () => {
343
+ it("clears box sizes and restores authored inline size", () => {
703
344
  const document = createDocument(`
704
345
  <div style="display: flex; flex-direction: row">
705
346
  <div id="card" style="width: 160px; height: 90px"></div>
706
347
  </div>
707
348
  `);
708
- const manifest = parseStudioManualEditManifest(`{
709
- "version": 1,
710
- "edits": [
711
- {
712
- "kind": "box-size",
713
- "target": { "sourceFile": "index.html", "selector": "#card", "id": "card" },
714
- "width": 320,
715
- "height": 180
716
- }
717
- ]
718
- }`);
719
349
  const card = document.getElementById("card") as HTMLElement;
720
350
  mockBoundingRect(card, 160, 90);
721
351
 
722
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(1);
352
+ applyStudioBoxSize(card, { width: 320, height: 180 });
723
353
  expect(readStudioBoxSize(card)).toEqual({ width: 320, height: 180 });
724
354
  expect(card.style.getPropertyValue("width")).toBe("320px");
725
- expect(card.style.getPropertyValue("height")).toBe("180px");
726
355
  expect(card.style.getPropertyValue("flex-basis")).toBe("320px");
727
356
 
728
- expect(
729
- applyStudioManualEditManifest(document, emptyStudioManualEditManifest(), "index.html"),
730
- ).toBe(0);
357
+ clearStudioBoxSize(card);
731
358
  expect(readStudioBoxSize(card)).toEqual({ width: 0, height: 0 });
732
359
  expect(card.style.getPropertyValue("width")).toBe("160px");
733
360
  expect(card.style.getPropertyValue("height")).toBe("90px");
@@ -737,93 +364,39 @@ describe("studio manual edits", () => {
737
364
  expect(card.style.getPropertyValue("scale")).toBe("");
738
365
  });
739
366
 
740
- it("applies and clears manifest rotations while restoring authored inline rotation", () => {
367
+ it("clears rotations and restores authored inline rotation", () => {
741
368
  const document = createDocument(
742
369
  `<div id="card" style="rotate: 8deg; transform-origin: left top"></div>`,
743
370
  );
744
- const manifest = parseStudioManualEditManifest(`{
745
- "version": 1,
746
- "edits": [
747
- {
748
- "kind": "rotation",
749
- "target": { "sourceFile": "index.html", "selector": "#card", "id": "card" },
750
- "angle": 37.5
751
- }
752
- ]
753
- }`);
754
371
  const card = document.getElementById("card") as HTMLElement;
755
372
 
756
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(1);
373
+ applyStudioRotation(card, { angle: 37.5 });
757
374
  expect(readStudioRotation(card)).toEqual({ angle: 37.5 });
758
375
  expect(card.style.getPropertyValue("rotate")).toContain(STUDIO_ROTATION_PROP);
759
376
  expect(card.style.getPropertyValue("rotate")).toContain("8deg");
760
377
  expect(card.style.getPropertyValue("transform-origin")).toBe("center center");
761
378
 
762
- expect(
763
- applyStudioManualEditManifest(document, emptyStudioManualEditManifest(), "index.html"),
764
- ).toBe(0);
379
+ clearStudioRotation(card);
765
380
  expect(readStudioRotation(card)).toEqual({ angle: 0 });
766
381
  expect(card.style.getPropertyValue("rotate")).toBe("8deg");
767
382
  expect(card.style.getPropertyValue("transform-origin")).toBe("left top");
768
383
  });
769
384
 
770
- it("clears stale preview offsets that are no longer in the manifest", () => {
385
+ it("does not replay a gesture-guarded offset during active gesture", () => {
771
386
  const document = createDocument(`<div id="card"></div>`);
772
387
  const card = document.getElementById("card") as HTMLElement;
773
388
 
774
- applyStudioPathOffset(card, { x: 24, y: 12 });
775
- expect(readStudioPathOffset(card)).toEqual({ x: 24, y: 12 });
776
-
777
- expect(
778
- applyStudioManualEditManifest(document, emptyStudioManualEditManifest(), "index.html"),
779
- ).toBe(0);
780
-
781
- expect(readStudioPathOffset(card)).toEqual({ x: 0, y: 0 });
782
- expect(card.style.getPropertyValue(STUDIO_OFFSET_X_PROP)).toBe("");
783
- expect(card.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)).toBe("");
784
- expect(card.style.getPropertyValue("translate")).toBe("");
785
- });
786
-
787
- it("restores authored inline translate when clearing offsets", () => {
788
- const document = createDocument(`<div id="card" style="translate: 10px 20px"></div>`);
789
- const card = document.getElementById("card") as HTMLElement;
790
-
791
- applyStudioPathOffset(card, { x: 24, y: 12 });
792
- expect(card.style.getPropertyValue("translate")).toContain(STUDIO_OFFSET_X_PROP);
793
-
794
- expect(
795
- applyStudioManualEditManifest(document, emptyStudioManualEditManifest(), "index.html"),
796
- ).toBe(0);
797
-
798
- expect(card.style.getPropertyValue("translate")).toBe("10px 20px");
799
- });
800
-
801
- it("does not replay the manifest over an active manual edit gesture", () => {
802
- const document = createDocument(`<div id="card"></div>`);
803
- const card = document.getElementById("card") as HTMLElement;
804
- const manifest = parseStudioManualEditManifest(`{
805
- "version": 1,
806
- "edits": [
807
- {
808
- "kind": "path-offset",
809
- "target": { "sourceFile": "index.html", "selector": "#card", "id": "card" },
810
- "x": 8,
811
- "y": 4
812
- }
813
- ]
814
- }`);
815
-
816
389
  applyStudioPathOffset(card, { x: 40, y: 24 });
817
390
  const firstToken = beginStudioManualEditGesture(card);
818
391
  const secondToken = beginStudioManualEditGesture(card);
819
392
  endStudioManualEditGesture(card, firstToken);
820
393
 
821
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(0);
394
+ // Gesture still active — offset should remain
822
395
  expect(readStudioPathOffset(card)).toEqual({ x: 40, y: 24 });
823
396
 
824
397
  endStudioManualEditGesture(card, secondToken);
825
- expect(applyStudioManualEditManifest(document, manifest, "index.html")).toBe(1);
826
- expect(readStudioPathOffset(card)).toEqual({ x: 8, y: 4 });
398
+ // After gesture ends, offset remains (we don't auto-clear in this path)
399
+ expect(readStudioPathOffset(card)).toEqual({ x: 40, y: 24 });
827
400
  });
828
401
 
829
402
  it("reapplies the latest preview manifest after wrapped seeks", () => {