@hyperframes/studio 0.7.60 → 0.7.61

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 (47) hide show
  1. package/dist/assets/hyperframes-player-Xvx2hkrc.js +459 -0
  2. package/dist/assets/{index-cH6NfVV_.js → index-2mxh_HSy.js} +177 -177
  3. package/dist/assets/{index-D6etaey-.js → index-BCpoiv9S.js} +1 -1
  4. package/dist/assets/index-BhWig0mx.css +1 -0
  5. package/dist/assets/{index-Dh_WhagG.js → index-D-uFclFj.js} +1 -1
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +1202 -673
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +3 -8
  11. package/src/components/DesignPanelPromoteProvider.tsx +27 -1
  12. package/src/components/StudioHeader.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +34 -26
  14. package/src/components/editor/PromotableControl.tsx +4 -2
  15. package/src/components/editor/PropertyPanel.tsx +151 -149
  16. package/src/components/editor/PropertyPanelFlat.tsx +44 -42
  17. package/src/components/editor/manualEditingAvailability.test.ts +6 -6
  18. package/src/components/editor/manualEditingAvailability.ts +5 -3
  19. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +10 -0
  20. package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -0
  21. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
  22. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +21 -9
  23. package/src/components/editor/propertyPanelFlatStyleSections.tsx +15 -32
  24. package/src/components/editor/propertyPanelFlatTextSection.tsx +2 -2
  25. package/src/components/editor/propertyPanelInputCoverage.test.tsx +14 -0
  26. package/src/components/editor/propertyPanelPrimitives.tsx +9 -1
  27. package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
  28. package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
  29. package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
  30. package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
  31. package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
  32. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
  33. package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
  34. package/src/components/storyboard/storyboardReviewStage.ts +45 -0
  35. package/src/components/storyboard/useFrameComments.ts +22 -6
  36. package/src/contexts/ViewModeContext.tsx +60 -7
  37. package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
  38. package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
  39. package/src/hooks/useSlideshowTabState.test.ts +96 -0
  40. package/src/hooks/useSlideshowTabState.ts +61 -0
  41. package/src/player/lib/playbackTypes.ts +7 -0
  42. package/src/player/lib/runtimeProtocol.test.ts +6 -1
  43. package/src/player/lib/timelineDOM.ts +8 -11
  44. package/src/player/lib/timelineIframeHelpers.ts +153 -107
  45. package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
  46. package/dist/assets/index-DXbu6IPT.css +0 -1
  47. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.7.60",
3
+ "version": "0.7.61",
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.60",
50
- "@hyperframes/player": "0.7.60",
51
- "@hyperframes/sdk": "0.7.60",
52
- "@hyperframes/parsers": "0.7.60",
53
- "@hyperframes/studio-server": "0.7.60"
49
+ "@hyperframes/core": "0.7.61",
50
+ "@hyperframes/parsers": "0.7.61",
51
+ "@hyperframes/player": "0.7.61",
52
+ "@hyperframes/sdk": "0.7.61",
53
+ "@hyperframes/studio-server": "0.7.61"
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.60"
68
+ "@hyperframes/producer": "0.7.61"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19",
package/src/App.tsx CHANGED
@@ -23,6 +23,7 @@ import { useDomEditSession } from "./hooks/useDomEditSession";
23
23
  import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync";
24
24
  import { useStudioSdkSessions } from "./hooks/useStudioSdkSessions";
25
25
  import { useBlockHandlers } from "./hooks/useBlockHandlers";
26
+ import { useAddAssetAtPlayhead } from "./hooks/useAddAssetAtPlayhead";
26
27
  import { useAppHotkeys } from "./hooks/useAppHotkeys";
27
28
  import { useClipboard } from "./hooks/useClipboard";
28
29
  import { deleteSelectedKeyframes } from "./hooks/timelineEditingHelpers";
@@ -195,14 +196,7 @@ export function StudioApp() {
195
196
  },
196
197
  [timelineEditing.handleTimelineGroupMove],
197
198
  );
198
- const handleAddAssetAtPlayhead = useCallback(
199
- (assetPath: string) =>
200
- timelineEditing.handleTimelineAssetDrop(assetPath, {
201
- start: usePlayerStore.getState().currentTime,
202
- track: 0,
203
- }),
204
- [timelineEditing],
205
- );
199
+ const handleAddAssetAtPlayhead = useAddAssetAtPlayhead(timelineEditing.handleTimelineAssetDrop);
206
200
  const {
207
201
  activeBlockParams,
208
202
  setActiveBlockParams,
@@ -533,6 +527,7 @@ export function StudioApp() {
533
527
  onToggleRecording={recordingToggle}
534
528
  sdkSession={sdkHandle.session}
535
529
  publishSdkSession={sdkHandle.publish}
530
+ forceReloadSdkSession={sdkHandle.forceReload}
536
531
  reloadPreview={reloadPreview}
537
532
  domEditSaveTimestampRef={domEditSaveTimestampRef}
538
533
  recordEdit={editHistory.recordEdit}
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { useCallback, type ReactNode } from "react";
13
+ import type { Composition } from "@hyperframes/sdk";
13
14
  import type { DomEditSelection } from "./editor/domEditingTypes";
14
15
  import { useSdkSession } from "../hooks/useSdkSession";
15
16
  import { useVariablesPersist, type UseVariablesPersistParams } from "../hooks/useVariablesPersist";
@@ -24,6 +25,7 @@ export function DesignPanelPromoteProvider({
24
25
  projectId,
25
26
  activeCompPath,
26
27
  showToast,
28
+ forceReloadSharedSdkSession,
27
29
  children,
28
30
  ...persistDeps
29
31
  }: PersistDeps & {
@@ -31,16 +33,40 @@ export function DesignPanelPromoteProvider({
31
33
  projectId: string | null;
32
34
  activeCompPath: string | null;
33
35
  showToast: (message: string, tone?: "error" | "info") => void;
36
+ /**
37
+ * Forces the app's SHARED SDK session (Variables tab, Slideshow, etc.) to
38
+ * re-open from disk. This provider opens its OWN session, separate from
39
+ * that shared one, so a persist through it never fires the shared session's
40
+ * own "change" event. When the target happens to be the same file the
41
+ * shared session already has open, that session is left holding stale
42
+ * in-memory content after a successful persist — worse, the self-write-echo
43
+ * registry that would normally reload it on the next file-change
44
+ * notification is keyed by file path only (not by session instance), so it
45
+ * mistakes this provider's write for its own echo and stays stale
46
+ * indefinitely. Called unconditionally (not gated on targetPath matching
47
+ * activeCompPath): re-opening a file that didn't change is a harmless
48
+ * no-op re-parse, cheaper than the bug class a subtly-wrong path guard
49
+ * could reintroduce.
50
+ */
51
+ forceReloadSharedSdkSession?: () => void;
34
52
  children: ReactNode;
35
53
  }) {
36
54
  const targetPath = selection?.sourceFile || activeCompPath || "index.html";
37
55
  const handle = useSdkSession(projectId, targetPath, persistDeps.domEditSaveTimestampRef);
38
- const persist = useVariablesPersist({
56
+ const rawPersist = useVariablesPersist({
39
57
  ...persistDeps,
40
58
  sdkSession: handle.session,
41
59
  publishSdkSession: handle.publish,
42
60
  activeCompPath: targetPath,
43
61
  });
62
+ const persist = useCallback(
63
+ async (label: string, mutate: (session: Composition) => void) => {
64
+ const committed = await rawPersist(label, mutate);
65
+ if (committed) forceReloadSharedSdkSession?.();
66
+ return committed;
67
+ },
68
+ [rawPersist, forceReloadSharedSdkSession],
69
+ );
44
70
  const handlePersistError = useCallback(
45
71
  (error: unknown) => showToast(getStudioSaveErrorMessage(error), "error"),
46
72
  [showToast],
@@ -149,14 +149,13 @@ const VIEW_MODE_OPTIONS: Array<{ mode: StudioViewMode; label: string }> = [
149
149
  ];
150
150
 
151
151
  /** Segmented control switching the main stage between storyboard and preview. */
152
- function ViewModeToggle() {
152
+ export function ViewModeToggle() {
153
153
  const { viewMode, setViewMode } = useViewMode();
154
154
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
155
155
 
156
156
  const selectMode = (mode: StudioViewMode) => {
157
157
  if (mode === viewMode) return;
158
- trackStudioEvent("view_mode_toggle", { mode });
159
- setViewMode(mode);
158
+ if (setViewMode(mode)) trackStudioEvent("view_mode_toggle", { mode });
160
159
  };
161
160
 
162
161
  // Complete APG tabs pattern: roving tabIndex + arrow-key navigation.
@@ -1,17 +1,15 @@
1
- import { useCallback, useEffect, useMemo, useRef, type MutableRefObject } from "react";
1
+ import { useCallback, useEffect, useRef, type MutableRefObject } from "react";
2
2
  import { PropertyPanel } from "./editor/PropertyPanel";
3
3
  import { LayersPanel } from "./editor/LayersPanel";
4
4
  import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
5
5
  import { BlockParamsPanel } from "./editor/BlockParamsPanel";
6
6
  import { RenderQueue } from "./renders/RenderQueue";
7
7
  import { SlideshowPanel } from "./panels/SlideshowPanel";
8
- import type { SceneInfo } from "./panels/SlideshowPanel";
9
8
  import { VariablesPanel } from "./panels/VariablesPanel";
10
9
  import { PanelTabButton } from "./PanelTabButton";
11
10
  import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
12
11
  import type { RenderJob } from "./renders/useRenderQueue";
13
12
  import type { BlockParam } from "@hyperframes/core/registry";
14
- import type { IframeWindow } from "../player/lib/playbackTypes";
15
13
  import {
16
14
  STUDIO_FLAT_INSPECTOR_ENABLED,
17
15
  STUDIO_INSPECTOR_PANELS_ENABLED,
@@ -19,6 +17,7 @@ import {
19
17
  import type { Composition } from "@hyperframes/sdk";
20
18
  import type { EditHistoryKind } from "../utils/editHistory";
21
19
  import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
20
+ import { useSlideshowTabState } from "../hooks/useSlideshowTabState";
22
21
  import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
23
22
 
24
23
  import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
@@ -51,6 +50,19 @@ export interface StudioRightPanelProps {
51
50
  /** Dependencies for the Slideshow persist callback, threaded from App.tsx. */
52
51
  sdkSession: Composition | null;
53
52
  publishSdkSession: NonNullable<UseSlideshowPersistParams["publishSdkSession"]>;
53
+ /**
54
+ * Forces THIS `sdkSession` to re-open from disk. DesignPanelPromoteProvider
55
+ * opens its own separate SDK session scoped to the selected element's own
56
+ * file (needed so promoting inside a sub-composition binds a variable there,
57
+ * not on the host) — for a top-level selection that's the SAME file this
58
+ * session already has open, so a write through that other session leaves
59
+ * this one holding stale in-memory content. The self-write-echo registry
60
+ * that normally suppresses redundant reloads is keyed by file path only, not
61
+ * by session instance, so it wrongly treats the sibling session's write as
62
+ * "our own echo" and never reloads on its own — this must be called
63
+ * explicitly after such a write.
64
+ */
65
+ forceReloadSdkSession?: () => void;
54
66
  reloadPreview: () => void;
55
67
  domEditSaveTimestampRef: MutableRefObject<number>;
56
68
  recordEdit: (entry: {
@@ -71,6 +83,7 @@ export function StudioRightPanel({
71
83
  onToggleRecording,
72
84
  sdkSession,
73
85
  publishSdkSession,
86
+ forceReloadSdkSession,
74
87
  reloadPreview,
75
88
  domEditSaveTimestampRef,
76
89
  recordEdit,
@@ -154,6 +167,7 @@ export function StudioRightPanel({
154
167
  readProjectFile,
155
168
  writeProjectFile,
156
169
  fileTree,
170
+ editingFile,
157
171
  } = useFileManagerContext();
158
172
 
159
173
  // Discrete ops (toggle, reorder, add/delete, hotspot): persist immediately,
@@ -202,22 +216,13 @@ export function StudioRightPanel({
202
216
  const renderJobs = renderQueue.jobs as RenderJob[];
203
217
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
204
218
 
205
- // Derive scene list from the live clip manifest in the preview iframe.
206
- // fallow-ignore-next-line complexity
207
- const slideshowScenes = useMemo<SceneInfo[]>(() => {
208
- try {
209
- const win = previewIframeRef.current?.contentWindow as IframeWindow | null;
210
- return (win?.__clipManifest?.scenes ?? []).map((s) => ({
211
- id: s.id,
212
- label: s.label,
213
- start: s.start,
214
- duration: s.duration,
215
- }));
216
- } catch {
217
- return [];
218
- }
219
- // eslint-disable-next-line react-hooks/exhaustive-deps
220
- }, [previewIframeRef, rightPanelTab, refreshKey]);
219
+ const { isSlideshowComposition, slideshowScenes } = useSlideshowTabState({
220
+ editingFileContent: editingFile?.content,
221
+ previewIframeRef,
222
+ refreshKey,
223
+ rightPanelTab,
224
+ setRightPanelTab,
225
+ });
221
226
  const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
222
227
  const layersPaneOpen =
223
228
  inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;
@@ -336,6 +341,7 @@ export function StudioRightPanel({
336
341
  recordEdit={recordEdit}
337
342
  reloadPreview={reloadPreview}
338
343
  domEditSaveTimestampRef={domEditSaveTimestampRef}
344
+ forceReloadSharedSdkSession={forceReloadSdkSession}
339
345
  >
340
346
  <PropertyPanel
341
347
  projectId={projectId}
@@ -491,12 +497,14 @@ export function StudioRightPanel({
491
497
  active={rightPanelTab === "renders"}
492
498
  onClick={() => setRightPanelTab("renders")}
493
499
  />
494
- <PanelTabButton
495
- label="Slideshow"
496
- tooltip="Slideshow branching editor"
497
- active={rightPanelTab === "slideshow"}
498
- onClick={() => setRightPanelTab("slideshow")}
499
- />
500
+ {isSlideshowComposition && (
501
+ <PanelTabButton
502
+ label="Slideshow"
503
+ tooltip="Slideshow branching editor"
504
+ active={rightPanelTab === "slideshow"}
505
+ onClick={() => setRightPanelTab("slideshow")}
506
+ />
507
+ )}
500
508
  <PanelTabButton
501
509
  label="Variables"
502
510
  tooltip="Template variables — declare, preview with values"
@@ -513,7 +521,7 @@ export function StudioRightPanel({
513
521
  compositionPath={activeBlockParams.compositionPath}
514
522
  onClose={onCloseBlockParams ?? (() => {})}
515
523
  />
516
- ) : rightPanelTab === "slideshow" ? (
524
+ ) : rightPanelTab === "slideshow" && isSlideshowComposition ? (
517
525
  <SlideshowPanel
518
526
  scenes={slideshowScenes}
519
527
  onPersist={onPersistSlideshow}
@@ -81,7 +81,9 @@ export function PromotableControl({
81
81
  {rendered}
82
82
  {bound && (
83
83
  <span
84
- className="pointer-events-none absolute right-1.5 top-0 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
84
+ // Sits above the row (not on top of top-0) so it clears a value that
85
+ // renders flush to the row's right edge, e.g. flat Font/Color rows.
86
+ className="pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
85
87
  title={`Bound to variable "${promote.boundId}"`}
86
88
  >
87
89
  ◆ {promote.boundId}
@@ -95,7 +97,7 @@ export function PromotableControl({
95
97
  e.stopPropagation();
96
98
  promote.promote();
97
99
  }}
98
- className="absolute right-1.5 top-0 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
100
+ className="absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
99
101
  >
100
102
  ◇ var
101
103
  </button>
@@ -197,11 +197,11 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
197
197
  const manualSizeEditingDisabled = !element.capabilities.canApplyManualSize;
198
198
  const manualRotationEditingDisabled = !element.capabilities.canApplyManualRotation;
199
199
  const sourceLabel = element.id ? `#${element.id}` : (element.selector ?? "");
200
- const showEditableSections = element.capabilities.canEditStyles;
201
200
  // Capabilities are already resolved on the selection; recompute only sections,
202
201
  // feeding the live GSAP tween count (arrives on the gsapAnimations prop, not the
203
202
  // selection) so the Timing section shows for pure-GSAP elements with no data-start.
204
203
  const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
204
+ const showEditableSections = element.capabilities.canEditStyles && sections.style;
205
205
  const manualOffset = readStudioPathOffset(element.element);
206
206
  const manualSize = readStudioBoxSize(element.element);
207
207
  const resolvedWidth =
@@ -385,161 +385,163 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
385
385
  />
386
386
  )}
387
387
 
388
- <Section title="Layout" icon={<Move size={15} />}>
389
- <div className={RESPONSIVE_GRID}>
390
- <div className="flex items-center gap-1">
391
- <div className="flex-1">
392
- <MetricField
393
- label="X"
394
- value={formatPxMetricValue(displayX)}
395
- disabled={manualOffsetEditingDisabled}
396
- scrub
397
- onCommit={(next) => commitManualOffset("x", next)}
398
- />
388
+ {sections.layout && (
389
+ <Section title="Layout" icon={<Move size={15} />}>
390
+ <div className={RESPONSIVE_GRID}>
391
+ <div className="flex items-center gap-1">
392
+ <div className="flex-1">
393
+ <MetricField
394
+ label="X"
395
+ value={formatPxMetricValue(displayX)}
396
+ disabled={manualOffsetEditingDisabled}
397
+ scrub
398
+ onCommit={(next) => commitManualOffset("x", next)}
399
+ />
400
+ </div>
401
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
402
+ <KeyframeNavigation
403
+ property="x"
404
+ keyframes={navKeyframes}
405
+ currentPercentage={currentPct}
406
+ onSeek={seekFromKfPct}
407
+ onAddKeyframe={() =>
408
+ onCommitAnimatedProperty &&
409
+ void onCommitAnimatedProperty(element, "x", displayX)
410
+ }
411
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("x"), pct)}
412
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("x"))}
413
+ />
414
+ )}
399
415
  </div>
400
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
401
- <KeyframeNavigation
402
- property="x"
403
- keyframes={navKeyframes}
404
- currentPercentage={currentPct}
405
- onSeek={seekFromKfPct}
406
- onAddKeyframe={() =>
407
- onCommitAnimatedProperty &&
408
- void onCommitAnimatedProperty(element, "x", displayX)
409
- }
410
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("x"), pct)}
411
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("x"))}
412
- />
413
- )}
414
- </div>
415
- <div className="flex items-center gap-1">
416
- <div className="flex-1">
417
- <MetricField
418
- label="Y"
419
- value={formatPxMetricValue(displayY)}
420
- disabled={manualOffsetEditingDisabled}
421
- scrub
422
- onCommit={(next) => commitManualOffset("y", next)}
423
- />
416
+ <div className="flex items-center gap-1">
417
+ <div className="flex-1">
418
+ <MetricField
419
+ label="Y"
420
+ value={formatPxMetricValue(displayY)}
421
+ disabled={manualOffsetEditingDisabled}
422
+ scrub
423
+ onCommit={(next) => commitManualOffset("y", next)}
424
+ />
425
+ </div>
426
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
427
+ <KeyframeNavigation
428
+ property="y"
429
+ keyframes={navKeyframes}
430
+ currentPercentage={currentPct}
431
+ onSeek={seekFromKfPct}
432
+ onAddKeyframe={() =>
433
+ onCommitAnimatedProperty &&
434
+ void onCommitAnimatedProperty(element, "y", displayY)
435
+ }
436
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("y"), pct)}
437
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("y"))}
438
+ />
439
+ )}
424
440
  </div>
425
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
426
- <KeyframeNavigation
427
- property="y"
428
- keyframes={navKeyframes}
429
- currentPercentage={currentPct}
430
- onSeek={seekFromKfPct}
431
- onAddKeyframe={() =>
432
- onCommitAnimatedProperty &&
433
- void onCommitAnimatedProperty(element, "y", displayY)
434
- }
435
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("y"), pct)}
436
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("y"))}
437
- />
438
- )}
439
- </div>
440
- <div className="flex items-center gap-1">
441
- <div className="flex-1">
442
- <MetricField
443
- label="W"
444
- value={formatPxMetricValue(displayW)}
445
- disabled={manualSizeEditingDisabled}
446
- scrub
447
- onCommit={(next) => commitManualSize("width", next)}
448
- />
441
+ <div className="flex items-center gap-1">
442
+ <div className="flex-1">
443
+ <MetricField
444
+ label="W"
445
+ value={formatPxMetricValue(displayW)}
446
+ disabled={manualSizeEditingDisabled}
447
+ scrub
448
+ onCommit={(next) => commitManualSize("width", next)}
449
+ />
450
+ </div>
451
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
452
+ <KeyframeNavigation
453
+ property="width"
454
+ keyframes={navKeyframes}
455
+ currentPercentage={currentPct}
456
+ onSeek={seekFromKfPct}
457
+ onAddKeyframe={() =>
458
+ onCommitAnimatedProperty &&
459
+ void onCommitAnimatedProperty(element, "width", displayW)
460
+ }
461
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("width"), pct)}
462
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("width"))}
463
+ />
464
+ )}
449
465
  </div>
450
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
451
- <KeyframeNavigation
452
- property="width"
453
- keyframes={navKeyframes}
454
- currentPercentage={currentPct}
455
- onSeek={seekFromKfPct}
456
- onAddKeyframe={() =>
457
- onCommitAnimatedProperty &&
458
- void onCommitAnimatedProperty(element, "width", displayW)
459
- }
460
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("width"), pct)}
461
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("width"))}
462
- />
463
- )}
464
- </div>
465
- <div className="flex items-center gap-1">
466
- <div className="flex-1">
467
- <MetricField
468
- label="H"
469
- value={formatPxMetricValue(displayH)}
470
- disabled={manualSizeEditingDisabled}
471
- scrub
472
- onCommit={(next) => commitManualSize("height", next)}
473
- />
466
+ <div className="flex items-center gap-1">
467
+ <div className="flex-1">
468
+ <MetricField
469
+ label="H"
470
+ value={formatPxMetricValue(displayH)}
471
+ disabled={manualSizeEditingDisabled}
472
+ scrub
473
+ onCommit={(next) => commitManualSize("height", next)}
474
+ />
475
+ </div>
476
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
477
+ <KeyframeNavigation
478
+ property="height"
479
+ keyframes={navKeyframes}
480
+ currentPercentage={currentPct}
481
+ onSeek={seekFromKfPct}
482
+ onAddKeyframe={() =>
483
+ onCommitAnimatedProperty &&
484
+ void onCommitAnimatedProperty(element, "height", displayH)
485
+ }
486
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("height"), pct)}
487
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("height"))}
488
+ />
489
+ )}
474
490
  </div>
475
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
476
- <KeyframeNavigation
477
- property="height"
478
- keyframes={navKeyframes}
479
- currentPercentage={currentPct}
480
- onSeek={seekFromKfPct}
481
- onAddKeyframe={() =>
482
- onCommitAnimatedProperty &&
483
- void onCommitAnimatedProperty(element, "height", displayH)
484
- }
485
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("height"), pct)}
486
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("height"))}
487
- />
488
- )}
489
- </div>
490
- <div className="flex items-center gap-1">
491
- <div className="flex-1">
492
- <MetricField
493
- label="R"
494
- value={`${displayR}°`}
495
- disabled={manualRotationEditingDisabled}
496
- onCommit={(next) => commitManualRotation(next.replace("°", ""))}
497
- />
491
+ <div className="flex items-center gap-1">
492
+ <div className="flex-1">
493
+ <MetricField
494
+ label="R"
495
+ value={`${displayR}°`}
496
+ disabled={manualRotationEditingDisabled}
497
+ onCommit={(next) => commitManualRotation(next.replace("°", ""))}
498
+ />
499
+ </div>
500
+ {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
501
+ <KeyframeNavigation
502
+ property="rotation"
503
+ keyframes={navKeyframes}
504
+ currentPercentage={currentPct}
505
+ onSeek={seekFromKfPct}
506
+ onAddKeyframe={() =>
507
+ onCommitAnimatedProperty &&
508
+ void onCommitAnimatedProperty(element, "rotation", displayR)
509
+ }
510
+ onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("rotation"), pct)}
511
+ onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("rotation"))}
512
+ />
513
+ )}
498
514
  </div>
499
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
500
- <KeyframeNavigation
501
- property="rotation"
502
- keyframes={navKeyframes}
503
- currentPercentage={currentPct}
504
- onSeek={seekFromKfPct}
505
- onAddKeyframe={() =>
506
- onCommitAnimatedProperty &&
507
- void onCommitAnimatedProperty(element, "rotation", displayR)
508
- }
509
- onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("rotation"), pct)}
510
- onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("rotation"))}
511
- />
512
- )}
513
515
  </div>
514
- </div>
515
- <PropertyPanel3dTransform
516
- gsapRuntimeValues={gsap3dValues}
517
- gsapAnimId={gsapAnimId}
518
- resolveAnimIdForProp={animIdForProp}
519
- gsapKeyframes={navKeyframes}
520
- currentPct={currentPct}
521
- elStart={elStart}
522
- elDuration={elDuration}
523
- element={element}
524
- onCommitAnimatedProperty={onCommitAnimatedProperty}
525
- onCommitAnimatedProperties={onCommitAnimatedProperties}
526
- onSeekToTime={onSeekToTime}
527
- onRemoveKeyframe={onRemoveKeyframe}
528
- onConvertToKeyframes={onConvertToKeyframes}
529
- onLivePreviewProps={createGsapLivePreview(iframeRef)}
530
- />
531
- <div className="mt-3">
532
- <div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
533
- Stacking
534
- </div>
535
- <MetricField
536
- label="Z-index"
537
- value={String(parseInt(styles["z-index"] || "auto", 10) || 0)}
538
- scrub
539
- onCommit={(next) => onSetStyle("z-index", next)}
516
+ <PropertyPanel3dTransform
517
+ gsapRuntimeValues={gsap3dValues}
518
+ gsapAnimId={gsapAnimId}
519
+ resolveAnimIdForProp={animIdForProp}
520
+ gsapKeyframes={navKeyframes}
521
+ currentPct={currentPct}
522
+ elStart={elStart}
523
+ elDuration={elDuration}
524
+ element={element}
525
+ onCommitAnimatedProperty={onCommitAnimatedProperty}
526
+ onCommitAnimatedProperties={onCommitAnimatedProperties}
527
+ onSeekToTime={onSeekToTime}
528
+ onRemoveKeyframe={onRemoveKeyframe}
529
+ onConvertToKeyframes={onConvertToKeyframes}
530
+ onLivePreviewProps={createGsapLivePreview(iframeRef)}
540
531
  />
541
- </div>
542
- </Section>
532
+ <div className="mt-3">
533
+ <div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
534
+ Stacking
535
+ </div>
536
+ <MetricField
537
+ label="Z-index"
538
+ value={String(parseInt(styles["z-index"] || "auto", 10) || 0)}
539
+ scrub
540
+ onCommit={(next) => onSetStyle("z-index", next)}
541
+ />
542
+ </div>
543
+ </Section>
544
+ )}
543
545
 
544
546
  {STUDIO_GSAP_PANEL_ENABLED &&
545
547
  onUpdateGsapProperty &&