@hyperframes/studio 0.6.86 → 0.6.87
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/index-BA19FAPN.js +143 -0
- package/dist/assets/index-CGlIm_-E.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +159 -6
- package/src/components/StudioHeader.tsx +20 -7
- package/src/components/StudioPreviewArea.tsx +6 -1
- package/src/components/StudioRightPanel.tsx +13 -0
- package/src/components/StudioToast.tsx +47 -7
- package/src/components/TimelineToolbar.tsx +12 -122
- package/src/components/editor/AnimationCard.tsx +64 -10
- package/src/components/editor/ArcPathControls.tsx +131 -0
- package/src/components/editor/BorderRadiusEditor.tsx +209 -0
- package/src/components/editor/DomEditOverlay.tsx +70 -11
- package/src/components/editor/DopesheetStrip.tsx +141 -0
- package/src/components/editor/EaseCurveSection.tsx +82 -7
- package/src/components/editor/GestureTrailOverlay.tsx +132 -0
- package/src/components/editor/GsapAnimationSection.tsx +14 -1
- package/src/components/editor/KeyframeDiamond.tsx +27 -12
- package/src/components/editor/LayersPanel.tsx +14 -12
- package/src/components/editor/MotionPathOverlay.tsx +146 -0
- package/src/components/editor/PropertyPanel.tsx +196 -66
- package/src/components/editor/SourceEditor.tsx +0 -1
- package/src/components/editor/StaggerControls.tsx +61 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +13 -0
- package/src/components/editor/domEditOverlayGeometry.ts +2 -1
- package/src/components/editor/domEditing.test.ts +43 -0
- package/src/components/editor/domEditing.ts +2 -0
- package/src/components/editor/domEditingElement.ts +25 -2
- package/src/components/editor/domEditingLayers.test.ts +78 -0
- package/src/components/editor/domEditingLayers.ts +33 -13
- package/src/components/editor/domEditingTypes.ts +1 -0
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +3 -0
- package/src/components/editor/manualEditsDom.ts +23 -5
- package/src/components/editor/manualOffsetDrag.ts +59 -0
- package/src/components/editor/panelTokens.ts +10 -0
- package/src/components/editor/propertyPanelColor.tsx +2 -2
- package/src/components/editor/propertyPanelFill.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +18 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +38 -25
- package/src/components/editor/propertyPanelSections.tsx +4 -6
- package/src/components/editor/propertyPanelStyleSections.tsx +30 -8
- package/src/components/editor/useDomEditOverlayRects.ts +46 -2
- package/src/components/renders/RenderQueue.tsx +121 -100
- package/src/components/renders/RenderQueueItem.tsx +13 -13
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +0 -4
- package/src/hooks/gsapKeyframeCommit.ts +92 -0
- package/src/hooks/gsapRuntimeBridge.ts +147 -85
- package/src/hooks/gsapRuntimeKeyframes.ts +75 -24
- package/src/hooks/gsapRuntimePreview.ts +19 -0
- package/src/hooks/useAppHotkeys.ts +18 -0
- package/src/hooks/useAskAgentModal.ts +2 -4
- package/src/hooks/useDomEditCommits.ts +11 -17
- package/src/hooks/useDomEditSession.ts +47 -4
- package/src/hooks/useEnableKeyframes.ts +171 -0
- package/src/hooks/useFileManager.ts +7 -0
- package/src/hooks/useGestureRecording.ts +340 -0
- package/src/hooks/useGsapScriptCommits.ts +171 -35
- package/src/hooks/useGsapSelectionHandlers.ts +27 -8
- package/src/hooks/useGsapTweenCache.ts +169 -11
- package/src/hooks/useKeyframeKeyboard.ts +103 -0
- package/src/hooks/useStudioContextValue.ts +5 -4
- package/src/hooks/useStudioUrlState.ts +1 -2
- package/src/hooks/useTimelineEditing.ts +50 -3
- package/src/hooks/useToast.ts +6 -1
- package/src/player/components/ShortcutsPanel.tsx +40 -0
- package/src/player/components/TimelineClipDiamonds.tsx +3 -3
- package/src/player/components/TimelinePropertyRows.tsx +120 -0
- package/src/player/lib/timelineDOM.test.ts +55 -0
- package/src/player/lib/timelineDOM.ts +13 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +51 -0
- package/src/player/lib/timelineIframeHelpers.ts +1 -0
- package/src/player/store/playerStore.ts +43 -0
- package/src/utils/audioBeatDetection.ts +58 -0
- package/src/utils/globalTimeCompiler.test.ts +169 -0
- package/src/utils/globalTimeCompiler.ts +77 -0
- package/src/utils/gsapSoftReload.ts +30 -10
- package/src/utils/keyframeSnapping.test.ts +74 -0
- package/src/utils/keyframeSnapping.ts +63 -0
- package/src/utils/rdpSimplify.ts +183 -0
- package/src/utils/sourcePatcher.ts +2 -0
- package/dist/assets/index-BT9VHgSy.js +0 -140
- package/dist/assets/index-DHcptK1_.css +0 -1
|
@@ -1156,3 +1156,46 @@ describe("patch builders and prompt builder", () => {
|
|
|
1156
1156
|
).not.toThrow();
|
|
1157
1157
|
});
|
|
1158
1158
|
});
|
|
1159
|
+
|
|
1160
|
+
describe("hfId — find, key, capabilities (R7 fixes)", () => {
|
|
1161
|
+
it("getDomEditTargetKey keeps two hfId-only elements distinct", () => {
|
|
1162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1163
|
+
const a = getDomEditTargetKey({ sourceFile: "index.html", hfId: "hf-aaa" } as any);
|
|
1164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1165
|
+
const b = getDomEditTargetKey({ sourceFile: "index.html", hfId: "hf-bbb" } as any);
|
|
1166
|
+
expect(a).not.toBe(b);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
it("findElementForSelection finds element by data-hf-id when no id or selector", () => {
|
|
1170
|
+
const doc = createDocument(`
|
|
1171
|
+
<div data-composition-id="root">
|
|
1172
|
+
<div data-hf-id="hf-xyz789" class="clip" style="position:absolute;left:0;top:0;width:100px;height:100px;"></div>
|
|
1173
|
+
</div>
|
|
1174
|
+
`);
|
|
1175
|
+
const el = doc.querySelector('[data-hf-id="hf-xyz789"]') as HTMLElement;
|
|
1176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1177
|
+
const found = findElementForSelection(doc, { hfId: "hf-xyz789" } as any);
|
|
1178
|
+
expect(found).toBe(el);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
it("resolveDomEditCapabilities enables editing for hfId-only element (no CSS selector)", () => {
|
|
1182
|
+
const result = resolveDomEditCapabilities({
|
|
1183
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1184
|
+
hfId: "hf-abc" as any,
|
|
1185
|
+
selector: undefined,
|
|
1186
|
+
inlineStyles: { left: "10px", top: "20px", width: "100px", height: "50px" },
|
|
1187
|
+
computedStyles: {
|
|
1188
|
+
position: "absolute",
|
|
1189
|
+
left: "10px",
|
|
1190
|
+
top: "20px",
|
|
1191
|
+
width: "100px",
|
|
1192
|
+
height: "50px",
|
|
1193
|
+
},
|
|
1194
|
+
isCompositionHost: false,
|
|
1195
|
+
isInsideLockedComposition: false,
|
|
1196
|
+
isMasterView: false,
|
|
1197
|
+
});
|
|
1198
|
+
expect(result.canSelect).toBe(true);
|
|
1199
|
+
expect(result.canMove).toBe(true);
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
@@ -26,6 +26,7 @@ export {
|
|
|
26
26
|
// Layers, text fields, capabilities, selection, patch ops
|
|
27
27
|
export {
|
|
28
28
|
buildDefaultDomEditTextField,
|
|
29
|
+
buildDomEditPatchTarget,
|
|
29
30
|
buildDomEditStylePatchOperation,
|
|
30
31
|
buildDomEditTextPatchOperation,
|
|
31
32
|
collectDomEditLayerItems,
|
|
@@ -34,6 +35,7 @@ export {
|
|
|
34
35
|
getDomEditNonEditableReason,
|
|
35
36
|
getDomEditTargetKey,
|
|
36
37
|
isTextEditableSelection,
|
|
38
|
+
readHfId,
|
|
37
39
|
refreshDomEditSelection,
|
|
38
40
|
resolveDomEditCapabilities,
|
|
39
41
|
resolveDomEditSelection,
|
|
@@ -37,9 +37,26 @@ export function isElementComputedVisible(el: HTMLElement): boolean {
|
|
|
37
37
|
|
|
38
38
|
const VISUAL_LEAF_TAGS = new Set(["img", "video", "canvas", "svg", "audio"]);
|
|
39
39
|
|
|
40
|
+
function hasVisualPresence(el: HTMLElement): boolean {
|
|
41
|
+
const win = el.ownerDocument.defaultView;
|
|
42
|
+
if (!win) return false;
|
|
43
|
+
const cs = win.getComputedStyle(el);
|
|
44
|
+
if (cs.backgroundImage !== "none") return true;
|
|
45
|
+
if (
|
|
46
|
+
cs.backgroundColor &&
|
|
47
|
+
cs.backgroundColor !== "transparent" &&
|
|
48
|
+
cs.backgroundColor !== "rgba(0, 0, 0, 0)"
|
|
49
|
+
)
|
|
50
|
+
return true;
|
|
51
|
+
if (cs.borderWidth && parseFloat(cs.borderWidth) > 0 && cs.borderStyle !== "none") return true;
|
|
52
|
+
if (cs.boxShadow && cs.boxShadow !== "none") return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
function isEmptyVisualContainer(el: HTMLElement): boolean {
|
|
41
57
|
const tag = el.tagName.toLowerCase();
|
|
42
58
|
if (VISUAL_LEAF_TAGS.has(tag)) return false;
|
|
59
|
+
if (hasVisualPresence(el)) return false;
|
|
43
60
|
|
|
44
61
|
const { children } = el;
|
|
45
62
|
if (children.length === 0) {
|
|
@@ -95,7 +112,7 @@ function isInspectableLayerElement(el: HTMLElement): boolean {
|
|
|
95
112
|
export function getDomLayerPatchTarget(
|
|
96
113
|
el: HTMLElement,
|
|
97
114
|
activeCompositionPath: string | null,
|
|
98
|
-
): Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile"> | null {
|
|
115
|
+
): Pick<DomEditSelection, "id" | "hfId" | "selector" | "selectorIndex" | "sourceFile"> | null {
|
|
99
116
|
if (!isInspectableLayerElement(el)) return null;
|
|
100
117
|
if (el.hasAttribute("data-composition-id")) return null;
|
|
101
118
|
|
|
@@ -105,6 +122,7 @@ export function getDomLayerPatchTarget(
|
|
|
105
122
|
const { sourceFile } = getSourceFileForElement(el, activeCompositionPath);
|
|
106
123
|
return {
|
|
107
124
|
id: el.id || undefined,
|
|
125
|
+
hfId: el.getAttribute("data-hf-id") || undefined,
|
|
108
126
|
selector,
|
|
109
127
|
selectorIndex: getSelectorIndex(
|
|
110
128
|
el.ownerDocument,
|
|
@@ -229,9 +247,14 @@ export function isLargeRasterDomEditSelection(
|
|
|
229
247
|
|
|
230
248
|
export function findElementForSelection(
|
|
231
249
|
doc: Document,
|
|
232
|
-
selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
|
|
250
|
+
selection: Pick<DomEditSelection, "id" | "hfId" | "selector" | "selectorIndex" | "sourceFile">,
|
|
233
251
|
activeCompositionPath: string | null = null,
|
|
234
252
|
): HTMLElement | null {
|
|
253
|
+
if (selection.hfId) {
|
|
254
|
+
const byHfId = doc.querySelector(`[data-hf-id="${selection.hfId}"]`);
|
|
255
|
+
if (isHtmlElement(byHfId)) return byHfId;
|
|
256
|
+
}
|
|
257
|
+
|
|
235
258
|
if (selection.id) {
|
|
236
259
|
const byId = doc.getElementById(selection.id);
|
|
237
260
|
if (
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { resolveDomEditSelection, buildDomEditPatchTarget, readHfId } from "./domEditingLayers";
|
|
4
|
+
|
|
5
|
+
const opts = { activeCompositionPath: "index.html", isMasterView: true, skipSourceProbe: true };
|
|
6
|
+
|
|
7
|
+
describe("buildDomEditPatchTarget", () => {
|
|
8
|
+
it("includes hfId when selection has hfId", () => {
|
|
9
|
+
const target = buildDomEditPatchTarget({
|
|
10
|
+
id: undefined,
|
|
11
|
+
hfId: "hf-abc",
|
|
12
|
+
selector: ".foo",
|
|
13
|
+
selectorIndex: 0,
|
|
14
|
+
});
|
|
15
|
+
expect(target.hfId).toBe("hf-abc");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("includes id and selector when hfId absent", () => {
|
|
19
|
+
const target = buildDomEditPatchTarget({
|
|
20
|
+
id: "hero",
|
|
21
|
+
hfId: undefined,
|
|
22
|
+
selector: "#hero",
|
|
23
|
+
selectorIndex: undefined,
|
|
24
|
+
});
|
|
25
|
+
expect(target.id).toBe("hero");
|
|
26
|
+
expect(target.hfId).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("readHfId", () => {
|
|
31
|
+
it("returns the attribute value when present", () => {
|
|
32
|
+
const el = document.createElement("div");
|
|
33
|
+
el.setAttribute("data-hf-id", "hf-abc");
|
|
34
|
+
expect(readHfId(el)).toBe("hf-abc");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns undefined when attribute is absent", () => {
|
|
38
|
+
const el = document.createElement("div");
|
|
39
|
+
expect(readHfId(el)).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns undefined when attribute is empty string", () => {
|
|
43
|
+
const el = document.createElement("div");
|
|
44
|
+
el.setAttribute("data-hf-id", "");
|
|
45
|
+
expect(readHfId(el)).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns undefined when attribute is whitespace-only", () => {
|
|
49
|
+
const el = document.createElement("div");
|
|
50
|
+
el.setAttribute("data-hf-id", " ");
|
|
51
|
+
expect(readHfId(el)).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("resolveDomEditSelection — hfId from data-hf-id", () => {
|
|
56
|
+
it("populates hfId from the element data-hf-id attribute", async () => {
|
|
57
|
+
const el = document.createElement("div");
|
|
58
|
+
el.id = "hero";
|
|
59
|
+
el.setAttribute("data-hf-id", "hf-x7k2");
|
|
60
|
+
document.body.appendChild(el);
|
|
61
|
+
|
|
62
|
+
const selection = await resolveDomEditSelection(el, opts);
|
|
63
|
+
document.body.removeChild(el);
|
|
64
|
+
|
|
65
|
+
expect(selection?.hfId).toBe("hf-x7k2");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("leaves hfId undefined when element has no data-hf-id", async () => {
|
|
69
|
+
const el = document.createElement("div");
|
|
70
|
+
el.id = "no-hfid-el";
|
|
71
|
+
document.body.appendChild(el);
|
|
72
|
+
|
|
73
|
+
const selection = await resolveDomEditSelection(el, opts);
|
|
74
|
+
document.body.removeChild(el);
|
|
75
|
+
|
|
76
|
+
expect(selection?.hfId).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -173,6 +173,7 @@ export function buildDefaultDomEditTextField(base?: Partial<DomEditTextField>):
|
|
|
173
173
|
// fallow-ignore-next-line complexity
|
|
174
174
|
export function resolveDomEditCapabilities(args: {
|
|
175
175
|
selector?: string;
|
|
176
|
+
hfId?: string;
|
|
176
177
|
tagName?: string;
|
|
177
178
|
className?: string;
|
|
178
179
|
inlineStyles: Record<string, string>;
|
|
@@ -182,7 +183,7 @@ export function resolveDomEditCapabilities(args: {
|
|
|
182
183
|
isMasterView: boolean;
|
|
183
184
|
existsInSource?: boolean;
|
|
184
185
|
}): DomEditCapabilities {
|
|
185
|
-
if (!args.selector || args.isInsideLockedComposition) {
|
|
186
|
+
if ((!args.selector && !args.hfId) || args.isInsideLockedComposition) {
|
|
186
187
|
return {
|
|
187
188
|
canSelect: !args.isInsideLockedComposition,
|
|
188
189
|
canEditStyles: false,
|
|
@@ -289,7 +290,7 @@ export function buildElementLabel(el: HTMLElement): string {
|
|
|
289
290
|
async function probeSourceElement(
|
|
290
291
|
projectId: string,
|
|
291
292
|
sourceFile: string,
|
|
292
|
-
target: { id?: string; selector?: string; selectorIndex?: number },
|
|
293
|
+
target: { id?: string; hfId?: string; selector?: string; selectorIndex?: number },
|
|
293
294
|
): Promise<boolean> {
|
|
294
295
|
try {
|
|
295
296
|
const response = await fetch(
|
|
@@ -321,7 +322,8 @@ export async function resolveDomEditSelection(
|
|
|
321
322
|
let current: HTMLElement | null = getSelectionCandidate(startEl, options);
|
|
322
323
|
while (current && current !== doc.body && current !== doc.documentElement) {
|
|
323
324
|
const selector = buildStableSelector(current);
|
|
324
|
-
|
|
325
|
+
const hfId = readHfId(current);
|
|
326
|
+
if (!selector && !hfId) {
|
|
325
327
|
current = current.parentElement;
|
|
326
328
|
continue;
|
|
327
329
|
}
|
|
@@ -330,13 +332,9 @@ export async function resolveDomEditSelection(
|
|
|
330
332
|
current,
|
|
331
333
|
options.activeCompositionPath,
|
|
332
334
|
);
|
|
333
|
-
const selectorIndex =
|
|
334
|
-
doc,
|
|
335
|
-
|
|
336
|
-
selector,
|
|
337
|
-
sourceFile,
|
|
338
|
-
options.activeCompositionPath,
|
|
339
|
-
);
|
|
335
|
+
const selectorIndex = selector
|
|
336
|
+
? getSelectorIndex(doc, current, selector, sourceFile, options.activeCompositionPath)
|
|
337
|
+
: undefined;
|
|
340
338
|
const compositionSrc =
|
|
341
339
|
current.getAttribute("data-composition-src") ??
|
|
342
340
|
current.getAttribute("data-composition-file") ??
|
|
@@ -346,15 +344,18 @@ export async function resolveDomEditSelection(
|
|
|
346
344
|
const textFields = collectDomEditTextFields(current);
|
|
347
345
|
const isInsideLocked = Boolean(findClosestByAttribute(current, ["data-timeline-locked"]));
|
|
348
346
|
let existsInSource: boolean | undefined;
|
|
349
|
-
if (!options.skipSourceProbe && options.projectId && (current.id || selector)) {
|
|
350
|
-
const probeTarget: { id?: string; selector?: string; selectorIndex?: number } =
|
|
347
|
+
if (!options.skipSourceProbe && options.projectId && (current.id || selector || hfId)) {
|
|
348
|
+
const probeTarget: { id?: string; hfId?: string; selector?: string; selectorIndex?: number } =
|
|
349
|
+
{};
|
|
351
350
|
if (current.id) probeTarget.id = current.id;
|
|
351
|
+
if (hfId) probeTarget.hfId = hfId;
|
|
352
352
|
if (selector) probeTarget.selector = selector;
|
|
353
353
|
if (selectorIndex != null) probeTarget.selectorIndex = selectorIndex;
|
|
354
354
|
existsInSource = await probeSourceElement(options.projectId, sourceFile, probeTarget);
|
|
355
355
|
}
|
|
356
356
|
const capabilities = resolveDomEditCapabilities({
|
|
357
357
|
selector,
|
|
358
|
+
hfId,
|
|
358
359
|
tagName: current.tagName.toLowerCase(),
|
|
359
360
|
className: current.className,
|
|
360
361
|
inlineStyles,
|
|
@@ -369,6 +370,7 @@ export async function resolveDomEditSelection(
|
|
|
369
370
|
return {
|
|
370
371
|
element: current,
|
|
371
372
|
id: current.id || undefined,
|
|
373
|
+
hfId,
|
|
372
374
|
selector,
|
|
373
375
|
selectorIndex,
|
|
374
376
|
sourceFile,
|
|
@@ -451,6 +453,7 @@ export function collectDomEditLayerItems(
|
|
|
451
453
|
if (!root) return [];
|
|
452
454
|
|
|
453
455
|
const items: DomEditLayerItem[] = [];
|
|
456
|
+
// fallow-ignore-next-line complexity
|
|
454
457
|
const visit = (el: HTMLElement, depth: number) => {
|
|
455
458
|
if (items.length >= maxItems) return;
|
|
456
459
|
|
|
@@ -464,6 +467,7 @@ export function collectDomEditLayerItems(
|
|
|
464
467
|
depth,
|
|
465
468
|
childCount: getDirectLayerChildren(el, options).length,
|
|
466
469
|
id: target.id ?? undefined,
|
|
470
|
+
hfId: target.hfId ?? undefined,
|
|
467
471
|
selector: target.selector ?? undefined,
|
|
468
472
|
selectorIndex: target.selectorIndex,
|
|
469
473
|
sourceFile: target.sourceFile,
|
|
@@ -535,10 +539,11 @@ export function getDomEditNonEditableReason(
|
|
|
535
539
|
}
|
|
536
540
|
|
|
537
541
|
export function getDomEditTargetKey(
|
|
538
|
-
selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
|
|
542
|
+
selection: Pick<DomEditSelection, "id" | "hfId" | "selector" | "selectorIndex" | "sourceFile">,
|
|
539
543
|
): string {
|
|
540
544
|
return [
|
|
541
545
|
selection.sourceFile || "index.html",
|
|
546
|
+
selection.hfId ?? "",
|
|
542
547
|
selection.id ?? "",
|
|
543
548
|
selection.selector ?? "",
|
|
544
549
|
selection.selectorIndex ?? "",
|
|
@@ -554,3 +559,18 @@ export function isTextEditableSelection(selection: DomEditSelection): boolean {
|
|
|
554
559
|
}
|
|
555
560
|
|
|
556
561
|
// buildElementAgentPrompt is in domEditingAgentPrompt.ts
|
|
562
|
+
|
|
563
|
+
export function readHfId(element: Element): string | undefined {
|
|
564
|
+
return element.getAttribute("data-hf-id")?.trim() || undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function buildDomEditPatchTarget(
|
|
568
|
+
selection: Pick<DomEditSelection, "id" | "hfId" | "selector" | "selectorIndex">,
|
|
569
|
+
): { id?: string | null; hfId?: string; selector?: string; selectorIndex?: number } {
|
|
570
|
+
return {
|
|
571
|
+
id: selection.id,
|
|
572
|
+
hfId: selection.hfId,
|
|
573
|
+
selector: selection.selector,
|
|
574
|
+
selectorIndex: selection.selectorIndex,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
@@ -74,7 +74,7 @@ export const STUDIO_GSAP_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
|
|
|
74
74
|
export const STUDIO_KEYFRAMES_ENABLED = resolveStudioBooleanEnvFlag(
|
|
75
75
|
env,
|
|
76
76
|
["VITE_STUDIO_ENABLE_KEYFRAMES", "VITE_STUDIO_KEYFRAMES_ENABLED"],
|
|
77
|
-
|
|
77
|
+
true,
|
|
78
78
|
);
|
|
79
79
|
|
|
80
80
|
export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
|
|
@@ -240,6 +240,7 @@ export function installStudioManualEditSeekReapply(win: Window, apply: () => voi
|
|
|
240
240
|
"renderSeek",
|
|
241
241
|
);
|
|
242
242
|
const wrappedTimelineSeek = wrapSeekReapplyFunction(studioWin, studioWin.__timeline, "seek");
|
|
243
|
+
wrapSeekReapplyFunction(studioWin, studioWin.__timeline, "totalTime");
|
|
243
244
|
const wrappedPlayerPlay = wrapPlayReapplyFunction(studioWin, studioWin.__player, "play");
|
|
244
245
|
const wrappedTimelinePlay = wrapPlayReapplyFunction(studioWin, studioWin.__timeline, "play");
|
|
245
246
|
const wrappedPlayerPause = wrapApplyAfterFunction(studioWin, studioWin.__player, "pause");
|
|
@@ -250,6 +251,7 @@ export function installStudioManualEditSeekReapply(win: Window, apply: () => voi
|
|
|
250
251
|
for (const timeline of Object.values(studioWin.__timelines ?? {})) {
|
|
251
252
|
wrappedNamedTimelineSeek =
|
|
252
253
|
wrapSeekReapplyFunction(studioWin, timeline, "seek") || wrappedNamedTimelineSeek;
|
|
254
|
+
wrapSeekReapplyFunction(studioWin, timeline, "totalTime");
|
|
253
255
|
wrappedNamedTimelinePlay =
|
|
254
256
|
wrapPlayReapplyFunction(studioWin, timeline, "play") || wrappedNamedTimelinePlay;
|
|
255
257
|
wrappedNamedTimelinePause =
|
|
@@ -268,6 +270,7 @@ export function installStudioManualEditSeekReapply(win: Window, apply: () => voi
|
|
|
268
270
|
if (typeof value === "object" && value !== null) {
|
|
269
271
|
const tl = value as Record<string, unknown>;
|
|
270
272
|
wrapSeekReapplyFunction(studioWin, tl, "seek");
|
|
273
|
+
wrapSeekReapplyFunction(studioWin, tl, "totalTime");
|
|
271
274
|
wrapPlayReapplyFunction(studioWin, tl, "play");
|
|
272
275
|
wrapApplyAfterFunction(studioWin, tl, "pause");
|
|
273
276
|
studioWin.__hfStudioManualEditsApply?.();
|
|
@@ -273,11 +273,25 @@ export function applyStudioPathOffsetDraft(
|
|
|
273
273
|
): void {
|
|
274
274
|
promoteInlineForTransform(element);
|
|
275
275
|
writeStudioPathOffsetVars(element, offset, { updateBase: false });
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
|
|
277
|
+
const isGsapAnimated = gsapAnimatesProperty(element, "x", "y");
|
|
278
|
+
if (isGsapAnimated) {
|
|
279
|
+
// For GSAP-animated elements: use gsap.set for positioning (the timeline
|
|
280
|
+
// is paused during drag). Set translate:none explicitly to prevent
|
|
281
|
+
// double-counting with the transform.
|
|
282
|
+
element.style.setProperty("translate", "none");
|
|
283
|
+
const win = element.ownerDocument.defaultView as
|
|
284
|
+
| (Window & { gsap?: { set: (el: Element, vars: Record<string, unknown>) => void } })
|
|
285
|
+
| null;
|
|
286
|
+
win?.gsap?.set(element, { x: offset.x, y: offset.y });
|
|
287
|
+
} else {
|
|
288
|
+
// Non-GSAP elements: use CSS translate as before.
|
|
289
|
+
element.style.setProperty(
|
|
290
|
+
"translate",
|
|
291
|
+
composeTranslateValue(element, `${Math.round(offset.x)}px`, `${Math.round(offset.y)}px`),
|
|
292
|
+
);
|
|
293
|
+
stripGsapTranslateFromTransform(element);
|
|
294
|
+
}
|
|
281
295
|
}
|
|
282
296
|
|
|
283
297
|
/* ── Box size apply ───────────────────────────────────────────────── */
|
|
@@ -505,6 +519,10 @@ function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
|
|
|
505
519
|
|
|
506
520
|
function reapplyPathOffsets(doc: Document): void {
|
|
507
521
|
for (const el of queryStudioElements(doc, STUDIO_PATH_OFFSET_ATTR)) {
|
|
522
|
+
// Skip elements where GSAP actively animates position — GSAP bakes the
|
|
523
|
+
// CSS translate into its transform and sets translate: none every tick.
|
|
524
|
+
// Stripping/restoring would oscillate against GSAP's rendering.
|
|
525
|
+
if (gsapAnimatesProperty(el, "x", "y")) continue;
|
|
508
526
|
const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
|
|
509
527
|
const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
|
|
510
528
|
if (x || y) {
|
|
@@ -232,6 +232,41 @@ export function createManualOffsetDragMember(input: {
|
|
|
232
232
|
rect: ManualOffsetDragRect;
|
|
233
233
|
}): ManualOffsetDragMemberResult {
|
|
234
234
|
const initialOffset = readStudioPathOffset(input.element);
|
|
235
|
+
input.element.setAttribute("data-hf-drag-initial-offset-x", String(initialOffset.x));
|
|
236
|
+
input.element.setAttribute("data-hf-drag-initial-offset-y", String(initialOffset.y));
|
|
237
|
+
|
|
238
|
+
// Capture GSAP's x/y BEFORE any draft applies gsap.set — the commit path
|
|
239
|
+
// needs the original (uncorrupted) GSAP position to compute the new keyframe value.
|
|
240
|
+
const win = input.element.ownerDocument.defaultView as
|
|
241
|
+
| (Window & {
|
|
242
|
+
gsap?: { getProperty?: (el: Element, prop: string) => number };
|
|
243
|
+
__timelines?: Record<string, { pause?: () => void; paused?: () => boolean }>;
|
|
244
|
+
})
|
|
245
|
+
| null;
|
|
246
|
+
const gsapX = win?.gsap?.getProperty?.(input.element, "x") || 0;
|
|
247
|
+
const gsapY = win?.gsap?.getProperty?.(input.element, "y") || 0;
|
|
248
|
+
input.element.setAttribute("data-hf-drag-gsap-base-x", String(gsapX));
|
|
249
|
+
input.element.setAttribute("data-hf-drag-gsap-base-y", String(gsapY));
|
|
250
|
+
|
|
251
|
+
// Pause GSAP timelines during drag to prevent the tween from overwriting
|
|
252
|
+
// the draft's gsap.set on every tick. Track which we paused to resume later.
|
|
253
|
+
if (win?.__timelines) {
|
|
254
|
+
const paused: string[] = [];
|
|
255
|
+
for (const [id, tl] of Object.entries(win.__timelines)) {
|
|
256
|
+
try {
|
|
257
|
+
if (tl?.pause && !tl.paused?.()) {
|
|
258
|
+
tl.pause();
|
|
259
|
+
paused.push(id);
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
/* cross-origin guard */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (paused.length > 0) {
|
|
266
|
+
input.element.setAttribute("data-hf-drag-paused-timelines", paused.join(","));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
235
270
|
const initialPathOffset = captureStudioPathOffset(input.element);
|
|
236
271
|
const gestureToken = beginStudioManualEditGesture(input.element);
|
|
237
272
|
const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset);
|
|
@@ -313,11 +348,35 @@ function restoreManualOffsetDragMember(member: ManualOffsetDragMember): void {
|
|
|
313
348
|
export function restoreManualOffsetDragMembers(members: ManualOffsetDragMember[]): void {
|
|
314
349
|
for (const member of members) {
|
|
315
350
|
restoreManualOffsetDragMember(member);
|
|
351
|
+
resumeGsapTimelines(member.element);
|
|
316
352
|
}
|
|
317
353
|
}
|
|
318
354
|
|
|
319
355
|
export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): void {
|
|
320
356
|
for (const member of members) {
|
|
321
357
|
endStudioManualEditGesture(member.element, member.gestureToken);
|
|
358
|
+
member.element.removeAttribute("data-hf-drag-initial-offset-x");
|
|
359
|
+
member.element.removeAttribute("data-hf-drag-initial-offset-y");
|
|
360
|
+
member.element.removeAttribute("data-hf-drag-gsap-base-x");
|
|
361
|
+
member.element.removeAttribute("data-hf-drag-gsap-base-y");
|
|
362
|
+
resumeGsapTimelines(member.element);
|
|
322
363
|
}
|
|
323
364
|
}
|
|
365
|
+
|
|
366
|
+
function resumeGsapTimelines(element: HTMLElement): void {
|
|
367
|
+
const ids = element.getAttribute("data-hf-drag-paused-timelines");
|
|
368
|
+
element.removeAttribute("data-hf-drag-paused-timelines");
|
|
369
|
+
if (!ids) return;
|
|
370
|
+
const win = element.ownerDocument.defaultView as
|
|
371
|
+
| (Window & {
|
|
372
|
+
__timelines?: Record<string, { pause?: () => void }>;
|
|
373
|
+
__player?: { seek?: (t: number) => void; getTime?: () => number };
|
|
374
|
+
})
|
|
375
|
+
| null;
|
|
376
|
+
if (!win) return;
|
|
377
|
+
// Re-seek to the current time to restore the paused timeline's render state.
|
|
378
|
+
// play() would start playback; pause() already stops. Seek re-renders at the
|
|
379
|
+
// current position without starting playback.
|
|
380
|
+
const t = win.__player?.getTime?.() ?? 0;
|
|
381
|
+
win.__player?.seek?.(t);
|
|
382
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// ── Design Panel Tokens (for inline style={{}} usage) ──────────────────
|
|
2
|
+
// Tailwind classes use `panel-*` from tailwind.config.js theme.extend.colors.
|
|
3
|
+
// This file provides the same values for inline styles where Tailwind can't reach.
|
|
4
|
+
|
|
5
|
+
export const P = {
|
|
6
|
+
accent: "#3CE6AC",
|
|
7
|
+
borderInput: "#27272A",
|
|
8
|
+
textMuted: "#52525B",
|
|
9
|
+
white: "#FAFAFA",
|
|
10
|
+
} as const;
|
|
@@ -71,7 +71,7 @@ function ColorSlider({
|
|
|
71
71
|
aria-valuemax={max}
|
|
72
72
|
aria-valuenow={value}
|
|
73
73
|
aria-disabled={disabled}
|
|
74
|
-
className={`relative h-4 rounded-full border border-neutral-700 shadow-[inset_0_1px_2px_rgba(0,0,0,0.55)] outline-none focus:border-
|
|
74
|
+
className={`relative h-4 rounded-full border border-neutral-700 shadow-[inset_0_1px_2px_rgba(0,0,0,0.55)] outline-none focus:border-panel-accent focus:ring-2 focus:ring-panel-accent/40 ${
|
|
75
75
|
disabled ? "cursor-not-allowed opacity-50" : "cursor-ew-resize"
|
|
76
76
|
}`}
|
|
77
77
|
style={{ background }}
|
|
@@ -294,7 +294,7 @@ export function ColorField({
|
|
|
294
294
|
<div className="truncate text-[11px] font-medium text-neutral-100">
|
|
295
295
|
{currentColor}
|
|
296
296
|
</div>
|
|
297
|
-
<div className="mt-0.5 text-[9px]
|
|
297
|
+
<div className="mt-0.5 text-[9px] text-neutral-600">
|
|
298
298
|
S {saturationPercent}% · B {brightnessPercent}% · A {alphaPercent}%
|
|
299
299
|
</div>
|
|
300
300
|
</div>
|
|
@@ -278,7 +278,7 @@ export function GradientField({
|
|
|
278
278
|
checked={parsed.repeating}
|
|
279
279
|
disabled={disabled}
|
|
280
280
|
onChange={(e) => patch({ repeating: e.target.checked })}
|
|
281
|
-
className="h-4 w-4 rounded border-neutral-700 bg-neutral-950 text-
|
|
281
|
+
className="h-4 w-4 rounded border-neutral-700 bg-neutral-950 text-panel-accent focus:ring-panel-accent"
|
|
282
282
|
/>
|
|
283
283
|
Repeat
|
|
284
284
|
</label>
|
|
@@ -41,6 +41,19 @@ export interface PropertyPanelProps {
|
|
|
41
41
|
onAddGsapFromProperty?: (animId: string, prop: string) => void;
|
|
42
42
|
onRemoveGsapFromProperty?: (animId: string, prop: string) => void;
|
|
43
43
|
onAddGsapAnimation?: (method: "to" | "from" | "set" | "fromTo") => void;
|
|
44
|
+
onSetArcPath?: (
|
|
45
|
+
animId: string,
|
|
46
|
+
config: {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
autoRotate?: boolean | number;
|
|
49
|
+
segments?: import("@hyperframes/core/gsap-parser").ArcPathSegment[];
|
|
50
|
+
},
|
|
51
|
+
) => void;
|
|
52
|
+
onUpdateArcSegment?: (
|
|
53
|
+
animId: string,
|
|
54
|
+
segmentIndex: number,
|
|
55
|
+
update: Partial<import("@hyperframes/core/gsap-parser").ArcPathSegment>,
|
|
56
|
+
) => void;
|
|
44
57
|
onAddKeyframe?: (
|
|
45
58
|
animationId: string,
|
|
46
59
|
percentage: number,
|
|
@@ -55,6 +68,9 @@ export interface PropertyPanelProps {
|
|
|
55
68
|
value: number | string,
|
|
56
69
|
) => Promise<void>;
|
|
57
70
|
onSeekToTime?: (time: number) => void;
|
|
71
|
+
recordingState?: "idle" | "recording" | "preview";
|
|
72
|
+
recordingDuration?: number;
|
|
73
|
+
onToggleRecording?: () => void;
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
/* ------------------------------------------------------------------ */
|
|
@@ -184,8 +200,8 @@ function fontSourceRank(source: FontSource): number {
|
|
|
184
200
|
/* ------------------------------------------------------------------ */
|
|
185
201
|
|
|
186
202
|
export const FIELD =
|
|
187
|
-
"min-w-0 rounded-
|
|
188
|
-
export const LABEL = "text-[11px] font-medium
|
|
203
|
+
"min-w-0 rounded-md bg-panel-input px-3 py-[7px] text-panel-text-1 transition-colors focus-within:ring-1 focus-within:ring-panel-accent/30";
|
|
204
|
+
export const LABEL = "text-[11px] font-medium text-panel-text-3";
|
|
189
205
|
export const RESPONSIVE_GRID = "grid grid-cols-[repeat(auto-fit,minmax(118px,1fr))] gap-3";
|
|
190
206
|
export const EMPTY_STYLES: Record<string, string> = {};
|
|
191
207
|
|
|
@@ -76,7 +76,7 @@ export function MediaSection({
|
|
|
76
76
|
{srcAttr && (
|
|
77
77
|
<div className="min-w-0">
|
|
78
78
|
<div className="flex items-center justify-between gap-2">
|
|
79
|
-
<div className="text-[
|
|
79
|
+
<div className="text-[11px] font-medium text-neutral-500">Source</div>
|
|
80
80
|
<button
|
|
81
81
|
type="button"
|
|
82
82
|
onClick={() => {
|