@hyperframes/studio 0.7.82 → 0.7.83

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 (39) hide show
  1. package/dist/assets/{hyperframes-player-HOyDfIO9.js → hyperframes-player-DSRuJMfh.js} +1 -1
  2. package/dist/assets/{index-C0c6LmEC.js → index-BrWVfpCQ.js} +1 -1
  3. package/dist/assets/{index-DlPIfJDz.js → index-DRtvHA1J.js} +150 -150
  4. package/dist/assets/{index-fH499K3b.js → index-YZR6OfQ5.js} +1 -1
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +3053 -3091
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -7
  9. package/src/App.tsx +2 -5
  10. package/src/components/StudioHeader.tsx +3 -18
  11. package/src/components/StudioRightPanel.tsx +14 -22
  12. package/src/components/TimelineToolbar.tsx +120 -132
  13. package/src/components/editor/EaseCurveSection.tsx +9 -90
  14. package/src/components/editor/EaseModeControls.tsx +110 -0
  15. package/src/components/editor/PropertyPanel.test.tsx +2 -3
  16. package/src/components/editor/PropertyPanel.tsx +7 -12
  17. package/src/components/editor/PropertyPanelFlat.tsx +0 -2
  18. package/src/components/editor/floatingPanel.test.ts +20 -1
  19. package/src/components/editor/floatingPanel.ts +15 -0
  20. package/src/components/editor/manualEditingAvailability.test.ts +13 -40
  21. package/src/components/editor/manualEditingAvailability.ts +0 -41
  22. package/src/components/editor/propertyPanel3dTransform.tsx +1 -2
  23. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +1 -2
  24. package/src/components/nle/PreviewOverlays.tsx +8 -20
  25. package/src/components/renders/RenderQueue.tsx +54 -23
  26. package/src/components/sidebar/LeftSidebar.tsx +15 -22
  27. package/src/components/ui/Tooltip.tsx +25 -7
  28. package/src/hooks/useAppHotkeys.ts +1 -2
  29. package/src/hooks/useDomEditPreviewSync.ts +1 -2
  30. package/src/hooks/useDomEditWiring.ts +2 -3
  31. package/src/hooks/useDomSelection.ts +0 -27
  32. package/src/hooks/usePreviewInteraction.ts +2 -3
  33. package/src/hooks/useStudioContextValue.ts +3 -7
  34. package/src/player/components/Timeline.tsx +1 -2
  35. package/src/player/components/TimelineLanes.tsx +33 -35
  36. package/src/player/components/useAutoExpandKeyframedClips.ts +0 -2
  37. package/src/player/components/useTimelineTrackLayout.ts +1 -5
  38. package/src/utils/studioUrlState.test.ts +5 -8
  39. package/src/utils/studioUrlState.ts +1 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.7.82",
3
+ "version": "0.7.83",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,11 +46,11 @@
46
46
  "gsap": "^3.13.0",
47
47
  "marked": "^14.1.4",
48
48
  "mediabunny": "^1.45.3",
49
- "@hyperframes/core": "0.7.82",
50
- "@hyperframes/parsers": "0.7.82",
51
- "@hyperframes/player": "0.7.82",
52
- "@hyperframes/sdk": "0.7.82",
53
- "@hyperframes/studio-server": "0.7.82"
49
+ "@hyperframes/core": "0.7.83",
50
+ "@hyperframes/sdk": "0.7.83",
51
+ "@hyperframes/parsers": "0.7.83",
52
+ "@hyperframes/studio-server": "0.7.83",
53
+ "@hyperframes/player": "0.7.83"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "19",
@@ -65,7 +65,7 @@
65
65
  "vite": "^6.4.2",
66
66
  "vitest": "^3.2.4",
67
67
  "zustand": "^5.0.0",
68
- "@hyperframes/producer": "0.7.82"
68
+ "@hyperframes/producer": "0.7.83"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19",
package/src/App.tsx CHANGED
@@ -43,7 +43,6 @@ import {
43
43
  import type { DomEditSelection } from "./components/editor/domEditing";
44
44
  import { StudioHeader } from "./components/StudioHeader";
45
45
  import { useGestureCommit } from "./hooks/useGestureCommit";
46
- import { STUDIO_KEYFRAMES_ENABLED } from "./components/editor/manualEditingAvailability";
47
46
  import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
48
47
  import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
49
48
  import { EditorShell } from "./components/EditorShell";
@@ -263,9 +262,7 @@ export function StudioApp() {
263
262
  onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
264
263
  activeCompPath,
265
264
  forceReloadSdkSession: sdkHandle.forceReload,
266
- onToggleRecording: STUDIO_KEYFRAMES_ENABLED
267
- ? () => handleToggleRecordingRef.current()
268
- : undefined,
265
+ onToggleRecording: () => handleToggleRecordingRef.current(),
269
266
  });
270
267
  const sidebarTabRef = useRef({
271
268
  select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t),
@@ -367,7 +364,7 @@ export function StudioApp() {
367
364
  isGestureRecordingRef,
368
365
  });
369
366
  handleToggleRecordingRef.current = handleToggleRecording;
370
- const recordingToggle = STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : undefined;
367
+ const recordingToggle = handleToggleRecording;
371
368
  const canvasRectRef = useRef<DOMRect | null>(null);
372
369
  useLayoutEffect(() => {
373
370
  if (gestureState !== "recording" || !previewIframe) {
@@ -1,9 +1,5 @@
1
1
  import { useRef, type MouseEvent } from "react";
2
2
  import { RotateCcw, RotateCw, Camera } from "../icons/SystemIcons";
3
- import {
4
- STUDIO_INSPECTOR_PANELS_ENABLED,
5
- STUDIO_MANUAL_EDITING_DISABLED_TITLE,
6
- } from "./editor/manualEditingAvailability";
7
3
  import { getHistoryShortcutLabel } from "../utils/studioHelpers";
8
4
  import { useStudioShellContext } from "../contexts/StudioContext";
9
5
  import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
@@ -328,16 +324,10 @@ export function StudioHeader({
328
324
  <span>{capturing ? "Capturing…" : "Capture"}</span>
329
325
  </a>
330
326
  </Tooltip>
331
- <Tooltip
332
- label={
333
- STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
334
- }
335
- side="bottom"
336
- >
327
+ <Tooltip label="Inspector" side="bottom">
337
328
  <button
338
329
  type="button"
339
330
  onClick={() => {
340
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
341
331
  if (rightCollapsed || !inspectorPanelActive) {
342
332
  trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
343
333
  setRightPanelTab("design");
@@ -349,18 +339,13 @@ export function StudioHeader({
349
339
  // the panel shouldn't deselect the element.
350
340
  setRightCollapsed(true);
351
341
  }}
352
- disabled={!STUDIO_INSPECTOR_PANELS_ENABLED}
353
342
  aria-pressed={inspectorButtonActive}
354
343
  className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors active:scale-[0.98] ${
355
344
  inspectorButtonActive
356
345
  ? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
357
- : STUDIO_INSPECTOR_PANELS_ENABLED
358
- ? "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
359
- : "cursor-not-allowed border-transparent text-neutral-700"
346
+ : "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
360
347
  }`}
361
- aria-label={
362
- STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
363
- }
348
+ aria-label="Inspector"
364
349
  >
365
350
  <svg
366
351
  width="12"
@@ -10,10 +10,7 @@ import { PanelTabButton } from "./PanelTabButton";
10
10
  import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
11
11
  import type { RenderJob } from "./renders/useRenderQueue";
12
12
  import type { BlockParam } from "@hyperframes/core/registry";
13
- import {
14
- STUDIO_FLAT_INSPECTOR_ENABLED,
15
- STUDIO_INSPECTOR_PANELS_ENABLED,
16
- } from "./editor/manualEditingAvailability";
13
+ import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
17
14
  import type { Composition } from "@hyperframes/sdk";
18
15
  import type { EditHistoryKind } from "../utils/editHistory";
19
16
  import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
@@ -229,8 +226,7 @@ export function StudioRightPanel({
229
226
  setRightPanelTab,
230
227
  });
231
228
  const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
232
- const layersPaneOpen =
233
- inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;
229
+ const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers;
234
230
 
235
231
  const handleInspectorPaneButtonClick = (pane: "design" | "layers") => {
236
232
  if (!inspectorTabActive) {
@@ -483,22 +479,18 @@ export function StudioRightPanel({
483
479
  ) : (
484
480
  <>
485
481
  <div className="flex min-w-0 items-center gap-1 overflow-hidden border-b border-neutral-800 px-3 py-2">
486
- {STUDIO_INSPECTOR_PANELS_ENABLED && (
487
- <>
488
- <PanelTabButton
489
- label="Design"
490
- tooltip="Element styles and properties"
491
- active={designPaneOpen}
492
- onClick={() => handleInspectorPaneButtonClick("design")}
493
- />
494
- <PanelTabButton
495
- label="Layers"
496
- tooltip="Composition layer stack"
497
- active={layersPaneOpen}
498
- onClick={() => handleInspectorPaneButtonClick("layers")}
499
- />
500
- </>
501
- )}
482
+ <PanelTabButton
483
+ label="Design"
484
+ tooltip="Element styles and properties"
485
+ active={designPaneOpen}
486
+ onClick={() => handleInspectorPaneButtonClick("design")}
487
+ />
488
+ <PanelTabButton
489
+ label="Layers"
490
+ tooltip="Composition layer stack"
491
+ active={layersPaneOpen}
492
+ onClick={() => handleInspectorPaneButtonClick("layers")}
493
+ />
502
494
  <PanelTabButton
503
495
  label={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
504
496
  tooltip="Render queue and exports"
@@ -15,10 +15,6 @@ import {
15
15
  } from "../player/components/timelineZoom";
16
16
  import { useTimelineZoom } from "../player/components/useTimelineZoom";
17
17
  import { usePlayerStore, type TimelineElement } from "../player";
18
- import {
19
- STUDIO_KEYFRAMES_ENABLED,
20
- STUDIO_RAZOR_TOOL_ENABLED,
21
- } from "./editor/manualEditingAvailability";
22
18
  import { Tooltip } from "./ui";
23
19
  import { Scissors } from "../icons/SystemIcons";
24
20
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
@@ -135,7 +131,7 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
135
131
  // Wire the "Add keyframe (K)" shortcut the toolbar advertises. Active only when
136
132
  // there's a keyframeable selection; otherwise K stays JKL-pause in playback.
137
133
  useKeyframeKeyboard({
138
- enabled: STUDIO_KEYFRAMES_ENABLED && Boolean(onToggleKeyframe),
134
+ enabled: Boolean(onToggleKeyframe),
139
135
  onAddKeyframe: onToggleKeyframe,
140
136
  });
141
137
 
@@ -170,36 +166,32 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
170
166
  <div className="border-b border-neutral-800/60">
171
167
  <div className="flex items-center justify-between px-2 py-0.5">
172
168
  <div className="flex items-center gap-0.5">
173
- {STUDIO_RAZOR_TOOL_ENABLED && (
174
- <>
175
- <Tooltip label="Selection tool (V)">
176
- <button
177
- type="button"
178
- onClick={() => setActiveTool("select")}
179
- aria-label="Selection tool"
180
- aria-pressed={activeTool === "select"}
181
- className={activeTool === "select" ? flatActive : flatIdle}
182
- >
183
- <svg width="16" height="16" viewBox="0 0 12 12" fill="currentColor">
184
- <path d="M2 0.5L10 6L6.5 6.5L8.5 11L6.5 11.5L4.5 7L2 9Z" />
185
- </svg>
186
- </button>
187
- </Tooltip>
188
- <Tooltip label="Razor tool (B) — Shift+click splits all tracks">
189
- <button
190
- type="button"
191
- onClick={() => setActiveTool("razor")}
192
- aria-label="Razor tool"
193
- aria-pressed={activeTool === "razor"}
194
- className={activeTool === "razor" ? flatActive : flatIdle}
195
- >
196
- <Scissors size={16} />
197
- </button>
198
- </Tooltip>
199
- {/* Divider: tool-mode | editing-actions */}
200
- <div aria-hidden="true" className="mx-1 h-4 w-px bg-neutral-800" />
201
- </>
202
- )}
169
+ <Tooltip label="Selection tool (V)">
170
+ <button
171
+ type="button"
172
+ onClick={() => setActiveTool("select")}
173
+ aria-label="Selection tool"
174
+ aria-pressed={activeTool === "select"}
175
+ className={activeTool === "select" ? flatActive : flatIdle}
176
+ >
177
+ <svg width="16" height="16" viewBox="0 0 12 12" fill="currentColor">
178
+ <path d="M2 0.5L10 6L6.5 6.5L8.5 11L6.5 11.5L4.5 7L2 9Z" />
179
+ </svg>
180
+ </button>
181
+ </Tooltip>
182
+ <Tooltip label="Razor tool (B) — Shift+click splits all tracks">
183
+ <button
184
+ type="button"
185
+ onClick={() => setActiveTool("razor")}
186
+ aria-label="Razor tool"
187
+ aria-pressed={activeTool === "razor"}
188
+ className={activeTool === "razor" ? flatActive : flatIdle}
189
+ >
190
+ <Scissors size={16} />
191
+ </button>
192
+ </Tooltip>
193
+ {/* Divider: tool-mode | editing-actions */}
194
+ <div aria-hidden="true" className="mx-1 h-4 w-px bg-neutral-800" />
203
195
  <Tooltip label={timelineSnapEnabled ? "Snapping on (N)" : "Snapping off (N)"}>
204
196
  <button
205
197
  type="button"
@@ -211,111 +203,107 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
211
203
  <Magnet size={16} weight="bold" aria-hidden="true" />
212
204
  </button>
213
205
  </Tooltip>
214
- {STUDIO_KEYFRAMES_ENABLED && (
215
- // Always rendered (CapCut-style): with no keyframeable selection the
216
- // button fades to a disabled state instead of unmounting, so the
217
- // toolbar layout never shifts.
218
- <Tooltip
219
- label={
220
- keyframePathEndpoint
221
- ? "Motion path endpoints cannot be removed"
222
- : !onToggleKeyframe
223
- ? "Select an animated element to add keyframes"
224
- : keyframeIsMotionPath
225
- ? keyframeWillExtend
226
- ? "Extend motion path to playhead (K)"
227
- : keyframeState === "active"
228
- ? "Remove waypoint from motion path (K)"
229
- : "Add waypoint to motion path (K)"
206
+ {/* Always rendered (CapCut-style): with no keyframeable selection the
207
+ button fades to a disabled state instead of unmounting, so the
208
+ toolbar layout never shifts. */}
209
+ <Tooltip
210
+ label={
211
+ keyframePathEndpoint
212
+ ? "Motion path endpoints cannot be removed"
213
+ : !onToggleKeyframe
214
+ ? "Select an animated element to add keyframes"
215
+ : keyframeIsMotionPath
216
+ ? keyframeWillExtend
217
+ ? "Extend motion path to playhead (K)"
230
218
  : keyframeState === "active"
231
- ? "Remove keyframe at playhead (K)"
219
+ ? "Remove waypoint from motion path (K)"
220
+ : "Add waypoint to motion path (K)"
221
+ : keyframeState === "active"
222
+ ? "Remove keyframe at playhead (K)"
223
+ : keyframeState === "inactive"
224
+ ? keyframeWillExtend
225
+ ? "Add keyframe at playhead, extends animation (K)"
226
+ : "Add keyframe at playhead (K)"
227
+ : "Add keyframe (K)"
228
+ }
229
+ >
230
+ <button
231
+ type="button"
232
+ disabled={!onToggleKeyframe}
233
+ onClick={onToggleKeyframe}
234
+ aria-label={
235
+ keyframePathEndpoint
236
+ ? "Motion path endpoint"
237
+ : keyframeIsMotionPath
238
+ ? keyframeState === "active"
239
+ ? "Remove motion path waypoint"
240
+ : keyframeWillExtend
241
+ ? "Extend motion path to playhead"
242
+ : "Add motion path waypoint"
243
+ : keyframeState === "active"
244
+ ? "Remove keyframe at playhead"
245
+ : "Add keyframe at playhead"
246
+ }
247
+ className={
248
+ !onToggleKeyframe
249
+ ? flatDisabled
250
+ : `${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
251
+ keyframeState === "active"
252
+ ? "text-studio-accent"
232
253
  : keyframeState === "inactive"
233
- ? keyframeWillExtend
234
- ? "Add keyframe at playhead, extends animation (K)"
235
- : "Add keyframe at playhead (K)"
236
- : "Add keyframe (K)"
254
+ ? "text-neutral-400 hover:text-studio-accent"
255
+ : "text-neutral-600 hover:text-neutral-400"
256
+ }`
237
257
  }
238
258
  >
239
- <button
240
- type="button"
241
- disabled={!onToggleKeyframe}
242
- onClick={onToggleKeyframe}
243
- aria-label={
244
- keyframePathEndpoint
245
- ? "Motion path endpoint"
246
- : keyframeIsMotionPath
247
- ? keyframeState === "active"
248
- ? "Remove motion path waypoint"
249
- : keyframeWillExtend
250
- ? "Extend motion path to playhead"
251
- : "Add motion path waypoint"
252
- : keyframeState === "active"
253
- ? "Remove keyframe at playhead"
254
- : "Add keyframe at playhead"
255
- }
256
- className={
257
- !onToggleKeyframe
258
- ? flatDisabled
259
- : `${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
260
- keyframeState === "active"
261
- ? "text-studio-accent"
262
- : keyframeState === "inactive"
263
- ? "text-neutral-400 hover:text-studio-accent"
264
- : "text-neutral-600 hover:text-neutral-400"
265
- }`
266
- }
267
- >
268
- <svg width="16" height="16" viewBox="0 0 10 10" fill="currentColor">
269
- {keyframeState === "active" ? (
270
- <path d="M5 0.5L9.5 5L5 9.5L0.5 5Z" />
271
- ) : (
272
- <path
273
- d="M5 1.2L8.8 5L5 8.8L1.2 5Z"
274
- fill="none"
275
- stroke="currentColor"
276
- strokeWidth="1.2"
277
- />
278
- )}
279
- </svg>
280
- </button>
281
- </Tooltip>
282
- )}
283
- {STUDIO_KEYFRAMES_ENABLED && (
284
- <Tooltip
285
- label={
259
+ <svg width="16" height="16" viewBox="0 0 10 10" fill="currentColor">
260
+ {keyframeState === "active" ? (
261
+ <path d="M5 0.5L9.5 5L5 9.5L0.5 5Z" />
262
+ ) : (
263
+ <path
264
+ d="M5 1.2L8.8 5L5 8.8L1.2 5Z"
265
+ fill="none"
266
+ stroke="currentColor"
267
+ strokeWidth="1.2"
268
+ />
269
+ )}
270
+ </svg>
271
+ </button>
272
+ </Tooltip>
273
+ <Tooltip
274
+ label={
275
+ autoKeyframeEnabled
276
+ ? "Auto-record manual edits as keyframes (click to turn off)"
277
+ : "Manual edits will not be recorded as keyframes (click to turn on)"
278
+ }
279
+ >
280
+ <button
281
+ type="button"
282
+ onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
283
+ aria-label="Auto-record manual edits as keyframes"
284
+ aria-pressed={autoKeyframeEnabled}
285
+ className={`${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
286
286
  autoKeyframeEnabled
287
- ? "Auto-record manual edits as keyframes (click to turn off)"
288
- : "Manual edits will not be recorded as keyframes (click to turn on)"
289
- }
287
+ ? "text-red-400 hover:text-red-300"
288
+ : "text-neutral-600 hover:text-neutral-400"
289
+ }`}
290
290
  >
291
- <button
292
- type="button"
293
- onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
294
- aria-label="Auto-record manual edits as keyframes"
295
- aria-pressed={autoKeyframeEnabled}
296
- className={`${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
297
- autoKeyframeEnabled
298
- ? "text-red-400 hover:text-red-300"
299
- : "text-neutral-600 hover:text-neutral-400"
300
- }`}
301
- >
302
- <svg width="16" height="16" viewBox="0 0 10 10" fill="none">
303
- {/* Same diamond outline as the Add-keyframe icon, with a
291
+ <svg width="16" height="16" viewBox="0 0 10 10" fill="none">
292
+ {/* Same diamond outline as the Add-keyframe icon, with a
304
293
  record-style dot inside: filled = auto-recording,
305
294
  hollow = manual edits won't be keyframed. */}
306
- <path d="M5 0.7L9.3 5L5 9.3L0.7 5Z" stroke="currentColor" strokeWidth="1" />
307
- <circle
308
- cx="5"
309
- cy="5"
310
- r="1.8"
311
- fill={autoKeyframeEnabled ? "currentColor" : "none"}
312
- stroke="currentColor"
313
- strokeWidth="1"
314
- />
315
- </svg>
316
- </button>
317
- </Tooltip>
318
- )}
295
+ <path d="M5 0.7L9.3 5L5 9.3L0.7 5Z" stroke="currentColor" strokeWidth="1" />
296
+ <circle
297
+ cx="5"
298
+ cy="5"
299
+ r="1.8"
300
+ fill={autoKeyframeEnabled ? "currentColor" : "none"}
301
+ stroke="currentColor"
302
+ strokeWidth="1"
303
+ />
304
+ </svg>
305
+ </button>
306
+ </Tooltip>
319
307
  {onSplitElement &&
320
308
  (() => {
321
309
  // Render the button unconditionally (disabled when unusable):
@@ -5,7 +5,15 @@ import {
5
5
  parseWiggleEase,
6
6
  type WiggleEaseConfig,
7
7
  } from "@hyperframes/core/wiggle-ease";
8
- import { EASE_PRESETS, easePresetLabel } from "./easePresetLibrary";
8
+ import { easePresetLabel } from "./easePresetLibrary";
9
+ import {
10
+ DEFAULT_CURVE,
11
+ MODE_LABELS,
12
+ EaseModeToggle,
13
+ EasePresetGrid,
14
+ type EaseMode,
15
+ type Pts,
16
+ } from "./EaseModeControls";
9
17
  import { holdCurvePath, MiniCurveSvg, sampledPath } from "./easeCurveSvg";
10
18
  import { EaseBezierField, SpringBounceField, WiggleField } from "./EaseParamFields";
11
19
  import { EASE_CURVES, EASE_LABELS, resolveEaseCurveTuple } from "./gsapAnimationConstants";
@@ -14,51 +22,6 @@ import type { AnimationKeyframeTarget } from "../../hooks/gsapTweenSynth";
14
22
 
15
23
  export { MiniCurveSvg } from "./easeCurveSvg";
16
24
 
17
- const EASE_MODES = ["curve", "spring", "wiggle"] as const;
18
- type EaseMode = (typeof EASE_MODES)[number];
19
-
20
- const EasePresetGrid = function EasePresetGrid({
21
- kind,
22
- currentEase,
23
- onSelect,
24
- }: {
25
- kind: EaseMode;
26
- currentEase: string;
27
- onSelect: (ease: string) => void;
28
- }) {
29
- return (
30
- <div className="mb-2 grid max-h-56 grid-cols-4 gap-1 overflow-y-auto pr-0.5">
31
- {EASE_PRESETS.filter((preset) => preset.kind === kind).map((preset) => {
32
- const isActive = currentEase === preset.ease;
33
- return (
34
- <button
35
- key={preset.id}
36
- type="button"
37
- data-ease-preset-id={preset.id}
38
- role="menuitemradio"
39
- aria-checked={isActive}
40
- tabIndex={isActive ? 0 : -1}
41
- onClick={() => onSelect(preset.ease)}
42
- className={`flex flex-col items-center gap-0.5 rounded-md p-1 transition-colors ${
43
- isActive ? "bg-panel-accent/10 ring-1 ring-panel-accent/30" : "hover:bg-neutral-800"
44
- }`}
45
- title={preset.label}
46
- >
47
- <MiniCurveSvg ease={preset.ease} active={isActive} />
48
- <span
49
- className={`text-center text-[8px] leading-none ${
50
- isActive ? "text-panel-accent" : "text-neutral-500"
51
- }`}
52
- >
53
- {preset.label}
54
- </span>
55
- </button>
56
- );
57
- })}
58
- </div>
59
- );
60
- };
61
-
62
25
  const round2 = roundToCenti;
63
26
 
64
27
  // ── Graph geometry (Figma-style easing box) ─────────────────────────────────
@@ -81,50 +44,6 @@ const DRAG_VMAX = 2;
81
44
  const DRAG_VMIN = -1;
82
45
  const ACCENT = "#3CE6AC";
83
46
 
84
- type Pts = [number, number, number, number];
85
- const DEFAULT_CURVE: Pts = EASE_CURVES["power2.out"];
86
- const MODE_LABELS = { curve: "Curve", spring: "Spring", wiggle: "Wiggle" } satisfies Record<
87
- EaseMode,
88
- string
89
- >;
90
- const DEFAULT_EASE_BY_MODE = {
91
- curve: `custom(M0,0 C${DEFAULT_CURVE[0]},${DEFAULT_CURVE[1]} ${DEFAULT_CURVE[2]},${DEFAULT_CURVE[3]} 1,1)`,
92
- spring: "spring(0.42)",
93
- wiggle: "wiggle(3,easeInOut,0.12)",
94
- } satisfies Record<EaseMode, string>;
95
-
96
- function EaseModeToggle({ mode, onCommit }: { mode: EaseMode; onCommit: (ease: string) => void }) {
97
- return (
98
- <div
99
- className="mb-2 grid grid-cols-3 rounded-md bg-black/20 p-0.5"
100
- role="radiogroup"
101
- aria-label="Ease editor mode"
102
- >
103
- {EASE_MODES.map((candidateMode) => {
104
- const active = candidateMode === mode;
105
- return (
106
- <button
107
- key={candidateMode}
108
- type="button"
109
- data-ease-mode={candidateMode}
110
- role="radio"
111
- aria-checked={active}
112
- onClick={() => {
113
- if (active) return;
114
- onCommit(DEFAULT_EASE_BY_MODE[candidateMode]);
115
- }}
116
- className={`rounded px-2 py-1 text-[10px] font-medium transition-colors ${
117
- active ? "bg-neutral-700 text-neutral-100" : "text-neutral-500 hover:text-neutral-300"
118
- }`}
119
- >
120
- {MODE_LABELS[candidateMode]}
121
- </button>
122
- );
123
- })}
124
- </div>
125
- );
126
- }
127
-
128
47
  // Figma-style ease-type dropdown: the current ease (glyph + name) as a button
129
48
  // that opens the preset grid in a popover. This is where a preset is selected —
130
49
  // the grid is no longer shown inline.