@hyperframes/studio 0.6.95 → 0.6.97
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/assets/hyperframes-player-Daj5djxa.js +418 -0
- package/dist/assets/index-B0twsRu0.css +1 -0
- package/dist/assets/index-Cfye9xzo.js +251 -0
- package/dist/assets/{index-CAANLw9Q.js → index-HveJ0MuV.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +10 -5
- package/src/components/SaveQueuePausedBanner.tsx +23 -0
- package/src/components/StudioPreviewArea.tsx +7 -0
- package/src/components/StudioRightPanel.tsx +1 -38
- package/src/components/editor/DomEditOverlay.test.ts +169 -29
- package/src/components/editor/DomEditOverlay.tsx +13 -23
- package/src/components/editor/GestureRecordControl.tsx +98 -0
- package/src/components/editor/PropertyPanel.tsx +22 -38
- package/src/components/editor/domEditing.test.ts +84 -0
- package/src/components/editor/domEditingLayers.ts +19 -0
- package/src/components/editor/domEditingRootLayer.ts +64 -0
- package/src/components/editor/manualEditingAvailability.test.ts +1 -2
- package/src/components/editor/manualEditingAvailability.ts +0 -7
- package/src/contexts/DomEditContext.tsx +1 -6
- package/src/hooks/gsapScriptCommitHelpers.ts +128 -0
- package/src/hooks/useDomEditCommits.ts +97 -123
- package/src/hooks/useDomEditPositionPatchCommit.ts +53 -0
- package/src/hooks/useDomEditSession.ts +59 -65
- package/src/hooks/useFileManager.ts +19 -5
- package/src/hooks/useGsapAnimationFetchFallback.ts +19 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +25 -0
- package/src/hooks/useGsapScriptCommits.ts +152 -140
- package/src/hooks/useGsapSelectionHandlers.ts +38 -8
- package/src/hooks/usePreviewPersistence.ts +90 -51
- package/src/hooks/useSafeGsapCommitMutation.ts +66 -0
- package/src/hooks/useStudioContextValue.ts +3 -19
- package/src/player/hooks/useTimelinePlayer.ts +25 -28
- package/src/player/lib/playbackAdapter.test.ts +86 -1
- package/src/player/lib/playbackAdapter.ts +62 -0
- package/src/utils/domEditSaveQueue.test.ts +117 -0
- package/src/utils/domEditSaveQueue.ts +87 -0
- package/src/utils/studioHelpers.ts +1 -1
- package/src/utils/studioSaveDiagnostics.test.ts +127 -0
- package/src/utils/studioSaveDiagnostics.ts +200 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/studioUrlState.ts +2 -8
- package/dist/assets/hyperframes-player-0esDKGRk.js +0 -418
- package/dist/assets/index-DujOjou6.js +0 -251
- package/dist/assets/index-rm9tn9nH.css +0 -1
- package/src/components/editor/EaseCurveEditor.tsx +0 -221
- package/src/components/editor/MotionPanel.tsx +0 -277
- package/src/components/editor/MotionPanelFields.tsx +0 -185
- package/src/components/editor/MotionPathOverlay.tsx +0 -146
- package/src/components/editor/SpringEaseEditor.tsx +0 -256
|
@@ -21,6 +21,7 @@ import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEdi
|
|
|
21
21
|
import { usePlayerStore, liveTime } from "../../player";
|
|
22
22
|
import { TimingSection } from "./propertyPanelTimingSection";
|
|
23
23
|
import { type PropertyPanelProps } from "./propertyPanelHelpers";
|
|
24
|
+
import { GestureRecordPanelButton } from "./GestureRecordControl";
|
|
24
25
|
|
|
25
26
|
// Re-export helpers that external consumers import from this module
|
|
26
27
|
export {
|
|
@@ -34,10 +35,6 @@ export {
|
|
|
34
35
|
setCssFilterFunctionPx,
|
|
35
36
|
} from "./propertyPanelHelpers";
|
|
36
37
|
|
|
37
|
-
/* ------------------------------------------------------------------ */
|
|
38
|
-
/* PropertyPanel */
|
|
39
|
-
/* ------------------------------------------------------------------ */
|
|
40
|
-
|
|
41
38
|
// fallow-ignore-next-line complexity
|
|
42
39
|
export const PropertyPanel = memo(function PropertyPanel({
|
|
43
40
|
projectId,
|
|
@@ -177,10 +174,12 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
177
174
|
return;
|
|
178
175
|
}
|
|
179
176
|
const current = readStudioPathOffset(element.element);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
void Promise.resolve(
|
|
178
|
+
onSetManualOffset(element, {
|
|
179
|
+
x: axis === "x" ? parsed : current.x,
|
|
180
|
+
y: axis === "y" ? parsed : current.y,
|
|
181
|
+
}),
|
|
182
|
+
).catch(() => undefined);
|
|
184
183
|
};
|
|
185
184
|
|
|
186
185
|
// fallow-ignore-next-line complexity
|
|
@@ -204,17 +203,19 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
204
203
|
current.height > 0
|
|
205
204
|
? current.height
|
|
206
205
|
: (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
void Promise.resolve(
|
|
207
|
+
onSetManualSize(element, {
|
|
208
|
+
width: axis === "width" ? parsed : width,
|
|
209
|
+
height: axis === "height" ? parsed : height,
|
|
210
|
+
}),
|
|
211
|
+
).catch(() => undefined);
|
|
211
212
|
};
|
|
212
213
|
|
|
213
214
|
const manualRotation = readStudioRotation(element.element);
|
|
214
215
|
const commitManualRotation = (nextValue: string) => {
|
|
215
216
|
const parsed = Number.parseFloat(nextValue);
|
|
216
217
|
if (!Number.isFinite(parsed)) return;
|
|
217
|
-
onSetManualRotation(element, { angle: parsed });
|
|
218
|
+
void Promise.resolve(onSetManualRotation(element, { angle: parsed })).catch(() => undefined);
|
|
218
219
|
};
|
|
219
220
|
|
|
220
221
|
const elStart = Number.parseFloat(element?.dataAttributes?.start ?? "0") || 0;
|
|
@@ -354,6 +355,14 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
354
355
|
</div>
|
|
355
356
|
</div>
|
|
356
357
|
<div className="flex-1 overflow-y-auto">
|
|
358
|
+
{onToggleRecording && (
|
|
359
|
+
<GestureRecordPanelButton
|
|
360
|
+
recordingState={recordingState}
|
|
361
|
+
recordingDuration={recordingDuration}
|
|
362
|
+
onToggleRecording={onToggleRecording}
|
|
363
|
+
/>
|
|
364
|
+
)}
|
|
365
|
+
|
|
357
366
|
<TextSection
|
|
358
367
|
element={element}
|
|
359
368
|
styles={styles}
|
|
@@ -558,31 +567,6 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
558
567
|
/>
|
|
559
568
|
)}
|
|
560
569
|
|
|
561
|
-
{onToggleRecording && (
|
|
562
|
-
<div className="px-4 pb-3">
|
|
563
|
-
<button
|
|
564
|
-
type="button"
|
|
565
|
-
onMouseDown={(e) => e.preventDefault()}
|
|
566
|
-
onClick={onToggleRecording}
|
|
567
|
-
className={`w-full flex items-center justify-center gap-2 rounded-lg py-2 text-[11px] font-medium transition-colors ${
|
|
568
|
-
recordingState === "recording"
|
|
569
|
-
? "bg-red-500/15 text-red-400 border border-red-500/30 animate-pulse"
|
|
570
|
-
: "bg-panel-input text-panel-text-2 hover:bg-panel-hover border border-panel-border"
|
|
571
|
-
}`}
|
|
572
|
-
>
|
|
573
|
-
<svg width="10" height="10" viewBox="0 0 10 10">
|
|
574
|
-
{recordingState === "recording" ? (
|
|
575
|
-
<rect x="1" y="1" width="8" height="8" rx="1" fill="currentColor" />
|
|
576
|
-
) : (
|
|
577
|
-
<circle cx="5" cy="5" r="4.5" fill="currentColor" />
|
|
578
|
-
)}
|
|
579
|
-
</svg>
|
|
580
|
-
{recordingState === "recording"
|
|
581
|
-
? `Stop recording ${(recordingDuration ?? 0).toFixed(1)}s — press R`
|
|
582
|
-
: "Record gesture (R) — move pointer to capture motion"}
|
|
583
|
-
</button>
|
|
584
|
-
</div>
|
|
585
|
-
)}
|
|
586
570
|
{showEditableSections && (
|
|
587
571
|
<StyleSections
|
|
588
572
|
projectId={projectId}
|
|
@@ -431,6 +431,90 @@ describe("resolveDomEditSelection", () => {
|
|
|
431
431
|
});
|
|
432
432
|
});
|
|
433
433
|
|
|
434
|
+
it("keeps the full-canvas stage layer transform disabled while allowing style edits", async () => {
|
|
435
|
+
const document = createDocument(`
|
|
436
|
+
<div data-hf-id="hf-stage" id="stage">
|
|
437
|
+
<button id="cta">Add to basket</button>
|
|
438
|
+
</div>
|
|
439
|
+
`);
|
|
440
|
+
document.documentElement.setAttribute("data-composition-id", "root");
|
|
441
|
+
document.documentElement.setAttribute("data-width", "1920");
|
|
442
|
+
document.documentElement.setAttribute("data-height", "1080");
|
|
443
|
+
setElementRect(document.documentElement, { left: 0, top: 0, width: 1920, height: 1080 });
|
|
444
|
+
const stage = document.getElementById("stage") as HTMLElement;
|
|
445
|
+
setElementRect(stage, { left: 0, top: 0, width: 1920, height: 1080 });
|
|
446
|
+
|
|
447
|
+
const selection = await resolveDomEditSelection(stage, {
|
|
448
|
+
activeCompositionPath: null,
|
|
449
|
+
isMasterView: true,
|
|
450
|
+
skipSourceProbe: true,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(selection?.id).toBe("stage");
|
|
454
|
+
expect(selection?.capabilities).toMatchObject({
|
|
455
|
+
canSelect: true,
|
|
456
|
+
canEditStyles: true,
|
|
457
|
+
canMove: false,
|
|
458
|
+
canResize: false,
|
|
459
|
+
canApplyManualOffset: false,
|
|
460
|
+
canApplyManualSize: false,
|
|
461
|
+
canApplyManualRotation: false,
|
|
462
|
+
reasonIfDisabled: "The root composition defines the preview bounds.",
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("keeps direct full-bleed absolute layers editable", async () => {
|
|
467
|
+
const document = createDocument(`
|
|
468
|
+
<div id="hero" style="position: absolute; left: 0; top: 0; width: 1920px; height: 1080px;"></div>
|
|
469
|
+
`);
|
|
470
|
+
document.documentElement.setAttribute("data-composition-id", "root");
|
|
471
|
+
document.documentElement.setAttribute("data-width", "1920");
|
|
472
|
+
document.documentElement.setAttribute("data-height", "1080");
|
|
473
|
+
setElementRect(document.documentElement, { left: 0, top: 0, width: 1920, height: 1080 });
|
|
474
|
+
const hero = document.getElementById("hero") as HTMLElement;
|
|
475
|
+
setElementRect(hero, { left: 0, top: 0, width: 1920, height: 1080 });
|
|
476
|
+
|
|
477
|
+
const selection = await resolveDomEditSelection(hero, {
|
|
478
|
+
activeCompositionPath: null,
|
|
479
|
+
isMasterView: true,
|
|
480
|
+
skipSourceProbe: true,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(selection?.id).toBe("hero");
|
|
484
|
+
expect(selection?.capabilities).toMatchObject({
|
|
485
|
+
canSelect: true,
|
|
486
|
+
canEditStyles: true,
|
|
487
|
+
canMove: true,
|
|
488
|
+
canResize: true,
|
|
489
|
+
canApplyManualOffset: true,
|
|
490
|
+
canApplyManualSize: true,
|
|
491
|
+
canApplyManualRotation: true,
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("lets full-canvas layers opt out of root-layer classification", async () => {
|
|
496
|
+
const document = createDocument(`
|
|
497
|
+
<div data-hf-allow-root-edit id="editable-stage">
|
|
498
|
+
<button id="cta">Add to basket</button>
|
|
499
|
+
</div>
|
|
500
|
+
`);
|
|
501
|
+
document.documentElement.setAttribute("data-composition-id", "root");
|
|
502
|
+
document.documentElement.setAttribute("data-width", "1920");
|
|
503
|
+
document.documentElement.setAttribute("data-height", "1080");
|
|
504
|
+
setElementRect(document.documentElement, { left: 0, top: 0, width: 1920, height: 1080 });
|
|
505
|
+
const editableStage = document.getElementById("editable-stage") as HTMLElement;
|
|
506
|
+
setElementRect(editableStage, { left: 0, top: 0, width: 1920, height: 1080 });
|
|
507
|
+
|
|
508
|
+
const selection = await resolveDomEditSelection(editableStage, {
|
|
509
|
+
activeCompositionPath: null,
|
|
510
|
+
isMasterView: true,
|
|
511
|
+
skipSourceProbe: true,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
expect(selection?.id).toBe("editable-stage");
|
|
515
|
+
expect(selection?.capabilities.canApplyManualOffset).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
434
518
|
it("resolves child clicks inside a composition host to the child in master view", async () => {
|
|
435
519
|
const document = createDocument(`
|
|
436
520
|
<div data-composition-id="main">
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
getDirectLayerChildren,
|
|
32
32
|
getSelectionCandidate,
|
|
33
33
|
} from "./domEditingElement";
|
|
34
|
+
import { isCompositionRootLayer } from "./domEditingRootLayer";
|
|
34
35
|
|
|
35
36
|
// ─── Text fields ────────────────────────────────────────────────────────────
|
|
36
37
|
|
|
@@ -179,6 +180,7 @@ export function resolveDomEditCapabilities(args: {
|
|
|
179
180
|
inlineStyles: Record<string, string>;
|
|
180
181
|
computedStyles: Record<string, string>;
|
|
181
182
|
isCompositionHost: boolean;
|
|
183
|
+
isCompositionRoot?: boolean;
|
|
182
184
|
isInsideLockedComposition: boolean;
|
|
183
185
|
isMasterView: boolean;
|
|
184
186
|
existsInSource?: boolean;
|
|
@@ -211,6 +213,19 @@ export function resolveDomEditCapabilities(args: {
|
|
|
211
213
|
};
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
if (args.isCompositionRoot) {
|
|
217
|
+
return {
|
|
218
|
+
canSelect: true,
|
|
219
|
+
canEditStyles: true,
|
|
220
|
+
canMove: false,
|
|
221
|
+
canResize: false,
|
|
222
|
+
canApplyManualOffset: false,
|
|
223
|
+
canApplyManualSize: false,
|
|
224
|
+
canApplyManualRotation: false,
|
|
225
|
+
reasonIfDisabled: "The root composition defines the preview bounds.",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
214
229
|
const position = args.computedStyles.position;
|
|
215
230
|
const left = parsePx(args.inlineStyles.left) ?? parsePx(args.computedStyles.left);
|
|
216
231
|
const top = parsePx(args.inlineStyles.top) ?? parsePx(args.computedStyles.top);
|
|
@@ -341,6 +356,9 @@ export async function resolveDomEditSelection(
|
|
|
341
356
|
undefined;
|
|
342
357
|
const inlineStyles = getInlineStyles(current);
|
|
343
358
|
const computedStyles = getCuratedComputedStyles(current);
|
|
359
|
+
const isCompositionRoot =
|
|
360
|
+
(current.hasAttribute("data-composition-id") && !compositionSrc) ||
|
|
361
|
+
isCompositionRootLayer(current, doc, computedStyles);
|
|
344
362
|
const textFields = collectDomEditTextFields(current);
|
|
345
363
|
const isInsideLocked = Boolean(findClosestByAttribute(current, ["data-timeline-locked"]));
|
|
346
364
|
let existsInSource: boolean | undefined;
|
|
@@ -361,6 +379,7 @@ export async function resolveDomEditSelection(
|
|
|
361
379
|
inlineStyles,
|
|
362
380
|
computedStyles,
|
|
363
381
|
isCompositionHost: Boolean(compositionSrc),
|
|
382
|
+
isCompositionRoot,
|
|
364
383
|
isInsideLockedComposition: isInsideLocked,
|
|
365
384
|
isMasterView: options.isMasterView,
|
|
366
385
|
existsInSource,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parsePx } from "./domEditingDom";
|
|
2
|
+
|
|
3
|
+
const COMPOSITION_ROOT_LAYER_EPSILON_PX = 1;
|
|
4
|
+
|
|
5
|
+
function readPositiveDimension(value: string | null): number | null {
|
|
6
|
+
if (!value) return null;
|
|
7
|
+
const parsed = Number.parseFloat(value);
|
|
8
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function approximatelyEqual(a: number, b: number) {
|
|
12
|
+
return Math.abs(a - b) <= COMPOSITION_ROOT_LAYER_EPSILON_PX;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getCompositionRootBounds(doc: Document) {
|
|
16
|
+
const root =
|
|
17
|
+
doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.documentElement ?? null;
|
|
18
|
+
const rootWidth = readPositiveDimension(root?.getAttribute("data-width") ?? null);
|
|
19
|
+
const rootHeight = readPositiveDimension(root?.getAttribute("data-height") ?? null);
|
|
20
|
+
if (!root || !rootWidth || !rootHeight) return null;
|
|
21
|
+
return { rect: root.getBoundingClientRect(), width: rootWidth, height: rootHeight };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getRenderedLayerSize(element: HTMLElement, computedStyles: Record<string, string>) {
|
|
25
|
+
const rect = element.getBoundingClientRect();
|
|
26
|
+
const width = rect.width || parsePx(computedStyles.width);
|
|
27
|
+
const height = rect.height || parsePx(computedStyles.height);
|
|
28
|
+
return width && height ? { width, height } : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function matchesCompositionRootBounds(
|
|
32
|
+
elementRect: DOMRect,
|
|
33
|
+
elementSize: { width: number; height: number },
|
|
34
|
+
rootBounds: { rect: DOMRect; width: number; height: number },
|
|
35
|
+
) {
|
|
36
|
+
return (
|
|
37
|
+
approximatelyEqual(elementRect.left, rootBounds.rect.left) &&
|
|
38
|
+
approximatelyEqual(elementRect.top, rootBounds.rect.top) &&
|
|
39
|
+
approximatelyEqual(elementSize.width, rootBounds.width) &&
|
|
40
|
+
approximatelyEqual(elementSize.height, rootBounds.height)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isExplicitFullBleedLayer(computedStyles: Record<string, string>) {
|
|
45
|
+
return computedStyles.position === "absolute" || computedStyles.position === "fixed";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isCompositionRootLayer(
|
|
49
|
+
element: HTMLElement,
|
|
50
|
+
doc: Document,
|
|
51
|
+
computedStyles: Record<string, string>,
|
|
52
|
+
) {
|
|
53
|
+
if (element.parentElement !== doc.body) return false;
|
|
54
|
+
if (element.hasAttribute("data-hf-allow-root-edit")) return false;
|
|
55
|
+
if (isExplicitFullBleedLayer(computedStyles)) return false;
|
|
56
|
+
|
|
57
|
+
const rootBounds = getCompositionRootBounds(doc);
|
|
58
|
+
const elementSize = getRenderedLayerSize(element, computedStyles);
|
|
59
|
+
return Boolean(
|
|
60
|
+
rootBounds &&
|
|
61
|
+
elementSize &&
|
|
62
|
+
matchesCompositionRootBounds(element.getBoundingClientRect(), elementSize, rootBounds),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -16,13 +16,12 @@ describe("manual editing availability", () => {
|
|
|
16
16
|
vi.resetModules();
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it("enables inspector selection and manual dragging by default
|
|
19
|
+
it("enables inspector selection and manual dragging by default", async () => {
|
|
20
20
|
const availability = await loadAvailabilityWithEnv({});
|
|
21
21
|
|
|
22
22
|
expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(true);
|
|
23
23
|
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true);
|
|
24
24
|
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
|
|
25
|
-
expect(availability.STUDIO_MOTION_PANEL_ENABLED).toBe(false);
|
|
26
25
|
});
|
|
27
26
|
|
|
28
27
|
it("enables GSAP drag intercept by default", async () => {
|
|
@@ -2,7 +2,6 @@ export type StudioFeatureFlagEnv = Record<string, boolean | string | undefined>;
|
|
|
2
2
|
|
|
3
3
|
const STUDIO_PREVIEW_MANUAL_DRAGGING_ENV = "VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING";
|
|
4
4
|
const STUDIO_INSPECTOR_PANELS_ENV = "VITE_STUDIO_ENABLE_INSPECTOR_PANELS";
|
|
5
|
-
const STUDIO_MOTION_PANEL_ENV = "VITE_STUDIO_ENABLE_MOTION_PANEL";
|
|
6
5
|
const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on", "enabled"]);
|
|
7
6
|
const FALSY_ENV_VALUES = new Set(["0", "false", "no", "off", "disabled"]);
|
|
8
7
|
|
|
@@ -53,12 +52,6 @@ export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
53
52
|
true,
|
|
54
53
|
);
|
|
55
54
|
|
|
56
|
-
export const STUDIO_MOTION_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
57
|
-
env,
|
|
58
|
-
[STUDIO_MOTION_PANEL_ENV, "VITE_STUDIO_MOTION_PANEL_ENABLED"],
|
|
59
|
-
false,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
55
|
export const STUDIO_BLOCKS_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
63
56
|
env,
|
|
64
57
|
["VITE_STUDIO_ENABLE_BLOCKS_PANEL", "VITE_STUDIO_BLOCKS_PANEL_ENABLED"],
|
|
@@ -37,8 +37,7 @@ export function DomEditProvider({
|
|
|
37
37
|
handleDomBoxSizeCommit,
|
|
38
38
|
handleDomRotationCommit,
|
|
39
39
|
handleDomManualEditsReset,
|
|
40
|
-
|
|
41
|
-
handleDomMotionClear,
|
|
40
|
+
|
|
42
41
|
handleDomTextCommit,
|
|
43
42
|
handleDomTextFieldStyleCommit,
|
|
44
43
|
handleDomAddTextField,
|
|
@@ -111,8 +110,6 @@ export function DomEditProvider({
|
|
|
111
110
|
handleDomBoxSizeCommit,
|
|
112
111
|
handleDomRotationCommit,
|
|
113
112
|
handleDomManualEditsReset,
|
|
114
|
-
handleDomMotionCommit,
|
|
115
|
-
handleDomMotionClear,
|
|
116
113
|
handleDomTextCommit,
|
|
117
114
|
handleDomTextFieldStyleCommit,
|
|
118
115
|
handleDomAddTextField,
|
|
@@ -179,8 +176,6 @@ export function DomEditProvider({
|
|
|
179
176
|
handleDomBoxSizeCommit,
|
|
180
177
|
handleDomRotationCommit,
|
|
181
178
|
handleDomManualEditsReset,
|
|
182
|
-
handleDomMotionCommit,
|
|
183
|
-
handleDomMotionClear,
|
|
184
179
|
handleDomTextCommit,
|
|
185
180
|
handleDomTextFieldStyleCommit,
|
|
186
181
|
handleDomAddTextField,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
|
|
4
|
+
export const PROPERTY_DEFAULTS: Record<string, number> = {
|
|
5
|
+
opacity: 1,
|
|
6
|
+
x: 0,
|
|
7
|
+
y: 0,
|
|
8
|
+
scale: 1,
|
|
9
|
+
scaleX: 1,
|
|
10
|
+
scaleY: 1,
|
|
11
|
+
rotation: 0,
|
|
12
|
+
width: 100,
|
|
13
|
+
height: 100,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function ensureElementAddressable(selection: DomEditSelection): {
|
|
17
|
+
selector: string;
|
|
18
|
+
autoId?: string;
|
|
19
|
+
} {
|
|
20
|
+
if (selection.id) return { selector: `#${selection.id}` };
|
|
21
|
+
if (selection.selector) return { selector: selection.selector };
|
|
22
|
+
|
|
23
|
+
const el = selection.element;
|
|
24
|
+
const doc = el.ownerDocument;
|
|
25
|
+
const tag = el.tagName.toLowerCase();
|
|
26
|
+
let id = tag;
|
|
27
|
+
let n = 1;
|
|
28
|
+
while (doc.getElementById(id)) {
|
|
29
|
+
n += 1;
|
|
30
|
+
id = `${tag}-${n}`;
|
|
31
|
+
}
|
|
32
|
+
el.setAttribute("id", id);
|
|
33
|
+
return { selector: `#${id}`, autoId: id };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class GsapMutationHttpError extends Error {
|
|
37
|
+
constructor(
|
|
38
|
+
readonly statusCode: number,
|
|
39
|
+
readonly responseBody: unknown,
|
|
40
|
+
) {
|
|
41
|
+
super(formatGsapMutationHttpErrorMessage(statusCode, responseBody));
|
|
42
|
+
this.name = "GsapMutationHttpError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function readJsonResponseBody(res: Response): Promise<unknown> {
|
|
51
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
52
|
+
if (!contentType.includes("application/json")) {
|
|
53
|
+
return await res.text().catch(() => null);
|
|
54
|
+
}
|
|
55
|
+
return await res.json().catch(() => null);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatGsapMutationHttpErrorMessage(statusCode: number, body: unknown): string {
|
|
59
|
+
if (isRecord(body) && typeof body.error === "string") {
|
|
60
|
+
return body.error;
|
|
61
|
+
}
|
|
62
|
+
return `GSAP mutation failed with status ${statusCode}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatGsapMutationRejectionToast(error: GsapMutationHttpError): string {
|
|
66
|
+
const body = error.responseBody;
|
|
67
|
+
if (isRecord(body)) {
|
|
68
|
+
const fields = Array.isArray(body.fields)
|
|
69
|
+
? body.fields.filter((field): field is string => typeof field === "string")
|
|
70
|
+
: [];
|
|
71
|
+
const suffix = fields.length > 0 ? ` (${fields.join(", ")})` : "";
|
|
72
|
+
return `Couldn't save animation: ${formatGsapMutationHttpErrorMessage(
|
|
73
|
+
error.statusCode,
|
|
74
|
+
body,
|
|
75
|
+
)}${suffix}`;
|
|
76
|
+
}
|
|
77
|
+
return `Couldn't save animation: ${error.message}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface AssignAutoIdParams {
|
|
81
|
+
projectId: string;
|
|
82
|
+
targetPath: string;
|
|
83
|
+
selection: DomEditSelection;
|
|
84
|
+
autoId: string;
|
|
85
|
+
showToast?: (message: string, tone?: "error" | "info") => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function assignGsapTargetAutoIdIfNeeded({
|
|
89
|
+
projectId,
|
|
90
|
+
targetPath,
|
|
91
|
+
selection,
|
|
92
|
+
autoId,
|
|
93
|
+
showToast,
|
|
94
|
+
}: AssignAutoIdParams): Promise<boolean> {
|
|
95
|
+
const patchBody = {
|
|
96
|
+
target: {
|
|
97
|
+
id: selection.id,
|
|
98
|
+
hfId: selection.hfId,
|
|
99
|
+
selector: selection.selector,
|
|
100
|
+
selectorIndex: selection.selectorIndex,
|
|
101
|
+
},
|
|
102
|
+
operations: [{ type: "html-attribute", property: "id", value: autoId }],
|
|
103
|
+
};
|
|
104
|
+
const unsafePatchFields = findUnsafeDomPatchValues(patchBody);
|
|
105
|
+
if (unsafePatchFields.length > 0) {
|
|
106
|
+
showToast?.("Couldn't assign element id because the patch contains invalid values", "error");
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const res = await fetch(
|
|
110
|
+
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
|
|
111
|
+
{
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify(patchBody),
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
showToast?.(
|
|
119
|
+
formatGsapMutationRejectionToast(
|
|
120
|
+
new GsapMutationHttpError(res.status, await readJsonResponseBody(res)),
|
|
121
|
+
),
|
|
122
|
+
"error",
|
|
123
|
+
);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const data = (await res.json()) as { changed?: boolean };
|
|
127
|
+
return data.changed === true;
|
|
128
|
+
}
|