@hyperframes/studio 0.6.0-alpha.9 → 0.6.1

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 (111) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-hYc4aP7M.js +117 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +421 -4303
  8. package/src/captions/components/CaptionOverlay.tsx +13 -246
  9. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  10. package/src/components/AskAgentModal.tsx +120 -0
  11. package/src/components/StudioHeader.tsx +133 -0
  12. package/src/components/StudioLeftSidebar.tsx +125 -0
  13. package/src/components/StudioPreviewArea.tsx +167 -0
  14. package/src/components/StudioRightPanel.tsx +198 -0
  15. package/src/components/TimelineToolbar.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +88 -993
  17. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  18. package/src/components/editor/FileTree.tsx +13 -621
  19. package/src/components/editor/FileTreeIcons.tsx +128 -0
  20. package/src/components/editor/FileTreeNodes.tsx +496 -0
  21. package/src/components/editor/MotionPanel.tsx +16 -390
  22. package/src/components/editor/MotionPanelFields.tsx +185 -0
  23. package/src/components/editor/PropertyPanel.test.ts +0 -49
  24. package/src/components/editor/PropertyPanel.tsx +132 -2763
  25. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  26. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  27. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  28. package/src/components/editor/domEditing.ts +44 -1117
  29. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  30. package/src/components/editor/domEditingDom.ts +266 -0
  31. package/src/components/editor/domEditingElement.ts +329 -0
  32. package/src/components/editor/domEditingLayers.ts +460 -0
  33. package/src/components/editor/domEditingTypes.ts +125 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  35. package/src/components/editor/manualEditingAvailability.ts +1 -1
  36. package/src/components/editor/manualEdits.ts +84 -1049
  37. package/src/components/editor/manualEditsDom.ts +436 -0
  38. package/src/components/editor/manualEditsParsing.ts +280 -0
  39. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  40. package/src/components/editor/manualEditsTypes.ts +141 -0
  41. package/src/components/editor/propertyPanelColor.tsx +371 -0
  42. package/src/components/editor/propertyPanelFill.tsx +421 -0
  43. package/src/components/editor/propertyPanelFont.tsx +455 -0
  44. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  45. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  46. package/src/components/editor/propertyPanelSections.tsx +453 -0
  47. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  48. package/src/components/editor/studioMotion.ts +47 -434
  49. package/src/components/editor/studioMotionOps.ts +299 -0
  50. package/src/components/editor/studioMotionTypes.ts +168 -0
  51. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  52. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  53. package/src/components/nle/NLELayout.tsx +68 -155
  54. package/src/components/nle/NLEPreview.tsx +3 -0
  55. package/src/components/nle/useCompositionStack.ts +126 -0
  56. package/src/components/renders/RenderQueue.tsx +102 -31
  57. package/src/components/renders/useRenderQueue.ts +8 -2
  58. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  59. package/src/contexts/DomEditContext.tsx +137 -0
  60. package/src/contexts/FileManagerContext.tsx +110 -0
  61. package/src/contexts/PanelLayoutContext.tsx +68 -0
  62. package/src/contexts/StudioContext.tsx +135 -0
  63. package/src/hooks/useAppHotkeys.ts +326 -0
  64. package/src/hooks/useAskAgentModal.ts +162 -0
  65. package/src/hooks/useCaptionDetection.ts +132 -0
  66. package/src/hooks/useCompositionDimensions.ts +25 -0
  67. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  68. package/src/hooks/useDomEditCommits.ts +437 -0
  69. package/src/hooks/useDomEditSession.ts +342 -0
  70. package/src/hooks/useDomEditTextCommits.ts +330 -0
  71. package/src/hooks/useDomSelection.ts +398 -0
  72. package/src/hooks/useFileManager.ts +431 -0
  73. package/src/hooks/useFrameCapture.ts +77 -0
  74. package/src/hooks/useLintModal.ts +35 -0
  75. package/src/hooks/useManifestPersistence.ts +492 -0
  76. package/src/hooks/usePanelLayout.ts +68 -0
  77. package/src/hooks/usePreviewInteraction.ts +153 -0
  78. package/src/hooks/useRenderClipContent.ts +124 -0
  79. package/src/hooks/useTimelineEditing.ts +472 -0
  80. package/src/hooks/useToast.ts +20 -0
  81. package/src/player/components/Player.tsx +33 -2
  82. package/src/player/components/Timeline.test.ts +0 -8
  83. package/src/player/components/Timeline.tsx +196 -1518
  84. package/src/player/components/TimelineCanvas.tsx +434 -0
  85. package/src/player/components/TimelineClip.tsx +9 -244
  86. package/src/player/components/TimelineEmptyState.tsx +102 -0
  87. package/src/player/components/TimelineRuler.tsx +90 -0
  88. package/src/player/components/timelineIcons.tsx +49 -0
  89. package/src/player/components/timelineLayout.ts +215 -0
  90. package/src/player/components/timelineUtils.ts +211 -0
  91. package/src/player/components/useTimelineClipDrag.ts +388 -0
  92. package/src/player/components/useTimelinePlayhead.ts +200 -0
  93. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  94. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  95. package/src/player/hooks/useTimelinePlayer.ts +105 -1371
  96. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  97. package/src/player/lib/playbackAdapter.ts +145 -0
  98. package/src/player/lib/playbackShortcuts.ts +68 -0
  99. package/src/player/lib/playbackTypes.ts +60 -0
  100. package/src/player/lib/timelineDOM.ts +373 -0
  101. package/src/player/lib/timelineElementHelpers.ts +303 -0
  102. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  103. package/src/utils/domEditHelpers.ts +50 -0
  104. package/src/utils/studioFontHelpers.ts +83 -0
  105. package/src/utils/studioHelpers.ts +214 -0
  106. package/src/utils/studioPreviewHelpers.ts +185 -0
  107. package/src/utils/timelineDiscovery.ts +1 -1
  108. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  109. package/dist/assets/index-14zH9lqh.css +0 -1
  110. package/dist/assets/index-DYCiFGWQ.js +0 -108
  111. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,436 @@
1
+ import {
2
+ STUDIO_OFFSET_X_PROP,
3
+ STUDIO_OFFSET_Y_PROP,
4
+ STUDIO_WIDTH_PROP,
5
+ STUDIO_HEIGHT_PROP,
6
+ STUDIO_ROTATION_PROP,
7
+ STUDIO_PATH_OFFSET_ATTR,
8
+ STUDIO_MANUAL_EDIT_GESTURE_ATTR,
9
+ STUDIO_BOX_SIZE_ATTR,
10
+ STUDIO_ROTATION_ATTR,
11
+ STUDIO_ORIGINAL_TRANSLATE_ATTR,
12
+ STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
13
+ STUDIO_ORIGINAL_WIDTH_ATTR,
14
+ STUDIO_ORIGINAL_HEIGHT_ATTR,
15
+ STUDIO_ORIGINAL_MIN_WIDTH_ATTR,
16
+ STUDIO_ORIGINAL_MIN_HEIGHT_ATTR,
17
+ STUDIO_ORIGINAL_MAX_WIDTH_ATTR,
18
+ STUDIO_ORIGINAL_MAX_HEIGHT_ATTR,
19
+ STUDIO_ORIGINAL_FLEX_BASIS_ATTR,
20
+ STUDIO_ORIGINAL_FLEX_GROW_ATTR,
21
+ STUDIO_ORIGINAL_FLEX_SHRINK_ATTR,
22
+ STUDIO_ORIGINAL_BOX_SIZING_ATTR,
23
+ STUDIO_ORIGINAL_SCALE_ATTR,
24
+ STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR,
25
+ STUDIO_ORIGINAL_DISPLAY_ATTR,
26
+ STUDIO_ORIGINAL_ROTATE_ATTR,
27
+ STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
28
+ STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
29
+ STUDIO_ROTATION_DRAFT_ATTR,
30
+ STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
31
+ STUDIO_ROTATION_TRANSFORM_ORIGIN,
32
+ } from "./manualEditsTypes";
33
+ import { roundRotationAngle } from "./manualEditsParsing";
34
+
35
+ /* ── Gesture tracking ─────────────────────────────────────────────── */
36
+ let studioManualEditGestureId = 0;
37
+
38
+ export function beginStudioManualEditGesture(element: HTMLElement): string {
39
+ studioManualEditGestureId += 1;
40
+ const token = `gesture-${studioManualEditGestureId}`;
41
+ element.setAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR, token);
42
+ return token;
43
+ }
44
+
45
+ export function endStudioManualEditGesture(element: HTMLElement, token?: string): void {
46
+ if (token && element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) !== token) return;
47
+ element.removeAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
48
+ }
49
+
50
+ export function isStudioManualEditGestureActive(element: HTMLElement): boolean {
51
+ return element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
52
+ }
53
+
54
+ export function isStudioManualEditGestureCurrent(element: HTMLElement, token: string): boolean {
55
+ return element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) === token;
56
+ }
57
+
58
+ /* ── CSS custom-property readers ──────────────────────────────────── */
59
+ function readPxCustomProperty(element: HTMLElement, property: string): number {
60
+ const value = Number.parseFloat(element.style.getPropertyValue(property));
61
+ return Number.isFinite(value) ? value : 0;
62
+ }
63
+
64
+ export function readStudioPathOffset(element: HTMLElement): { x: number; y: number } {
65
+ return {
66
+ x: readPxCustomProperty(element, STUDIO_OFFSET_X_PROP),
67
+ y: readPxCustomProperty(element, STUDIO_OFFSET_Y_PROP),
68
+ };
69
+ }
70
+
71
+ export function readStudioBoxSize(element: HTMLElement): { width: number; height: number } {
72
+ return {
73
+ width: readPxCustomProperty(element, STUDIO_WIDTH_PROP),
74
+ height: readPxCustomProperty(element, STUDIO_HEIGHT_PROP),
75
+ };
76
+ }
77
+
78
+ export function readStudioRotation(element: HTMLElement): { angle: number } {
79
+ const value = Number.parseFloat(element.style.getPropertyValue(STUDIO_ROTATION_PROP));
80
+ return { angle: Number.isFinite(value) ? value : 0 };
81
+ }
82
+
83
+ /* ── Internal style helpers ───────────────────────────────────────── */
84
+ function safeComputedStyleProperty(element: HTMLElement, property: string): string {
85
+ try {
86
+ return (
87
+ element.ownerDocument.defaultView?.getComputedStyle(element).getPropertyValue(property) ?? ""
88
+ );
89
+ } catch {
90
+ return "";
91
+ }
92
+ }
93
+
94
+ function readStyleOrComputed(element: HTMLElement, property: string): string {
95
+ return element.style.getPropertyValue(property) || safeComputedStyleProperty(element, property);
96
+ }
97
+
98
+ function readTransformLonghandBase(element: HTMLElement, property: "translate" | "rotate"): string {
99
+ const value = readStyleOrComputed(element, property).trim();
100
+ return value === "none" ? "" : value;
101
+ }
102
+
103
+ export function styleUsesStudioOffset(value: string): boolean {
104
+ return value.includes(STUDIO_OFFSET_X_PROP) || value.includes(STUDIO_OFFSET_Y_PROP);
105
+ }
106
+
107
+ export function styleUsesStudioSize(value: string): boolean {
108
+ return value.includes(STUDIO_WIDTH_PROP) || value.includes(STUDIO_HEIGHT_PROP);
109
+ }
110
+
111
+ export function styleUsesStudioRotation(value: string): boolean {
112
+ return value.includes(STUDIO_ROTATION_PROP);
113
+ }
114
+
115
+ function compactStyleValue(value: string): string {
116
+ return value.replace(/\s+/g, "").toLowerCase();
117
+ }
118
+
119
+ function styleMatchesStudioRotationDraft(element: HTMLElement, value: string): boolean {
120
+ if (!element.hasAttribute(STUDIO_ROTATION_DRAFT_ATTR)) return false;
121
+ const rotation = element.style.getPropertyValue(STUDIO_ROTATION_PROP).trim();
122
+ if (!rotation || !value.trim()) return false;
123
+ return (
124
+ compactStyleValue(value) === compactStyleValue(composeStudioRotationValue(element, rotation))
125
+ );
126
+ }
127
+
128
+ /* ── Inline promotion ─────────────────────────────────────────────── */
129
+ function promoteInlineForTransform(element: HTMLElement): void {
130
+ const computedDisplay = safeComputedStyleProperty(element, "display");
131
+ if (computedDisplay !== "inline") return;
132
+ if (!element.hasAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR)) {
133
+ element.setAttribute(
134
+ STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
135
+ element.style.getPropertyValue("display"),
136
+ );
137
+ }
138
+ element.style.setProperty("display", "inline-block");
139
+ }
140
+
141
+ export function restoreInlineDisplay(element: HTMLElement): void {
142
+ const original = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
143
+ if (original == null) return;
144
+ if (original === "") element.style.removeProperty("display");
145
+ else element.style.setProperty("display", original);
146
+ element.removeAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
147
+ }
148
+
149
+ /* ── Translate helpers ────────────────────────────────────────────── */
150
+ function splitTopLevelWhitespace(value: string): string[] {
151
+ const parts: string[] = [];
152
+ let depth = 0;
153
+ let current = "";
154
+ for (const char of value.trim()) {
155
+ if (char === "(") depth += 1;
156
+ if (char === ")") depth = Math.max(0, depth - 1);
157
+ if (/\s/.test(char) && depth === 0) {
158
+ if (current) parts.push(current);
159
+ current = "";
160
+ } else {
161
+ current += char;
162
+ }
163
+ }
164
+ if (current) parts.push(current);
165
+ return parts;
166
+ }
167
+
168
+ function composeTranslateValue(element: HTMLElement, x: string, y: string): string {
169
+ const original = element.getAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR)?.trim();
170
+ if (!original || original === "none") return `${x} ${y}`;
171
+
172
+ const parts = splitTopLevelWhitespace(original);
173
+ if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`;
174
+ if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`;
175
+ if (parts.length === 3) {
176
+ return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`;
177
+ }
178
+ return `${x} ${y}`;
179
+ }
180
+
181
+ function prepareStudioPathOffsetBase(element: HTMLElement, updateBase: boolean): void {
182
+ const inlineTranslate = element.style.getPropertyValue("translate");
183
+ const currentTranslate = readTransformLonghandBase(element, "translate");
184
+ const hasMarker = element.hasAttribute(STUDIO_PATH_OFFSET_ATTR);
185
+ const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate);
186
+ if (!hasMarker) {
187
+ element.setAttribute(
188
+ STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
189
+ styleUsesStudioOffset(inlineTranslate) ? "" : inlineTranslate,
190
+ );
191
+ element.setAttribute(
192
+ STUDIO_ORIGINAL_TRANSLATE_ATTR,
193
+ wasResetByAnimation ? currentTranslate : "",
194
+ );
195
+ } else if (updateBase && wasResetByAnimation && !isStudioManualEditGestureActive(element)) {
196
+ element.setAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR, currentTranslate);
197
+ }
198
+ }
199
+
200
+ function writeStudioPathOffsetVars(
201
+ element: HTMLElement,
202
+ offset: { x: number; y: number },
203
+ options: { updateBase?: boolean } = {},
204
+ ): void {
205
+ prepareStudioPathOffsetBase(element, options.updateBase ?? true);
206
+ element.setAttribute(STUDIO_PATH_OFFSET_ATTR, "true");
207
+ element.style.setProperty(STUDIO_OFFSET_X_PROP, `${Math.round(offset.x)}px`);
208
+ element.style.setProperty(STUDIO_OFFSET_Y_PROP, `${Math.round(offset.y)}px`);
209
+ }
210
+
211
+ /* ── Path offset apply ────────────────────────────────────────────── */
212
+ export function applyStudioPathOffset(
213
+ element: HTMLElement,
214
+ offset: { x: number; y: number },
215
+ ): void {
216
+ promoteInlineForTransform(element);
217
+ writeStudioPathOffsetVars(element, offset);
218
+ element.style.setProperty(
219
+ "translate",
220
+ composeTranslateValue(
221
+ element,
222
+ `var(${STUDIO_OFFSET_X_PROP}, 0px)`,
223
+ `var(${STUDIO_OFFSET_Y_PROP}, 0px)`,
224
+ ),
225
+ );
226
+ }
227
+
228
+ export function applyStudioPathOffsetDraft(
229
+ element: HTMLElement,
230
+ offset: { x: number; y: number },
231
+ ): void {
232
+ promoteInlineForTransform(element);
233
+ writeStudioPathOffsetVars(element, offset, { updateBase: false });
234
+ element.style.setProperty(
235
+ "translate",
236
+ composeTranslateValue(element, `${Math.round(offset.x)}px`, `${Math.round(offset.y)}px`),
237
+ );
238
+ }
239
+
240
+ /* ── Box size apply ───────────────────────────────────────────────── */
241
+ function readParentFlexBasisPixels(
242
+ element: HTMLElement,
243
+ size: { width: number; height: number },
244
+ ): number | null {
245
+ const parent = element.parentElement;
246
+ if (!parent) return null;
247
+
248
+ const display = readStyleOrComputed(parent, "display").trim();
249
+ if (display !== "flex" && display !== "inline-flex") return null;
250
+
251
+ const direction = readStyleOrComputed(parent, "flex-direction").trim();
252
+ return Math.round(Math.max(1, direction.startsWith("column") ? size.height : size.width));
253
+ }
254
+
255
+ function restoreStaleStudioScaleResize(element: HTMLElement): void {
256
+ if (!element.hasAttribute(STUDIO_ORIGINAL_SCALE_ATTR)) return;
257
+ const origScale = element.getAttribute(STUDIO_ORIGINAL_SCALE_ATTR);
258
+ if (origScale == null || origScale === "") element.style.removeProperty("scale");
259
+ else element.style.setProperty("scale", origScale);
260
+ element.removeAttribute(STUDIO_ORIGINAL_SCALE_ATTR);
261
+ const origOrigin = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR);
262
+ if (origOrigin == null || origOrigin === "") element.style.removeProperty("transform-origin");
263
+ else element.style.setProperty("transform-origin", origOrigin);
264
+ element.removeAttribute(STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR);
265
+ }
266
+
267
+ function writeStudioBoxSizeVars(
268
+ element: HTMLElement,
269
+ size: { width: number; height: number },
270
+ ): void {
271
+ if (!element.hasAttribute(STUDIO_BOX_SIZE_ATTR)) {
272
+ element.setAttribute(STUDIO_ORIGINAL_WIDTH_ATTR, element.style.getPropertyValue("width"));
273
+ element.setAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR, element.style.getPropertyValue("height"));
274
+ element.setAttribute(
275
+ STUDIO_ORIGINAL_MIN_WIDTH_ATTR,
276
+ element.style.getPropertyValue("min-width"),
277
+ );
278
+ element.setAttribute(
279
+ STUDIO_ORIGINAL_MIN_HEIGHT_ATTR,
280
+ element.style.getPropertyValue("min-height"),
281
+ );
282
+ element.setAttribute(
283
+ STUDIO_ORIGINAL_MAX_WIDTH_ATTR,
284
+ element.style.getPropertyValue("max-width"),
285
+ );
286
+ element.setAttribute(
287
+ STUDIO_ORIGINAL_MAX_HEIGHT_ATTR,
288
+ element.style.getPropertyValue("max-height"),
289
+ );
290
+ element.setAttribute(
291
+ STUDIO_ORIGINAL_FLEX_BASIS_ATTR,
292
+ element.style.getPropertyValue("flex-basis"),
293
+ );
294
+ element.setAttribute(
295
+ STUDIO_ORIGINAL_FLEX_GROW_ATTR,
296
+ element.style.getPropertyValue("flex-grow"),
297
+ );
298
+ element.setAttribute(
299
+ STUDIO_ORIGINAL_FLEX_SHRINK_ATTR,
300
+ element.style.getPropertyValue("flex-shrink"),
301
+ );
302
+ element.setAttribute(
303
+ STUDIO_ORIGINAL_BOX_SIZING_ATTR,
304
+ element.style.getPropertyValue("box-sizing"),
305
+ );
306
+ element.setAttribute(STUDIO_ORIGINAL_SCALE_ATTR, element.style.getPropertyValue("scale"));
307
+ element.setAttribute(
308
+ STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR,
309
+ element.style.getPropertyValue("transform-origin"),
310
+ );
311
+ element.setAttribute(STUDIO_ORIGINAL_DISPLAY_ATTR, element.style.getPropertyValue("display"));
312
+ }
313
+
314
+ element.setAttribute(STUDIO_BOX_SIZE_ATTR, "true");
315
+ element.style.setProperty(STUDIO_WIDTH_PROP, `${Math.round(Math.max(1, size.width))}px`);
316
+ element.style.setProperty(STUDIO_HEIGHT_PROP, `${Math.round(Math.max(1, size.height))}px`);
317
+ }
318
+
319
+ function applyStudioBoxSizeDimensions(
320
+ element: HTMLElement,
321
+ size: { width: number; height: number },
322
+ ): void {
323
+ writeStudioBoxSizeVars(element, size);
324
+ restoreStaleStudioScaleResize(element);
325
+
326
+ const width = Math.round(Math.max(1, size.width));
327
+ const height = Math.round(Math.max(1, size.height));
328
+ element.style.setProperty("box-sizing", "border-box");
329
+ element.style.setProperty("width", `${width}px`);
330
+ element.style.setProperty("height", `${height}px`);
331
+ element.style.setProperty("min-width", "0px");
332
+ element.style.setProperty("min-height", "0px");
333
+ element.style.setProperty("max-width", "none");
334
+ element.style.setProperty("max-height", "none");
335
+ const flexBasis = readParentFlexBasisPixels(element, size);
336
+ if (flexBasis != null) {
337
+ element.style.setProperty("flex-basis", `${flexBasis}px`);
338
+ element.style.setProperty("flex-grow", "0");
339
+ element.style.setProperty("flex-shrink", "0");
340
+ }
341
+ const computedDisplay = safeComputedStyleProperty(element, "display");
342
+ if (computedDisplay === "inline") {
343
+ element.style.setProperty("display", "inline-block");
344
+ }
345
+ }
346
+
347
+ export function applyStudioBoxSize(
348
+ element: HTMLElement,
349
+ size: { width: number; height: number },
350
+ ): void {
351
+ promoteInlineForTransform(element);
352
+ applyStudioBoxSizeDimensions(element, size);
353
+ }
354
+
355
+ export function applyStudioBoxSizeDraft(
356
+ element: HTMLElement,
357
+ size: { width: number; height: number },
358
+ ): void {
359
+ promoteInlineForTransform(element);
360
+ applyStudioBoxSizeDimensions(element, size);
361
+ }
362
+
363
+ /* ── Rotation apply ───────────────────────────────────────────────── */
364
+ function isSimpleRotateAngle(value: string): boolean {
365
+ return /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
366
+ }
367
+
368
+ function composeStudioRotationValue(element: HTMLElement, rotationValue: string): string {
369
+ const original = element.getAttribute(STUDIO_ORIGINAL_ROTATE_ATTR)?.trim();
370
+ if (!original || original === "none" || !isSimpleRotateAngle(original)) {
371
+ return rotationValue;
372
+ }
373
+ return `calc(${original} + ${rotationValue})`;
374
+ }
375
+
376
+ function prepareStudioRotationBase(element: HTMLElement, updateBase: boolean): void {
377
+ const inlineRotate = element.style.getPropertyValue("rotate");
378
+ const currentRotate = readTransformLonghandBase(element, "rotate");
379
+ const hasMarker = element.hasAttribute(STUDIO_ROTATION_ATTR);
380
+ const wasResetByAnimation =
381
+ !styleUsesStudioRotation(currentRotate) &&
382
+ !styleMatchesStudioRotationDraft(element, currentRotate);
383
+ if (!hasMarker) {
384
+ element.setAttribute(
385
+ STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
386
+ styleUsesStudioRotation(inlineRotate) ? "" : inlineRotate,
387
+ );
388
+ element.setAttribute(STUDIO_ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : "");
389
+ } else if (updateBase && wasResetByAnimation && !isStudioManualEditGestureActive(element)) {
390
+ element.setAttribute(STUDIO_ORIGINAL_ROTATE_ATTR, currentRotate);
391
+ }
392
+ if (!element.hasAttribute(STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR)) {
393
+ element.setAttribute(
394
+ STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
395
+ element.style.getPropertyValue("transform-origin"),
396
+ );
397
+ }
398
+ }
399
+
400
+ function writeStudioRotationVars(
401
+ element: HTMLElement,
402
+ rotation: { angle: number },
403
+ options: { updateBase?: boolean } = {},
404
+ ): void {
405
+ prepareStudioRotationBase(element, options.updateBase ?? true);
406
+ element.setAttribute(STUDIO_ROTATION_ATTR, "true");
407
+ element.style.setProperty(STUDIO_ROTATION_PROP, `${roundRotationAngle(rotation.angle)}deg`);
408
+ element.style.setProperty("transform-origin", STUDIO_ROTATION_TRANSFORM_ORIGIN);
409
+ }
410
+
411
+ export function applyStudioRotation(element: HTMLElement, rotation: { angle: number }): void {
412
+ promoteInlineForTransform(element);
413
+ writeStudioRotationVars(element, rotation);
414
+ element.removeAttribute(STUDIO_ROTATION_DRAFT_ATTR);
415
+ element.style.setProperty(
416
+ "rotate",
417
+ composeStudioRotationValue(element, `var(${STUDIO_ROTATION_PROP}, 0deg)`),
418
+ );
419
+ }
420
+
421
+ export function applyStudioRotationDraft(element: HTMLElement, rotation: { angle: number }): void {
422
+ promoteInlineForTransform(element);
423
+ writeStudioRotationVars(element, rotation, { updateBase: false });
424
+ element.setAttribute(STUDIO_ROTATION_DRAFT_ATTR, "true");
425
+ element.style.setProperty(
426
+ "rotate",
427
+ composeStudioRotationValue(element, `${roundRotationAngle(rotation.angle)}deg`),
428
+ );
429
+ }
430
+
431
+ // Clear functions live in manualEditsSnapshot.ts (they depend on restoreInline* helpers).
432
+ export {
433
+ clearStudioPathOffset,
434
+ clearStudioRotation,
435
+ clearStudioBoxSize,
436
+ } from "./manualEditsSnapshot";