@hyperframes/studio 0.7.82 → 0.7.84

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-s9w3Y9Nj.js} +1 -1
  2. package/dist/assets/{index-fH499K3b.js → index-BWOPM8bq.js} +1 -1
  3. package/dist/assets/{index-DlPIfJDz.js → index-C3J3fmFc.js} +150 -150
  4. package/dist/assets/{index-C0c6LmEC.js → index-COqACN_S.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
@@ -12,7 +12,6 @@ import { AssetsTab } from "./AssetsTab";
12
12
  import { trackStudioEvent } from "../../utils/studioTelemetry";
13
13
  import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab";
14
14
  import { FileTree } from "../editor/FileTree";
15
- import { STUDIO_BLOCKS_PANEL_ENABLED } from "../editor/manualEditingAvailability";
16
15
  import { Tooltip } from "../ui";
17
16
 
18
17
  export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
@@ -127,11 +126,7 @@ export const LeftSidebar = memo(
127
126
  <div className="flex items-center gap-2">
128
127
  <div
129
128
  className="grid min-w-0 flex-1 gap-0.5 rounded-[18px] bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
130
- style={{
131
- gridTemplateColumns: STUDIO_BLOCKS_PANEL_ENABLED
132
- ? "1fr 1fr 1fr 1fr"
133
- : "1fr 1fr 1fr",
134
- }}
129
+ style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}
135
130
  >
136
131
  <Tooltip label="Source code editor" side="bottom">
137
132
  <button
@@ -172,21 +167,19 @@ export const LeftSidebar = memo(
172
167
  Assets
173
168
  </button>
174
169
  </Tooltip>
175
- {STUDIO_BLOCKS_PANEL_ENABLED && (
176
- <Tooltip label="Browse blocks and components" side="bottom">
177
- <button
178
- type="button"
179
- onClick={() => selectTab("blocks")}
180
- className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
181
- tab === "blocks"
182
- ? "bg-neutral-800 text-white"
183
- : "text-neutral-500 hover:text-neutral-200"
184
- }`}
185
- >
186
- Catalog
187
- </button>
188
- </Tooltip>
189
- )}
170
+ <Tooltip label="Browse blocks and components" side="bottom">
171
+ <button
172
+ type="button"
173
+ onClick={() => selectTab("blocks")}
174
+ className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
175
+ tab === "blocks"
176
+ ? "bg-neutral-800 text-white"
177
+ : "text-neutral-500 hover:text-neutral-200"
178
+ }`}
179
+ >
180
+ Catalog
181
+ </button>
182
+ </Tooltip>
190
183
  </div>
191
184
  {onToggleCollapse && (
192
185
  <button
@@ -267,7 +260,7 @@ export const LeftSidebar = memo(
267
260
  </div>
268
261
  )}
269
262
 
270
- {STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && (
263
+ {tab === "blocks" && (
271
264
  <BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
272
265
  )}
273
266
 
@@ -1,5 +1,14 @@
1
- import { useState, useRef, useCallback, useEffect, useId, type ReactNode } from "react";
1
+ import {
2
+ useState,
3
+ useRef,
4
+ useCallback,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useId,
8
+ type ReactNode,
9
+ } from "react";
2
10
  import { createPortal } from "react-dom";
11
+ import { clampCentredLeft } from "../editor/floatingPanel";
3
12
 
4
13
  interface TooltipProps {
5
14
  label: string;
@@ -19,6 +28,8 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
19
28
  const [resolvedSide, setResolvedSide] = useState<"top" | "bottom">(side);
20
29
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
21
30
  const triggerRef = useRef<HTMLSpanElement>(null);
31
+ const bubbleRef = useRef<HTMLDivElement>(null);
32
+ const [bubbleWidth, setBubbleWidth] = useState(0);
22
33
  // WCAG 4.1.2: programmatically associate the bubble with its trigger.
23
34
  const tooltipId = useId();
24
35
 
@@ -40,13 +51,11 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
40
51
  ) {
41
52
  nextSide = "top";
42
53
  }
43
- const x = Math.min(
44
- Math.max(rect.left + rect.width / 2, VIEWPORT_MARGIN),
45
- window.innerWidth - VIEWPORT_MARGIN,
46
- );
47
54
  setResolvedSide(nextSide);
48
55
  setPos({
49
- x,
56
+ // Raw trigger centre; clamped to the viewport at render, once the
57
+ // bubble's own width is known (see clampedX).
58
+ x: rect.left + rect.width / 2,
50
59
  y: nextSide === "top" ? rect.top - 6 : rect.bottom + 6,
51
60
  });
52
61
  setVisible(true);
@@ -61,6 +70,12 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
61
70
  setVisible(false);
62
71
  }, []);
63
72
 
73
+ // Measure before paint so a wide bubble near a viewport edge is clamped in
74
+ // the same commit it appears in (no visible jump).
75
+ useLayoutEffect(() => {
76
+ setBubbleWidth(visible ? (bubbleRef.current?.offsetWidth ?? 0) : 0);
77
+ }, [visible, label]);
78
+
64
79
  // WCAG 1.4.13: tooltip content must be dismissible with Escape.
65
80
  useEffect(() => {
66
81
  if (!visible) return;
@@ -71,6 +86,8 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
71
86
  return () => document.removeEventListener("keydown", onKeyDown);
72
87
  }, [visible, hide]);
73
88
 
89
+ const clampedX = clampCentredLeft(pos.x, bubbleWidth, window.innerWidth, VIEWPORT_MARGIN);
90
+
74
91
  return (
75
92
  <>
76
93
  <span
@@ -89,12 +106,13 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
89
106
  <div
90
107
  className="fixed z-[200] pointer-events-none"
91
108
  style={{
92
- left: pos.x,
109
+ left: clampedX,
93
110
  top: pos.y,
94
111
  transform: resolvedSide === "top" ? "translate(-50%, -100%)" : "translate(-50%, 0)",
95
112
  }}
96
113
  >
97
114
  <div
115
+ ref={bubbleRef}
98
116
  role="tooltip"
99
117
  id={tooltipId}
100
118
  className="px-2 py-1 rounded-md bg-neutral-800 border border-neutral-700/50 text-[10px] font-medium text-neutral-200 whitespace-nowrap shadow-lg"
@@ -7,7 +7,6 @@ import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
7
7
  import { isEditableTarget } from "../utils/timelineDiscovery";
8
8
  import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
9
9
  import { canSplitElement } from "../utils/timelineElementSplit";
10
- import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
11
10
  import { trackStudioEvent } from "../utils/studioTelemetry";
12
11
  import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
13
12
 
@@ -258,7 +257,7 @@ function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks
258
257
  }
259
258
  }
260
259
 
261
- if (STUDIO_RAZOR_TOOL_ENABLED && key === "b" && !event.shiftKey && !event.altKey) {
260
+ if (key === "b" && !event.shiftKey && !event.altKey) {
262
261
  event.preventDefault();
263
262
  const { activeTool, setActiveTool } = usePlayerStore.getState();
264
263
  setActiveTool(activeTool === "razor" ? "select" : "razor");
@@ -4,7 +4,6 @@
4
4
  * Extracted from useDomEditSession to keep file sizes under the 600-line limit.
5
5
  */
6
6
  import { useEffect, useRef } from "react";
7
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
8
7
  import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
9
8
  import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
10
9
  import type { SidebarTab } from "../components/sidebar/LeftSidebar";
@@ -53,7 +52,7 @@ export function useDomEditPreviewSync({
53
52
 
54
53
  // fallow-ignore-next-line complexity
55
54
  const syncSelectionFromDocument = async () => {
56
- if (!STUDIO_INSPECTOR_PANELS_ENABLED || captionEditMode) return;
55
+ if (captionEditMode) return;
57
56
  const currentSelection = domEditSelectionRef.current;
58
57
  if (!currentSelection) return;
59
58
  let doc: Document | null = null;
@@ -9,7 +9,6 @@
9
9
  */
10
10
  import { useCallback, useEffect, useRef } from "react";
11
11
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
12
- import { STUDIO_GSAP_PANEL_ENABLED } from "../components/editor/manualEditingAvailability";
13
12
  import { usePlayerStore } from "../player";
14
13
  import { useDomEditPreviewSync } from "./useDomEditPreviewSync";
15
14
  import { useGsapAnimationsForElement, usePopulateKeyframeCacheForFile } from "./useGsapTweenCache";
@@ -195,7 +194,7 @@ export function useDomEditWiring({
195
194
  const gsapSourceFile = domEditSelection?.sourceFile || activeCompPath || "index.html";
196
195
 
197
196
  usePopulateKeyframeCacheForFile(
198
- STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null,
197
+ projectId ?? null,
199
198
  gsapSourceFile,
200
199
  gsapCacheVersion,
201
200
  previewIframeRef,
@@ -206,7 +205,7 @@ export function useDomEditWiring({
206
205
  multipleTimelines: gsapMultipleTimelines,
207
206
  unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
208
207
  } = useGsapAnimationsForElement(
209
- STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null,
208
+ projectId ?? null,
210
209
  gsapSourceFile,
211
210
  domEditSelection
212
211
  ? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null }
@@ -16,7 +16,6 @@ import {
16
16
  replaceDomEditGroupSelection,
17
17
  seedDomEditGroupWithSelection,
18
18
  } from "../utils/domEditHelpers";
19
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
20
19
  import {
21
20
  findElementForSelection,
22
21
  findElementForTimelineElement,
@@ -164,14 +163,6 @@ export function useDomSelection({
164
163
  setSelectedTimelineElementId(null);
165
164
  return;
166
165
  }
167
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
168
- domEditSelectionRef.current = null;
169
- domEditGroupSelectionsRef.current = [];
170
- setDomEditSelection(null);
171
- setDomEditGroupSelections([]);
172
- setSelectedTimelineElementId(null);
173
- return;
174
- }
175
166
 
176
167
  const isAdditiveSelection = Boolean(options?.additive);
177
168
  const currentSelection = domEditSelectionRef.current;
@@ -370,7 +361,6 @@ export function useDomSelection({
370
361
 
371
362
  const handleTimelineElementSelect = useCallback(
372
363
  async (element: TimelineElement | null) => {
373
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
374
364
  const seq = ++timelineSelectSeqRef.current;
375
365
  if (!element) {
376
366
  applyDomSelection(null, { revealPanel: false });
@@ -513,14 +503,6 @@ export function useDomSelection({
513
503
  const applyMarqueeSelection = useCallback(
514
504
  // fallow-ignore-next-line complexity
515
505
  (selections: DomEditSelection[], additive: boolean) => {
516
- // Honor the inspector-panels kill switch like applyDomSelection does.
517
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
518
- domEditSelectionRef.current = null;
519
- domEditGroupSelectionsRef.current = [];
520
- setDomEditSelection(null);
521
- setDomEditGroupSelections([]);
522
- return;
523
- }
524
506
  if (selections.length === 0) {
525
507
  if (!additive) applyDomSelection(null, { revealPanel: false });
526
508
  return;
@@ -557,15 +539,6 @@ export function useDomSelection({
557
539
  [applyDomSelection, timelineElements, setSelectedTimelineElementId],
558
540
  );
559
541
 
560
- // Disabled inspector effect
561
- // eslint-disable-next-line no-restricted-syntax
562
- useEffect(() => {
563
- if (STUDIO_INSPECTOR_PANELS_ENABLED) return;
564
- updateDomEditHoverSelection(null);
565
- applyDomSelection(null, { revealPanel: false });
566
- if (rightPanelTab !== "renders") setRightPanelTab("renders");
567
- }, [applyDomSelection, rightPanelTab, updateDomEditHoverSelection, setRightPanelTab]);
568
-
569
542
  return {
570
543
  // State
571
544
  domEditSelection,
@@ -1,7 +1,6 @@
1
1
  import { useCallback, useRef } from "react";
2
2
  import { liveTime, usePlayerStore } from "../player";
3
3
  import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers";
4
- import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability";
5
4
  import { type DomEditSelection } from "../components/editor/domEditing";
6
5
  import type { ApplyDomSelectionOptions, ResolveDomSelectionOptions } from "./useDomSelection";
7
6
  import { trackStudioEvent } from "../utils/studioTelemetry";
@@ -87,7 +86,7 @@ export function usePreviewInteraction({
87
86
  const handlePreviewCanvasMouseDown = useCallback(
88
87
  // fallow-ignore-next-line complexity
89
88
  async (e: React.MouseEvent<HTMLDivElement>, options?: PreviewMouseDownOptions) => {
90
- if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return;
89
+ if (captionEditMode || compositionLoading) return;
91
90
 
92
91
  // Manual double-click detection (see DOUBLE_CLICK_MS): the first click
93
92
  // re-renders the overlay so `e.detail` never reaches 2 on the canvas.
@@ -236,7 +235,7 @@ export function usePreviewInteraction({
236
235
  const handlePreviewCanvasPointerMove = useCallback(
237
236
  // fallow-ignore-next-line complexity
238
237
  async (e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
239
- if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) {
238
+ if (captionEditMode || compositionLoading) {
240
239
  updateDomEditHoverSelection(null);
241
240
  return null;
242
241
  }
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useMemo, useRef, useState, type DragEvent } from "react";
2
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
3
2
  import type { DomEditSelection } from "../components/editor/domEditing";
4
3
  import type { StudioContextValue } from "../contexts/StudioContext";
5
4
  import type { RightInspectorPanes } from "../utils/studioHelpers";
@@ -85,17 +84,14 @@ export function useInspectorState(
85
84
  // fallow-ignore-next-line complexity
86
85
  return useMemo(() => {
87
86
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
88
- const layersPanelActive =
89
- STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.layers;
90
- const designPanelActive =
91
- STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.design;
87
+ const layersPanelActive = inspectorTabActive && rightInspectorPanes.layers;
88
+ const designPanelActive = inspectorTabActive && rightInspectorPanes.design;
92
89
  const inspectorPanelActive = layersPanelActive || designPanelActive;
93
90
  return {
94
91
  layersPanelActive,
95
92
  designPanelActive,
96
93
  inspectorPanelActive,
97
- inspectorButtonActive:
98
- STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive,
94
+ inspectorButtonActive: !rightCollapsed && inspectorPanelActive,
99
95
  // Deliberately wider than shouldShowSelectedDomBounds: the on-canvas path
100
96
  // handles ARE the arc-drag affordance, so gating them on an open Inspector
101
97
  // would make keyframe path editing reachable only from a side panel.
@@ -32,7 +32,6 @@ import {
32
32
  useTimelineTrackLayout,
33
33
  } from "./useTimelineTrackLayout";
34
34
  import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
35
- import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
36
35
  import { useTrackGapMenu } from "./useTrackGapMenu";
37
36
  import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
38
37
  import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext";
@@ -126,7 +125,7 @@ export const Timeline = memo(function Timeline({
126
125
  ),
127
126
  [gsapAnimations],
128
127
  );
129
- const labelMode = STUDIO_KEYFRAMES_ENABLED && hasKeyframedClips;
128
+ const labelMode = hasKeyframedClips;
130
129
  // Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD
131
130
  // (dropping it would jam clip 0 against the gutter on every non-keyframed
132
131
  // composition); in label mode the 232px label column already provides it.
@@ -17,7 +17,6 @@ import {
17
17
  } from "./timelineMultiDragPreview";
18
18
  import type { TimelineLaneBaseProps } from "./timelineLaneProps";
19
19
  import type { TimelineEditCallbacks } from "./timelineCallbacks";
20
- import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
21
20
  import { trackStudioKeyframeLaneExpand } from "../../telemetry/events";
22
21
  import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
23
22
  import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
@@ -134,9 +133,12 @@ export function TimelineLanes({
134
133
  // The one keyframed element this track shows lanes for (selected, else
135
134
  // most lanes). A track can hold several elements; scoping to one keeps
136
135
  // their keyframes from cramming into a single row.
137
- const keyframeClip = STUDIO_KEYFRAMES_ENABLED
138
- ? resolveTrackKeyframeClip(els, laneCounts, selectedElementId, selectedElementIds)
139
- : null;
136
+ const keyframeClip = resolveTrackKeyframeClip(
137
+ els,
138
+ laneCounts,
139
+ selectedElementId,
140
+ selectedElementIds,
141
+ );
140
142
  const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
141
143
  const keyframeClipExpanded =
142
144
  keyframeClipKey != null && expandedClipIds.has(keyframeClipKey);
@@ -249,8 +251,7 @@ export function TimelineLanes({
249
251
  // Only the track's active keyframe clip shows expanded lanes;
250
252
  // other clips (incl. siblings on a shared track) show compact
251
253
  // diamonds on their own bar instead.
252
- const isTrackKeyframeClip =
253
- STUDIO_KEYFRAMES_ENABLED && elementKey === keyframeClipKey;
254
+ const isTrackKeyframeClip = elementKey === keyframeClipKey;
254
255
  const showsLanes = isTrackKeyframeClip && keyframeClipExpanded;
255
256
  const capabilities = getTimelineEditCapabilities(el);
256
257
  const isSelected =
@@ -428,35 +429,32 @@ export function TimelineLanes({
428
429
  renderClipContent,
429
430
  renderClipOverlay,
430
431
  )}
431
- {STUDIO_KEYFRAMES_ENABLED &&
432
- !showsLanes &&
433
- keyframeCache?.get(elementKey) && (
434
- <TimelineClipDiamonds
435
- keyframesData={keyframeCache.get(elementKey)!}
436
- clipWidthPx={Math.max(previewElement.duration * pps, 4)}
437
- clipHeightPx={rowHeight - 2 * CLIP_Y}
438
- beatsActive={beatStripOnTrack}
439
- accentColor={clipStyle.accent}
440
- isSelected={isSelected}
441
- currentPercentage={
442
- previewElement.duration > 0
443
- ? ((currentTime - previewElement.start) /
444
- previewElement.duration) *
445
- 100
446
- : 0
447
- }
448
- elementId={elementKey}
449
- selectedKeyframes={selectedKeyframes}
450
- onClickKeyframe={(_elId, target) =>
451
- onClickKeyframe?.(previewElement, target)
452
- }
453
- onShiftClickKeyframe={onShiftClickKeyframe}
454
- onContextMenuKeyframe={onContextMenuKeyframe}
455
- onMoveKeyframe={onMoveKeyframe}
456
- onSelectSegment={onSelectSegment}
457
- suppressClickRef={suppressClickRef}
458
- />
459
- )}
432
+ {!showsLanes && keyframeCache?.get(elementKey) && (
433
+ <TimelineClipDiamonds
434
+ keyframesData={keyframeCache.get(elementKey)!}
435
+ clipWidthPx={Math.max(previewElement.duration * pps, 4)}
436
+ clipHeightPx={rowHeight - 2 * CLIP_Y}
437
+ beatsActive={beatStripOnTrack}
438
+ accentColor={clipStyle.accent}
439
+ isSelected={isSelected}
440
+ currentPercentage={
441
+ previewElement.duration > 0
442
+ ? ((currentTime - previewElement.start) / previewElement.duration) *
443
+ 100
444
+ : 0
445
+ }
446
+ elementId={elementKey}
447
+ selectedKeyframes={selectedKeyframes}
448
+ onClickKeyframe={(_elId, target) =>
449
+ onClickKeyframe?.(previewElement, target)
450
+ }
451
+ onShiftClickKeyframe={onShiftClickKeyframe}
452
+ onContextMenuKeyframe={onContextMenuKeyframe}
453
+ onMoveKeyframe={onMoveKeyframe}
454
+ onSelectSegment={onSelectSegment}
455
+ suppressClickRef={suppressClickRef}
456
+ />
457
+ )}
460
458
  </TimelineClip>
461
459
  );
462
460
  // Mounted for the track's keyframe clip in BOTH disclosure
@@ -1,7 +1,6 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
3
3
  import { usePlayerStore } from "../store/playerStore";
4
- import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
5
4
  import { useStudioShellContextOptional } from "../../contexts/StudioContext";
6
5
  import { animationContributesLane } from "./TimelinePropertyLanes";
7
6
 
@@ -34,7 +33,6 @@ export function useAutoExpandKeyframedClips(gsapAnimations: Map<string, GsapAnim
34
33
  const projectId = useStudioShellContextOptional()?.projectId ?? null;
35
34
  const seen = useRef({ projectId, source: gsapAnimations, clips: new Set<string>() });
36
35
  useEffect(() => {
37
- if (!STUDIO_KEYFRAMES_ENABLED) return;
38
36
  if (seen.current.projectId !== projectId) {
39
37
  const sourceChanged = seen.current.source !== gsapAnimations;
40
38
  seen.current = { projectId, source: gsapAnimations, clips: new Set() };
@@ -2,7 +2,6 @@ import { useMemo, useRef } from "react";
2
2
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
3
3
  import { animationLaneGroups } from "./TimelinePropertyLanes";
4
4
  import { usePlayerStore, type TimelineElement } from "../store/playerStore";
5
- import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
6
5
  import type { DraggedClipState } from "./timelineClipDragTypes";
7
6
  import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
8
7
  import {
@@ -90,10 +89,7 @@ function useTimelineRowHeights(
90
89
  });
91
90
  return {
92
91
  laneCounts,
93
- rowHeights: trackHeights(
94
- heightTracks,
95
- STUDIO_KEYFRAMES_ENABLED ? expandedClipIds : undefined,
96
- ),
92
+ rowHeights: trackHeights(heightTracks, expandedClipIds),
97
93
  };
98
94
  }, [expandedClipIds, gsapAnimations, tracks, selectedElementId, selectedElementIds]);
99
95
  const rowHeightsRef = useRef<readonly number[]>(rowHeights);
@@ -34,12 +34,8 @@ describe("resolveMasterCompositionPath", () => {
34
34
 
35
35
  describe("normalizeStudioUrlPanelTab", () => {
36
36
  it("accepts slideshow and variables as valid tabs", () => {
37
- expect(normalizeStudioUrlPanelTab("slideshow", { inspectorPanelsEnabled: true })).toBe(
38
- "slideshow",
39
- );
40
- expect(normalizeStudioUrlPanelTab("variables", { inspectorPanelsEnabled: true })).toBe(
41
- "variables",
42
- );
37
+ expect(normalizeStudioUrlPanelTab("slideshow")).toBe("slideshow");
38
+ expect(normalizeStudioUrlPanelTab("variables")).toBe("variables");
43
39
  });
44
40
  });
45
41
 
@@ -184,9 +180,10 @@ describe("studio url state", () => {
184
180
  ).toBe("compositions/title.html");
185
181
  });
186
182
 
187
- it("normalizes url tabs against feature flags", () => {
183
+ it("passes through every valid tab and rejects unknown ones", () => {
188
184
  expect(normalizeStudioUrlPanelTab("renders")).toBe("renders");
189
- expect(normalizeStudioUrlPanelTab("layers", { inspectorPanelsEnabled: false })).toBe("renders");
185
+ expect(normalizeStudioUrlPanelTab("layers")).toBe("layers");
186
+ expect(normalizeStudioUrlPanelTab("nope" as never)).toBeNull();
190
187
  });
191
188
 
192
189
  it("hydrates seek first, preserves the initial url state, then restores selection", async () => {
@@ -1,6 +1,5 @@
1
1
  import type { RightPanelTab } from "./studioHelpers";
2
2
  import { buildProjectHash, parseProjectHashRoute } from "./projectRouting";
3
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
4
3
  import { roundTo3 } from "./rounding";
5
4
 
6
5
  export interface StudioUrlSelectionState {
@@ -34,17 +33,9 @@ export function resolveMasterCompositionPath(fileTree: string[]): string | null
34
33
  return fileTree.find((p) => p.endsWith(".html")) ?? null;
35
34
  }
36
35
 
37
- export function normalizeStudioUrlPanelTab(
38
- tab: RightPanelTab | null,
39
- options: {
40
- inspectorPanelsEnabled?: boolean;
41
- } = {},
42
- ): RightPanelTab | null {
36
+ export function normalizeStudioUrlPanelTab(tab: RightPanelTab | null): RightPanelTab | null {
43
37
  if (!tab) return null;
44
38
  if (!VALID_TABS.includes(tab)) return null;
45
- const inspectorPanelsEnabled = options.inspectorPanelsEnabled ?? STUDIO_INSPECTOR_PANELS_ENABLED;
46
-
47
- if (!inspectorPanelsEnabled && tab !== "renders") return "renders";
48
39
  return tab;
49
40
  }
50
41