@hyperframes/studio 0.7.75 → 0.7.77

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 (134) hide show
  1. package/dist/assets/{hyperframes-player-BrfzYqX3.js → hyperframes-player-CEggaaxR.js} +1 -1
  2. package/dist/assets/{index-BvGKYVrH.js → index-B1INQNmj.js} +1 -1
  3. package/dist/assets/{index-CjGZkNxS.js → index-B1Tjjdse.js} +1 -1
  4. package/dist/assets/index-Bp4jAYZG.js +428 -0
  5. package/dist/assets/index-CBAfprvx.css +1 -0
  6. package/dist/index.d.ts +64 -17
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +18040 -16293
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +16 -17
  12. package/src/components/EditorShell.tsx +3 -0
  13. package/src/components/TimelineToolbar.test.tsx +48 -3
  14. package/src/components/TimelineToolbar.tsx +89 -36
  15. package/src/components/editor/AnimationCard.test.tsx +18 -75
  16. package/src/components/editor/AnimationCard.tsx +26 -2
  17. package/src/components/editor/GsapAddAnimationControl.tsx +68 -0
  18. package/src/components/editor/GsapAnimationSection.tsx +18 -157
  19. package/src/components/editor/KeyframeEaseList.tsx +5 -1
  20. package/src/components/editor/KeyframeNavigation.tsx +46 -14
  21. package/src/components/editor/KeyframeNavigationDiamond.test.tsx +59 -0
  22. package/src/components/editor/MotionPathOverlay.tsx +32 -10
  23. package/src/components/editor/PropertyPanel.tsx +15 -5
  24. package/src/components/editor/PropertyPanelFlat.tsx +48 -23
  25. package/src/components/editor/gsapAnimationCallbacks.test.ts +122 -0
  26. package/src/components/editor/gsapAnimationCallbacks.ts +108 -4
  27. package/src/components/editor/keyframeRetime.test.ts +25 -22
  28. package/src/components/editor/keyframeRetime.ts +1 -2
  29. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +2 -2
  30. package/src/components/editor/propertyPanelFlatMotionSection.tsx +25 -139
  31. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +32 -16
  32. package/src/components/editor/propertyPanelFlatTextSection.tsx +21 -4
  33. package/src/components/nle/PreviewOverlays.tsx +3 -1
  34. package/src/components/nle/useTimelineEditCallbacks.test.ts +30 -0
  35. package/src/components/nle/useTimelineEditCallbacks.test.tsx +693 -0
  36. package/src/components/nle/useTimelineEditCallbacks.ts +197 -57
  37. package/src/contexts/TimelineEditContext.tsx +1 -1
  38. package/src/hooks/deleteSelectedKeyframes.ts +24 -8
  39. package/src/hooks/gsapDragCommit.ts +2 -2
  40. package/src/hooks/gsapDragPositionCommit.ts +84 -5
  41. package/src/hooks/gsapKeyframeCacheHelpers.test.ts +176 -1
  42. package/src/hooks/gsapKeyframeCacheHelpers.ts +93 -52
  43. package/src/hooks/gsapRuntimeBridge.test.ts +100 -0
  44. package/src/hooks/gsapScriptCommitHelpers.ts +3 -2
  45. package/src/hooks/gsapScriptCommitTypes.ts +2 -0
  46. package/src/hooks/gsapShared.test.ts +109 -1
  47. package/src/hooks/gsapShared.ts +142 -17
  48. package/src/hooks/gsapTweenSynth.test.ts +30 -1
  49. package/src/hooks/gsapTweenSynth.ts +61 -14
  50. package/src/hooks/keyframeCacheAstLoad.ts +181 -0
  51. package/src/hooks/timelineEditingHelpers.test.ts +80 -0
  52. package/src/hooks/timelineMoveAdapter.ts +9 -2
  53. package/src/hooks/useDomEditWiring.ts +2 -2
  54. package/src/hooks/useDomSelection.ts +4 -0
  55. package/src/hooks/useEnableKeyframes.test.ts +122 -0
  56. package/src/hooks/useEnableKeyframes.ts +116 -56
  57. package/src/hooks/useGestureCommit.ts +2 -2
  58. package/src/hooks/useGsapAnimationFetchFallback.ts +1 -1
  59. package/src/hooks/useGsapKeyframeOps.test.tsx +92 -13
  60. package/src/hooks/useGsapKeyframeOps.ts +30 -13
  61. package/src/hooks/useGsapScriptCommits.test.tsx +21 -0
  62. package/src/hooks/useGsapScriptCommits.ts +2 -0
  63. package/src/hooks/useGsapSelectionHandlers.test.tsx +69 -3
  64. package/src/hooks/useGsapSelectionHandlers.ts +90 -65
  65. package/src/hooks/useGsapTweenCache.test.ts +16 -0
  66. package/src/hooks/useGsapTweenCache.ts +64 -178
  67. package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +68 -0
  68. package/src/hooks/useStudioContextValue.test.ts +93 -0
  69. package/src/hooks/useStudioContextValue.ts +17 -3
  70. package/src/hooks/useStudioTestHooks.ts +63 -0
  71. package/src/hooks/useTimelineEditing.test.tsx +187 -22
  72. package/src/hooks/useTimelineEditing.ts +43 -32
  73. package/src/hooks/useTimelineEditingTypes.ts +2 -0
  74. package/src/hooks/useTimelineGroupEditing.ts +106 -82
  75. package/src/player/components/KeyframeDiamondContextMenu.test.tsx +69 -0
  76. package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -8
  77. package/src/player/components/LayerDisclosureRow.tsx +76 -0
  78. package/src/player/components/Timeline.test.ts +420 -90
  79. package/src/player/components/Timeline.tsx +80 -78
  80. package/src/player/components/TimelineCanvas.tsx +33 -21
  81. package/src/player/components/TimelineClip.tsx +3 -1
  82. package/src/player/components/TimelineClipDiamonds.test.tsx +540 -16
  83. package/src/player/components/TimelineClipDiamonds.tsx +320 -117
  84. package/src/player/components/TimelineDiamondConnectors.tsx +145 -0
  85. package/src/player/components/TimelineLanes.tsx +153 -79
  86. package/src/player/components/TimelineOverlays.tsx +4 -18
  87. package/src/player/components/TimelinePropertyLanes.test.tsx +450 -0
  88. package/src/player/components/TimelinePropertyLanes.tsx +191 -0
  89. package/src/player/components/TimelineRazorInteraction.tsx +60 -0
  90. package/src/player/components/TimelineRuler.tsx +11 -21
  91. package/src/player/components/TimelineTrackHeader.test.tsx +346 -0
  92. package/src/player/components/TimelineTrackHeader.tsx +363 -0
  93. package/src/player/components/TrackClipCount.tsx +17 -0
  94. package/src/player/components/timelineCallbacks.ts +21 -6
  95. package/src/player/components/timelineClipDragPreview.test.ts +50 -3
  96. package/src/player/components/timelineClipDragPreview.ts +33 -15
  97. package/src/player/components/timelineCollision.ts +4 -13
  98. package/src/player/components/timelineDiamondTypes.ts +120 -0
  99. package/src/player/components/timelineDragDrop.ts +8 -3
  100. package/src/player/components/timelineEditing.test.ts +19 -27
  101. package/src/player/components/timelineEditing.ts +7 -8
  102. package/src/player/components/timelineKeyframeIdentity.test.ts +49 -0
  103. package/src/player/components/timelineKeyframeIdentity.ts +66 -0
  104. package/src/player/components/timelineLayout.test.ts +162 -9
  105. package/src/player/components/timelineLayout.ts +198 -38
  106. package/src/player/components/timelineMarquee.test.ts +76 -21
  107. package/src/player/components/timelineMarquee.ts +17 -14
  108. package/src/player/components/trackHeaderLaneState.ts +121 -0
  109. package/src/player/components/trackHeaderLaneValues.ts +139 -0
  110. package/src/player/components/useAutoExpandKeyframedClips.test.tsx +75 -0
  111. package/src/player/components/useAutoExpandKeyframedClips.ts +50 -0
  112. package/src/player/components/useTimelineClipDrag.ts +4 -1
  113. package/src/player/components/useTimelineGeometry.ts +4 -1
  114. package/src/player/components/useTimelineKeyframeHandlers.test.tsx +108 -0
  115. package/src/player/components/useTimelineKeyframeHandlers.ts +50 -15
  116. package/src/player/components/useTimelinePlayhead.ts +21 -12
  117. package/src/player/components/useTimelineRangeSelection.ts +35 -14
  118. package/src/player/components/useTimelineTrackLayout.test.ts +53 -0
  119. package/src/player/components/useTimelineTrackLayout.ts +162 -0
  120. package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
  121. package/src/player/hooks/useExpandedTimelineElements.ts +21 -12
  122. package/src/player/lib/timelineElementHelpers.ts +26 -11
  123. package/src/player/store/keyframeSlice.ts +122 -0
  124. package/src/player/store/playerStore.test.ts +25 -0
  125. package/src/player/store/playerStore.ts +7 -43
  126. package/src/telemetry/events.test.ts +9 -3
  127. package/src/telemetry/events.ts +16 -7
  128. package/src/utils/globalTimeCompiler.test.ts +10 -0
  129. package/src/utils/globalTimeCompiler.ts +3 -3
  130. package/dist/assets/index-B37rWXo4.css +0 -1
  131. package/dist/assets/index-DzDajONI.js +0 -428
  132. package/src/player/components/TimelineDragGhost.tsx +0 -59
  133. package/src/utils/keyframeSelection.test.ts +0 -45
  134. package/src/utils/keyframeSelection.ts +0 -29
@@ -0,0 +1,69 @@
1
+ // @vitest-environment happy-dom
2
+ import { act } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { TimelineElement } from "../store/playerStore";
6
+ import {
7
+ KeyframeDiamondContextMenu,
8
+ type KeyframeDiamondContextMenuState,
9
+ } from "./KeyframeDiamondContextMenu";
10
+
11
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
12
+
13
+ const element = { id: "box", start: 0, duration: 2, track: 0 } as unknown as TimelineElement;
14
+
15
+ const state: KeyframeDiamondContextMenuState = {
16
+ x: 10,
17
+ y: 10,
18
+ element,
19
+ elementId: "box",
20
+ percentage: 50,
21
+ tweenPercentage: 25,
22
+ propertyGroup: "position",
23
+ animationId: "box-to-1-position",
24
+ };
25
+
26
+ function clickMenuItem(label: string, props: Partial<Record<string, unknown>>) {
27
+ const host = document.createElement("div");
28
+ document.body.appendChild(host);
29
+ const root = createRoot(host);
30
+ act(() =>
31
+ root.render(
32
+ <KeyframeDiamondContextMenu
33
+ state={state}
34
+ onClose={() => {}}
35
+ onDelete={vi.fn()}
36
+ onDeleteAll={vi.fn()}
37
+ {...props}
38
+ />,
39
+ ),
40
+ );
41
+ const button = Array.from(document.body.querySelectorAll("button")).find(
42
+ (candidate) => candidate.textContent === label,
43
+ );
44
+ act(() => button?.click());
45
+ act(() => root.unmount());
46
+ host.remove();
47
+ }
48
+
49
+ describe("KeyframeDiamondContextMenu", () => {
50
+ // Two animations can carry a keyframe at the same clip percentage. Dropping the
51
+ // property group / tween percentage / animation id here sends the mutation back
52
+ // to first-match-by-percentage, which retimes or deletes the wrong tween.
53
+ it("hands every action the full keyframe identity, not just the percentage", () => {
54
+ const onDelete = vi.fn();
55
+ const onMoveToPlayhead = vi.fn();
56
+
57
+ clickMenuItem("Delete Keyframe", { onDelete });
58
+ clickMenuItem("Move to Playhead", { onMoveToPlayhead });
59
+
60
+ const target = {
61
+ percentage: 50,
62
+ tweenPercentage: 25,
63
+ propertyGroup: "position",
64
+ animationId: "box-to-1-position",
65
+ };
66
+ expect(onDelete).toHaveBeenCalledWith("box", target);
67
+ expect(onMoveToPlayhead).toHaveBeenCalledWith(element, target);
68
+ });
69
+ });
@@ -1,25 +1,28 @@
1
1
  import { memo } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
4
+ import type { TimelineElement } from "../store/playerStore";
5
+ import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
4
6
 
5
7
  export interface KeyframeDiamondContextMenuState {
6
8
  x: number;
7
9
  y: number;
10
+ element: TimelineElement;
8
11
  elementId: string;
9
12
  percentage: number;
10
13
  tweenPercentage?: number;
14
+ propertyGroup?: string;
15
+ animationId?: string;
11
16
  currentEase?: string;
12
17
  }
13
18
 
14
19
  interface KeyframeDiamondContextMenuProps {
15
20
  state: KeyframeDiamondContextMenuState;
16
21
  onClose: () => void;
17
- onDelete: (elementId: string, percentage: number) => void;
18
- onDeleteAll: (elementId: string) => void;
19
- onChangeEase?: (elementId: string, percentage: number, ease: string) => void;
20
- onCopyProperties?: (elementId: string, percentage: number) => void;
22
+ onDelete: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
23
+ onDeleteAll: (element: TimelineElement) => void;
21
24
  /** Retime the keyframe to the current playhead, preserving its value + ease. */
22
- onMoveToPlayhead?: (elementId: string, fromPercentage: number) => void;
25
+ onMoveToPlayhead?: (element: TimelineElement, keyframe: TimelineKeyframeTarget) => void;
23
26
  }
24
27
 
25
28
  export const KeyframeDiamondContextMenu = memo(function KeyframeDiamondContextMenu({
@@ -30,6 +33,14 @@ export const KeyframeDiamondContextMenu = memo(function KeyframeDiamondContextMe
30
33
  onMoveToPlayhead,
31
34
  }: KeyframeDiamondContextMenuProps) {
32
35
  const menuRef = useContextMenuDismiss(onClose);
36
+ // The clicked diamond's identity, built once: the menu's two mutating entries
37
+ // both act on it, and they must not disagree about which keyframe was clicked.
38
+ const keyframe: TimelineKeyframeTarget = {
39
+ percentage: state.percentage,
40
+ tweenPercentage: state.tweenPercentage,
41
+ propertyGroup: state.propertyGroup,
42
+ animationId: state.animationId,
43
+ };
33
44
 
34
45
  const menuWidth = 200;
35
46
  const menuHeight = onMoveToPlayhead ? 100 : 70;
@@ -51,7 +62,7 @@ export const KeyframeDiamondContextMenu = memo(function KeyframeDiamondContextMe
51
62
  // Pass clip-% — resolveKeyframeTarget keys the cache lookup on clip-%
52
63
  // and returns the tween-% for the mutation. Passing tween-% here would
53
64
  // miss the lookup on any tween whose window is shorter than the clip.
54
- onMoveToPlayhead(state.elementId, state.percentage);
65
+ onMoveToPlayhead(state.element, keyframe);
55
66
  onClose();
56
67
  }}
57
68
  >
@@ -64,7 +75,7 @@ export const KeyframeDiamondContextMenu = memo(function KeyframeDiamondContextMe
64
75
  type="button"
65
76
  className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 cursor-pointer text-left"
66
77
  onClick={() => {
67
- onDelete(state.elementId, state.percentage);
78
+ onDelete(state.elementId, keyframe);
68
79
  onClose();
69
80
  }}
70
81
  >
@@ -75,7 +86,7 @@ export const KeyframeDiamondContextMenu = memo(function KeyframeDiamondContextMe
75
86
  type="button"
76
87
  className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 cursor-pointer text-left"
77
88
  onClick={() => {
78
- onDeleteAll(state.elementId);
89
+ onDeleteAll(state.element);
79
90
  onClose();
80
91
  }}
81
92
  >
@@ -0,0 +1,76 @@
1
+ import { CaretRight } from "@phosphor-icons/react";
2
+ import type { TimelineElement } from "../store/playerStore";
3
+ import { TRACK_H } from "./timelineLayout";
4
+ import { TrackClipCount } from "./TrackClipCount";
5
+
6
+ // Layer row (Figma order: disclosure ▸/▾, diamond, name) — the disclosure lives
7
+ // here, not on the clip bar, and re-expands a collapsed layer.
8
+ export function LayerDisclosureRow({
9
+ keyframeClip,
10
+ clipCount,
11
+ isExpanded,
12
+ gutterBackground,
13
+ columnWidth,
14
+ lanesId,
15
+ onToggleClipExpanded,
16
+ children,
17
+ }: {
18
+ keyframeClip: TimelineElement;
19
+ clipCount: number;
20
+ isExpanded: boolean;
21
+ gutterBackground: string;
22
+ /** Same adaptive width the lane rows use: a narrowed header column must not
23
+ * leave this row hanging over the clips it labels. */
24
+ columnWidth: number;
25
+ /** Id of the element holding the lanes this row's caret expands. */
26
+ lanesId: string;
27
+ onToggleClipExpanded: () => void;
28
+ /** Trailing controls that act on the LAYER (the visibility eye), not on a lane. */
29
+ children?: React.ReactNode;
30
+ }) {
31
+ const name = keyframeClip.label ?? keyframeClip.domId ?? keyframeClip.id;
32
+ return (
33
+ <div
34
+ className="absolute left-0 top-0 flex items-center gap-1.5 overflow-hidden px-1.5 text-[11px]"
35
+ style={{
36
+ width: columnWidth,
37
+ height: TRACK_H,
38
+ color: "#ffffff",
39
+ background: gutterBackground,
40
+ }}
41
+ >
42
+ <button
43
+ type="button"
44
+ aria-expanded={isExpanded}
45
+ aria-controls={lanesId}
46
+ aria-label={`${isExpanded ? "Collapse" : "Expand"} ${name} keyframes`}
47
+ title={`${isExpanded ? "Collapse" : "Expand"} keyframe lanes`}
48
+ // h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The caret glyph stays 11px;
49
+ // only the hit box grows.
50
+ className="flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-white/55 hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
51
+ onPointerDown={(event) => event.stopPropagation()}
52
+ onClick={(event) => {
53
+ event.stopPropagation();
54
+ onToggleClipExpanded();
55
+ }}
56
+ >
57
+ <CaretRight
58
+ size={11}
59
+ weight="bold"
60
+ aria-hidden="true"
61
+ style={{ transform: isExpanded ? "rotate(90deg)" : undefined }}
62
+ />
63
+ </button>
64
+ {/* Decorative: the disclosure button above already names the row's keyframe
65
+ state, and aria-label on a plain span is not exposed reliably anyway. */}
66
+ <span aria-hidden="true" className="shrink-0 text-[13px] leading-none text-white/40">
67
+
68
+ </span>
69
+ <span className="min-w-0 flex-1 truncate font-medium" title={name}>
70
+ {name}
71
+ </span>
72
+ <TrackClipCount clipCount={clipCount} />
73
+ {children}
74
+ </div>
75
+ );
76
+ }