@hyperframes/studio 0.6.49 → 0.6.51

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.
package/dist/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-B4Cr7MVx.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Bvy50smZ.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-SKRp8mGz.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.49",
3
+ "version": "0.6.51",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,8 +31,8 @@
31
31
  "@codemirror/view": "6.40.0",
32
32
  "@phosphor-icons/react": "^2.1.10",
33
33
  "mediabunny": "^1.45.3",
34
- "@hyperframes/core": "0.6.49",
35
- "@hyperframes/player": "0.6.49"
34
+ "@hyperframes/player": "0.6.51",
35
+ "@hyperframes/core": "0.6.51"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/react": "19",
@@ -46,7 +46,7 @@
46
46
  "vite": "^6.4.2",
47
47
  "vitest": "^3.2.4",
48
48
  "zustand": "^5.0.0",
49
- "@hyperframes/producer": "0.6.49"
49
+ "@hyperframes/producer": "0.6.51"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": "19",
@@ -1,7 +1,12 @@
1
1
  import { memo } from "react";
2
2
  import { Clock, Eye, Layers, MessageSquare, Move, X } from "../../icons/SystemIcons";
3
3
  import { type DomEditSelection } from "./domEditing";
4
- import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
4
+ import {
5
+ readStudioBoxSize,
6
+ readStudioPathOffset,
7
+ readStudioRotation,
8
+ readGsapTranslateFromTransform,
9
+ } from "./manualEdits";
5
10
  import type { ImportedFontAsset } from "./fontAssets";
6
11
  import {
7
12
  EMPTY_STYLES,
@@ -181,6 +186,11 @@ export const PropertyPanel = memo(function PropertyPanel({
181
186
  const sourceLabel = element.id ? `#${element.id}` : element.selector;
182
187
  const showEditableSections = element.capabilities.canEditStyles;
183
188
  const manualOffset = readStudioPathOffset(element.element);
189
+ const gsapTranslate = readGsapTranslateFromTransform(element.element);
190
+ const visualOffset = {
191
+ x: manualOffset.x + gsapTranslate.x,
192
+ y: manualOffset.y + gsapTranslate.y,
193
+ };
184
194
  const manualSize = readStudioBoxSize(element.element);
185
195
  const resolvedWidth =
186
196
  manualSize.width > 0
@@ -194,10 +204,11 @@ export const PropertyPanel = memo(function PropertyPanel({
194
204
  const commitManualOffset = (axis: "x" | "y", nextValue: string) => {
195
205
  const parsed = parsePxMetricValue(nextValue);
196
206
  if (parsed == null) return;
197
- const current = readStudioPathOffset(element.element);
207
+ const currentRaw = readStudioPathOffset(element.element);
208
+ const currentGsap = readGsapTranslateFromTransform(element.element);
198
209
  onSetManualOffset(element, {
199
- x: axis === "x" ? parsed : current.x,
200
- y: axis === "y" ? parsed : current.y,
210
+ x: axis === "x" ? parsed - currentGsap.x : currentRaw.x,
211
+ y: axis === "y" ? parsed - currentGsap.y : currentRaw.y,
201
212
  });
202
213
  };
203
214
 
@@ -289,14 +300,14 @@ export const PropertyPanel = memo(function PropertyPanel({
289
300
  <div className={RESPONSIVE_GRID}>
290
301
  <MetricField
291
302
  label="X"
292
- value={formatPxMetricValue(manualOffset.x)}
303
+ value={formatPxMetricValue(visualOffset.x)}
293
304
  disabled={manualOffsetEditingDisabled}
294
305
  scrub
295
306
  onCommit={(next) => commitManualOffset("x", next)}
296
307
  />
297
308
  <MetricField
298
309
  label="Y"
299
- value={formatPxMetricValue(manualOffset.y)}
310
+ value={formatPxMetricValue(visualOffset.y)}
300
311
  disabled={manualOffsetEditingDisabled}
301
312
  scrub
302
313
  onCommit={(next) => commitManualOffset("y", next)}
@@ -20,6 +20,7 @@ export {
20
20
  readStudioPathOffset,
21
21
  readStudioBoxSize,
22
22
  readStudioRotation,
23
+ readGsapTranslateFromTransform,
23
24
  applyStudioPathOffset,
24
25
  applyStudioPathOffsetDraft,
25
26
  applyStudioBoxSize,
@@ -219,6 +219,20 @@ function isIdentityAfterTranslateStrip(m: DOMMatrix): boolean {
219
219
  return m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1;
220
220
  }
221
221
 
222
+ export function readGsapTranslateFromTransform(element: HTMLElement): { x: number; y: number } {
223
+ const transform = element.style.getPropertyValue("transform");
224
+ if (!transform || transform === "none") return { x: 0, y: 0 };
225
+ const DOMMatrixCtor = (element.ownerDocument.defaultView as (Window & typeof globalThis) | null)
226
+ ?.DOMMatrix;
227
+ if (!DOMMatrixCtor) return { x: 0, y: 0 };
228
+ try {
229
+ const m = new DOMMatrixCtor(transform);
230
+ return { x: m.m41, y: m.m42 };
231
+ } catch {
232
+ return { x: 0, y: 0 };
233
+ }
234
+ }
235
+
222
236
  function stripGsapTranslateFromTransform(element: HTMLElement): void {
223
237
  const transform = element.style.getPropertyValue("transform");
224
238
  if (!transform || transform === "none") return;
@@ -2,6 +2,7 @@ import { Window } from "happy-dom";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import {
4
4
  applyManualOffsetDragMatrix,
5
+ createManualOffsetDragMember,
5
6
  invertManualOffsetDragMatrix,
6
7
  measureManualOffsetDragScreenToOffsetMatrix,
7
8
  resolveManualOffsetForPointerDelta,
@@ -138,3 +139,83 @@ describe("measureManualOffsetDragScreenToOffsetMatrix", () => {
138
139
  expect(measured.ok).toBe(false);
139
140
  });
140
141
  });
142
+
143
+ describe("createManualOffsetDragMember GSAP translate compensation", () => {
144
+ it("folds GSAP translate from element.style.transform into initialOffset", () => {
145
+ const window = new Window();
146
+ const element = window.document.createElement("div");
147
+ window.document.body.append(element);
148
+
149
+ element.style.setProperty("transform", "translate(0px, -20px)");
150
+
151
+ element.getBoundingClientRect = () => {
152
+ const offsetX = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
153
+ const offsetY = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)) || 0;
154
+ return new window.DOMRect(10 + offsetX, 20 + offsetY, 100, 50);
155
+ };
156
+
157
+ const result = createManualOffsetDragMember({
158
+ key: "test",
159
+ selection: { element } as never,
160
+ element,
161
+ rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
162
+ });
163
+
164
+ expect(result.ok).toBe(true);
165
+ if (!result.ok) return;
166
+ expect(result.member.initialOffset.x).toBe(0);
167
+ expect(result.member.initialOffset.y).toBe(-20);
168
+ });
169
+
170
+ it("leaves initialOffset unchanged when no GSAP transform is present", () => {
171
+ const window = new Window();
172
+ const element = window.document.createElement("div");
173
+ window.document.body.append(element);
174
+
175
+ element.getBoundingClientRect = () => {
176
+ const offsetX = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
177
+ const offsetY = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)) || 0;
178
+ return new window.DOMRect(10 + offsetX, 20 + offsetY, 100, 50);
179
+ };
180
+
181
+ const result = createManualOffsetDragMember({
182
+ key: "test",
183
+ selection: { element } as never,
184
+ element,
185
+ rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
186
+ });
187
+
188
+ expect(result.ok).toBe(true);
189
+ if (!result.ok) return;
190
+ expect(result.member.initialOffset.x).toBe(0);
191
+ expect(result.member.initialOffset.y).toBe(0);
192
+ });
193
+
194
+ it("combines existing manual offset with GSAP translate", () => {
195
+ const window = new Window();
196
+ const element = window.document.createElement("div");
197
+ window.document.body.append(element);
198
+
199
+ element.style.setProperty(STUDIO_OFFSET_X_PROP, "30px");
200
+ element.style.setProperty(STUDIO_OFFSET_Y_PROP, "10px");
201
+ element.style.setProperty("transform", "translate(50px, -15px)");
202
+
203
+ element.getBoundingClientRect = () => {
204
+ const offsetX = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || 0;
205
+ const offsetY = Number.parseFloat(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)) || 0;
206
+ return new window.DOMRect(10 + offsetX, 20 + offsetY, 100, 50);
207
+ };
208
+
209
+ const result = createManualOffsetDragMember({
210
+ key: "test",
211
+ selection: { element } as never,
212
+ element,
213
+ rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
214
+ });
215
+
216
+ expect(result.ok).toBe(true);
217
+ if (!result.ok) return;
218
+ expect(result.member.initialOffset.x).toBe(80);
219
+ expect(result.member.initialOffset.y).toBe(-5);
220
+ });
221
+ });
@@ -5,6 +5,7 @@ import {
5
5
  beginStudioManualEditGesture,
6
6
  captureStudioPathOffset,
7
7
  endStudioManualEditGesture,
8
+ readGsapTranslateFromTransform,
8
9
  readStudioPathOffset,
9
10
  restoreStudioPathOffset,
10
11
  type StudioPathOffsetSnapshot,
@@ -231,7 +232,12 @@ export function createManualOffsetDragMember(input: {
231
232
  element: HTMLElement;
232
233
  rect: ManualOffsetDragRect;
233
234
  }): ManualOffsetDragMemberResult {
234
- const initialOffset = readStudioPathOffset(input.element);
235
+ const rawOffset = readStudioPathOffset(input.element);
236
+ const gsapTranslate = readGsapTranslateFromTransform(input.element);
237
+ const initialOffset = {
238
+ x: rawOffset.x + gsapTranslate.x,
239
+ y: rawOffset.y + gsapTranslate.y,
240
+ };
235
241
  const initialPathOffset = captureStudioPathOffset(input.element);
236
242
  const gestureToken = beginStudioManualEditGesture(input.element);
237
243
  const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset);
@@ -155,6 +155,11 @@ export function useDomEditCommits({
155
155
  selectorIndex: selection.selectorIndex,
156
156
  };
157
157
 
158
+ // Mark the save timestamp before the file write so the SSE file-change
159
+ // handler suppresses the reload even if the event arrives before the
160
+ // response (the server writes the file and emits SSE during the fetch).
161
+ domEditSaveTimestampRef.current = Date.now();
162
+
158
163
  const patchResponse = await fetch(
159
164
  `/api/projects/${pid}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
160
165
  {
@@ -193,9 +198,7 @@ export function useDomEditCommits({
193
198
  files: { [targetPath]: { before: originalContent, after: finalContent } },
194
199
  });
195
200
 
196
- if (options?.skipRefresh) {
197
- domEditSaveTimestampRef.current = Date.now();
198
- } else {
201
+ if (!options?.skipRefresh) {
199
202
  reloadPreview();
200
203
  }
201
204
  },
@@ -427,6 +430,7 @@ export function useDomEditCommits({
427
430
  throw new Error("Selected element has no patchable target");
428
431
  }
429
432
 
433
+ domEditSaveTimestampRef.current = Date.now();
430
434
  const removeResponse = await fetch(
431
435
  `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
432
436
  {
@@ -440,8 +444,6 @@ export function useDomEditCommits({
440
444
  const removeData = (await removeResponse.json()) as { changed?: boolean; content?: string };
441
445
  const patchedContent =
442
446
  typeof removeData.content === "string" ? removeData.content : originalContent;
443
-
444
- domEditSaveTimestampRef.current = Date.now();
445
447
  await saveProjectFilesWithHistory({
446
448
  projectId: pid,
447
449
  label: "Delete element",