@hyperframes/studio 0.7.15 → 0.7.16

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 (65) hide show
  1. package/dist/assets/index-C4cUbqql.js +375 -0
  2. package/dist/assets/{index-CTXExGxO.js → index-D2SMcOdY.js} +1 -1
  3. package/dist/assets/index-DmkOvZns.css +1 -0
  4. package/dist/assets/{index-Db9KG66z.js → index-sCOjSz40.js} +1 -1
  5. package/dist/{chunk-UUEDZ4UJ.js → chunk-SBGXX7WY.js} +52 -22
  6. package/dist/chunk-SBGXX7WY.js.map +1 -0
  7. package/dist/{domEditingLayers-NWCBED4R.js → domEditingLayers-VZMLL4AP.js} +2 -4
  8. package/dist/index.d.ts +18 -1
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +3157 -2808
  11. package/dist/index.js.map +1 -1
  12. package/package.json +7 -7
  13. package/src/App.tsx +2 -0
  14. package/src/components/StudioPreviewArea.tsx +0 -41
  15. package/src/components/StudioRightPanel.tsx +2 -0
  16. package/src/components/TimelineToolbar.tsx +4 -4
  17. package/src/components/editor/DomEditOverlay.tsx +5 -2
  18. package/src/components/editor/GestureRecordControl.tsx +2 -2
  19. package/src/components/editor/GsapAnimationSection.tsx +3 -3
  20. package/src/components/editor/InspectorHeaderActions.tsx +62 -0
  21. package/src/components/editor/LayersPanel.tsx +35 -2
  22. package/src/components/editor/MotionPathOverlay.tsx +24 -8
  23. package/src/components/editor/PropertyPanel.tsx +48 -60
  24. package/src/components/editor/Transform3DCube.tsx +84 -85
  25. package/src/components/editor/domEditOverlayGeometry.ts +28 -0
  26. package/src/components/editor/domEditingDom.ts +36 -2
  27. package/src/components/editor/domEditingGroups.ts +41 -0
  28. package/src/components/editor/domEditingLayers.test.ts +97 -1
  29. package/src/components/editor/domEditingLayers.ts +13 -29
  30. package/src/components/editor/domEditingTypes.ts +3 -0
  31. package/src/components/editor/manualOffsetDrag.ts +23 -2
  32. package/src/components/editor/propertyPanel3dTransform.tsx +70 -30
  33. package/src/components/editor/propertyPanelHelpers.ts +2 -0
  34. package/src/components/editor/useDomEditOverlayRects.ts +8 -3
  35. package/src/components/editor/useMotionPathData.ts +47 -1
  36. package/src/contexts/DomEditContext.tsx +16 -0
  37. package/src/contexts/TimelineEditContext.tsx +0 -1
  38. package/src/hooks/gsapRuntimeBridge.ts +9 -2
  39. package/src/hooks/gsapTweenSynth.ts +64 -0
  40. package/src/hooks/useAnimatedPropertyCommit.ts +126 -26
  41. package/src/hooks/useAppHotkeys.ts +34 -3
  42. package/src/hooks/useDomEditSession.ts +48 -0
  43. package/src/hooks/useDomSelection.ts +73 -9
  44. package/src/hooks/useEnableKeyframes.test.ts +35 -0
  45. package/src/hooks/useEnableKeyframes.ts +29 -1
  46. package/src/hooks/useGroupCommits.ts +188 -0
  47. package/src/hooks/useGsapSelectionHandlers.ts +4 -0
  48. package/src/hooks/useGsapTweenCache.ts +56 -72
  49. package/src/hooks/usePreviewInteraction.ts +60 -2
  50. package/src/player/components/ShortcutsPanel.tsx +2 -0
  51. package/src/player/components/Timeline.tsx +0 -15
  52. package/src/player/components/TimelineCanvas.tsx +0 -13
  53. package/src/player/components/TimelineClipDiamonds.tsx +4 -125
  54. package/src/player/components/timelineCallbacks.ts +0 -1
  55. package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
  56. package/src/player/hooks/useExpandedTimelineElements.ts +47 -4
  57. package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -4
  58. package/src/player/store/playerStore.ts +20 -0
  59. package/src/utils/studioPreviewHelpers.ts +37 -0
  60. package/dist/assets/index-B7VWFKy1.js +0 -375
  61. package/dist/assets/index-svYFaNuq.css +0 -1
  62. package/dist/chunk-UUEDZ4UJ.js.map +0 -1
  63. package/src/components/editor/keyframeMove.test.ts +0 -101
  64. package/src/components/editor/keyframeMove.ts +0 -151
  65. /package/dist/{domEditingLayers-NWCBED4R.js.map → domEditingLayers-VZMLL4AP.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import { usePlayerStore, type TimelineElement } from "../store/playerStore";
2
+ import { usePlayerStore, type TimelineElement, type DomClipChild } from "../store/playerStore";
3
3
  import type { ClipManifestClip } from "../lib/playbackTypes";
4
4
  import { createTimelineElementFromManifestClip } from "../lib/timelineDOM";
5
5
  import { buildTimelineElementKey } from "../lib/timelineElementHelpers";
@@ -111,6 +111,32 @@ function buildChildElements(
111
111
  return result;
112
112
  }
113
113
 
114
+ // Sub-comp DOM children (groups/pills) aren't manifest clips and have no timing
115
+ // of their own — they're "always on" within their sub-comp host, so synthesize
116
+ // clips spanning the host's full bounds. The host element supplies start/duration
117
+ // and the composition file edits write to.
118
+ function domSiblingClips(
119
+ domClipChildren: DomClipChild[],
120
+ siblingParentId: string,
121
+ host: TimelineElement,
122
+ ): ClipManifestClip[] {
123
+ return domClipChildren
124
+ .filter((c) => c.parentId === siblingParentId)
125
+ .map((c) => ({
126
+ id: c.id,
127
+ label: c.label,
128
+ start: host.start,
129
+ duration: host.duration,
130
+ track: host.track,
131
+ kind: "element" as const,
132
+ tagName: null,
133
+ compositionId: null,
134
+ parentCompositionId: host.id ?? null,
135
+ compositionSrc: host.compositionSrc ?? null,
136
+ assetUrl: null,
137
+ }));
138
+ }
139
+
114
140
  // Exported for tests.
115
141
  export function buildExpandedElements(
116
142
  elements: TimelineElement[],
@@ -118,11 +144,20 @@ export function buildExpandedElements(
118
144
  parentMap: Map<string, string>,
119
145
  topLevelId: string,
120
146
  siblingParentId: string,
147
+ domClipChildren: DomClipChild[] = [],
121
148
  ): TimelineElement[] {
122
149
  const topLevelElement = elements.find((el) => el.id === topLevelId || el.domId === topLevelId);
123
150
  if (!topLevelElement) return filterToTopLevel(elements, parentMap);
124
151
 
125
- const siblings = manifest.filter((c) => c.id != null && parentMap.get(c.id) === siblingParentId);
152
+ // Prefer real manifest children; fall back to DOM-only sub-comp children
153
+ // (groups/pills) that have no data-start and thus never enter the manifest.
154
+ const siblings = (() => {
155
+ const fromManifest = manifest.filter(
156
+ (c) => c.id != null && parentMap.get(c.id) === siblingParentId,
157
+ );
158
+ if (fromManifest.length > 0) return fromManifest;
159
+ return domSiblingClips(domClipChildren, siblingParentId, topLevelElement);
160
+ })();
126
161
  if (siblings.length === 0) return filterToTopLevel(elements, parentMap);
127
162
 
128
163
  // The sub-comp host the children actually live in: top-level host for 1-level
@@ -154,6 +189,7 @@ export function useExpandedTimelineElements(): TimelineElement[] {
154
189
  const elements = usePlayerStore((s) => s.elements);
155
190
  const clipManifest = usePlayerStore((s) => s.clipManifest);
156
191
  const clipParentMap = usePlayerStore((s) => s.clipParentMap);
192
+ const domClipChildren = usePlayerStore((s) => s.domClipChildren);
157
193
  const selectedElementId = usePlayerStore((s) => s.selectedElementId);
158
194
 
159
195
  return useMemo(() => {
@@ -166,6 +202,13 @@ export function useExpandedTimelineElements(): TimelineElement[] {
166
202
 
167
203
  const immediateParent = clipParentMap.get(rawId)!;
168
204
  const topLevel = findTopLevelAncestor(rawId, clipParentMap) ?? immediateParent;
169
- return buildExpandedElements(elements, clipManifest, clipParentMap, topLevel, immediateParent);
170
- }, [elements, clipManifest, clipParentMap, selectedElementId]);
205
+ return buildExpandedElements(
206
+ elements,
207
+ clipManifest,
208
+ clipParentMap,
209
+ topLevel,
210
+ immediateParent,
211
+ domClipChildren,
212
+ );
213
+ }, [elements, clipManifest, clipParentMap, domClipChildren, selectedElementId]);
171
214
  }
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { useCallback } from "react";
12
12
  import { liveTime, usePlayerStore } from "../store/playerStore";
13
- import type { TimelineElement } from "../store/playerStore";
13
+ import type { TimelineElement, DomClipChild } from "../store/playerStore";
14
14
  import type { PlaybackAdapter, ClipManifestClip, IframeWindow } from "../lib/playbackTypes";
15
15
  import {
16
16
  parseTimelineFromDOM,
@@ -85,8 +85,8 @@ export function useTimelineSyncCallbacks({
85
85
  | (Window & { __clipTree?: import("@hyperframes/core/runtime/clipTree").ClipTree })
86
86
  | null;
87
87
  const clipTree = iframeWin?.__clipTree;
88
+ const parentMap = new Map<string, string>();
88
89
  if (clipTree) {
89
- const parentMap = new Map<string, string>();
90
90
  const walk = (nodes: typeof clipTree.roots) => {
91
91
  for (const node of nodes) {
92
92
  if (node.id && node.parentId) parentMap.set(node.id, node.parentId);
@@ -94,11 +94,50 @@ export function useTimelineSyncCallbacks({
94
94
  }
95
95
  };
96
96
  walk(clipTree.roots);
97
- usePlayerStore.getState().setClipParentMap(parentMap);
98
97
  }
98
+
99
+ // Descend into each sub-composition host: its internal elements (group
100
+ // wrappers + their children) carry no `data-start`, so the clip
101
+ // tree/manifest never enumerate them. Surface them studio-side as DOM
102
+ // children + parent links so the timeline can expand a sub-comp/group
103
+ // row to show them. Manifest stays lean (timed clips only).
104
+ const domClipChildren: DomClipChild[] = [];
105
+ if (iframeDoc) {
106
+ for (const clip of data.clips) {
107
+ if (clip.kind !== "composition" || !clip.id) continue;
108
+ const hostEl = iframeDoc.getElementById(clip.id);
109
+ if (!hostEl) continue;
110
+ const hostId = clip.id;
111
+ const innerRoot = hostEl.querySelector("[data-hf-inner-root]") ?? hostEl;
112
+ // Collect the sub-comp's id'd descendants (grouped OR ungrouped) so they
113
+ // expand into timeline rows. Descends through id-less structural wrappers
114
+ // (the inlined sub-comp body), and one level into groups for drill-in.
115
+ const collect = (parentEl: Element, parentId: string) => {
116
+ for (const child of Array.from(parentEl.children)) {
117
+ if (!child.id) {
118
+ collect(child, parentId); // unwrap id-less structural containers
119
+ continue;
120
+ }
121
+ const isGroup = child.hasAttribute("data-hf-group");
122
+ domClipChildren.push({
123
+ id: child.id,
124
+ parentId,
125
+ hostId,
126
+ label: isGroup ? child.getAttribute("data-hf-group") || child.id : child.id,
127
+ });
128
+ parentMap.set(child.id, parentId);
129
+ if (isGroup) collect(child, child.id);
130
+ }
131
+ };
132
+ collect(innerRoot, hostId);
133
+ }
134
+ }
135
+ usePlayerStore.getState().setClipParentMap(parentMap);
136
+ usePlayerStore.getState().setDomClipChildren(domClipChildren);
99
137
  } catch {
100
- // cross-origin or __clipTree not available — parentMap stays empty
138
+ // cross-origin or __clipTree not available — maps stay empty
101
139
  }
140
+
102
141
  const usedHostEls = new Set<Element>();
103
142
  const els: TimelineElement[] = filtered.map((clip, index) => {
104
143
  const hostEl = iframeDoc
@@ -165,6 +165,23 @@ interface PlayerState {
165
165
  setClipManifest: (clips: ClipManifestClip[] | null) => void;
166
166
  clipParentMap: Map<string, string>;
167
167
  setClipParentMap: (map: Map<string, string>) => void;
168
+ /**
169
+ * Sub-composition DOM descendants (groups + their children) that have no
170
+ * `data-start`, so they're absent from the clip manifest/tree. Collected
171
+ * studio-side from the live preview so the timeline can expand a sub-comp row
172
+ * to show its DOM-only children. Keeps the manifest lean (timed clips only).
173
+ */
174
+ domClipChildren: DomClipChild[];
175
+ setDomClipChildren: (children: DomClipChild[]) => void;
176
+ }
177
+
178
+ /** A sub-comp DOM-only timeline child (no data-start) and its nesting context. */
179
+ export interface DomClipChild {
180
+ id: string;
181
+ parentId: string;
182
+ /** The manifest sub-comp host clip id this descendant ultimately lives under. */
183
+ hostId: string;
184
+ label: string;
168
185
  }
169
186
 
170
187
  interface BeatHistoryEntry {
@@ -296,6 +313,8 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
296
313
  setClipManifest: (clips) => set({ clipManifest: clips }),
297
314
  clipParentMap: new Map(),
298
315
  setClipParentMap: (map) => set({ clipParentMap: map }),
316
+ domClipChildren: [],
317
+ setDomClipChildren: (children) => set({ domClipChildren: children }),
299
318
 
300
319
  setIsPlaying: (playing) => {
301
320
  if (get().isPlaying === playing) return;
@@ -380,6 +399,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
380
399
  beatPersist: null,
381
400
  clipManifest: null,
382
401
  clipParentMap: new Map(),
402
+ domClipChildren: [],
383
403
  }),
384
404
  }));
385
405
 
@@ -81,6 +81,37 @@ function removePointerEventsOverride(style: HTMLStyleElement | null): void {
81
81
  }
82
82
  }
83
83
 
84
+ // Animated group members can move outside their wrapper's static layout box, so
85
+ // the empty space inside a group's *visual* bounds (the member-union the overlay
86
+ // draws) doesn't hit-test to the group via elementsFromPoint. Recover it: if the
87
+ // point falls within a group's live member-union rect, return that wrapper.
88
+ // Innermost (smallest-area) group wins for nested groups.
89
+ function findGroupAtPoint(doc: Document, x: number, y: number): HTMLElement | null {
90
+ let best: HTMLElement | null = null;
91
+ let bestArea = Infinity;
92
+ for (const group of Array.from(doc.querySelectorAll<HTMLElement>("[data-hf-group]"))) {
93
+ let left = Infinity;
94
+ let top = Infinity;
95
+ let right = -Infinity;
96
+ let bottom = -Infinity;
97
+ for (const member of Array.from(group.children)) {
98
+ const r = member.getBoundingClientRect();
99
+ if (r.width === 0 && r.height === 0) continue;
100
+ left = Math.min(left, r.left);
101
+ top = Math.min(top, r.top);
102
+ right = Math.max(right, r.right);
103
+ bottom = Math.max(bottom, r.bottom);
104
+ }
105
+ if (right < left || x < left || x > right || y < top || y > bottom) continue;
106
+ const area = (right - left) * (bottom - top);
107
+ if (area < bestArea) {
108
+ bestArea = area;
109
+ best = group;
110
+ }
111
+ }
112
+ return best;
113
+ }
114
+
84
115
  // fallow-ignore-next-line complexity
85
116
  export function getPreviewTargetFromPointer(
86
117
  iframe: HTMLIFrameElement,
@@ -113,6 +144,12 @@ export function getPreviewTargetFromPointer(
113
144
  if (visualTarget) return visualTarget;
114
145
  }
115
146
 
147
+ // No element hit (e.g. empty space inside an animated group's overlay) — fall
148
+ // back to the group whose member-union contains the point, so the whole group
149
+ // area is hoverable/selectable, not just where a member currently sits.
150
+ const groupHit = findGroupAtPoint(doc, localPointer.x, localPointer.y);
151
+ if (groupHit && getDomLayerPatchTarget(groupHit, activeCompositionPath)) return groupHit;
152
+
116
153
  const fallback = getEventTargetElement(doc.elementFromPoint(localPointer.x, localPointer.y));
117
154
  if (!fallback || !getDomLayerPatchTarget(fallback, activeCompositionPath)) return null;
118
155
  if (!isElementComputedVisible(fallback)) return null;