@hyperframes/studio 0.8.84 → 0.8.85

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.84",
3
+ "version": "0.8.85",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,12 +60,12 @@
60
60
  "dockview-react": "8.3.1",
61
61
  "gsap": "^3.13.0",
62
62
  "mediabunny": "^1.45.3",
63
- "@hyperframes/core": "0.8.84",
64
- "@hyperframes/parsers": "0.8.84",
65
- "@hyperframes/player": "0.8.84",
66
- "@hyperframes/shader-transitions": "0.8.84",
67
- "@hyperframes/studio-server": "0.8.84",
68
- "@hyperframes/sdk": "0.8.84"
63
+ "@hyperframes/core": "0.8.85",
64
+ "@hyperframes/player": "0.8.85",
65
+ "@hyperframes/parsers": "0.8.85",
66
+ "@hyperframes/shader-transitions": "0.8.85",
67
+ "@hyperframes/studio-server": "0.8.85",
68
+ "@hyperframes/sdk": "0.8.85"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@tailwindcss/vite": "^4.3.3",
@@ -81,7 +81,7 @@
81
81
  "vite": "^6.4.2",
82
82
  "vitest": "^4.1.11",
83
83
  "zustand": "^5.0.0",
84
- "@hyperframes/producer": "0.8.84"
84
+ "@hyperframes/producer": "0.8.85"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "react": "19",
@@ -21,7 +21,7 @@ export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcru
21
21
  return (
22
22
  <nav
23
23
  aria-label="Composition navigation"
24
- className="flex items-center gap-1 px-2 h-8 border-b border-neutral-800/50 bg-neutral-900/50 shrink-0"
24
+ className="flex items-center gap-1 px-2 h-8 border-b border-hairline bg-bg-1 shrink-0"
25
25
  >
26
26
  {/* Back button — always goes to parent */}
27
27
  <button
@@ -33,7 +33,7 @@ export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcru
33
33
  });
34
34
  onNavigate(stack.length - 2);
35
35
  }}
36
- className="flex items-center gap-1 px-1.5 py-0.5 rounded-sm text-xs text-neutral-400 hover:text-white hover:bg-neutral-800 active:scale-[0.98] transition-colors"
36
+ className="flex items-center gap-1 px-1.5 py-0.5 rounded-sm text-xs text-text-3 hover:text-text-0 hover:bg-hover active:scale-[0.98] transition-colors"
37
37
  title="Back (Esc, or double-click empty timeline)"
38
38
  aria-label="Back to parent composition"
39
39
  >
@@ -45,9 +45,9 @@ export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcru
45
45
  const isLast = i === stack.length - 1;
46
46
  return (
47
47
  <span key={level.id} className="flex items-center gap-1">
48
- {i > 0 && <CaretRight size={10} className="text-neutral-600 shrink-0" />}
48
+ {i > 0 && <CaretRight size={10} className="text-text-4 shrink-0" />}
49
49
  {isLast ? (
50
- <span className="text-xs text-neutral-200 font-medium">{level.label}</span>
50
+ <span className="text-xs text-text-1 font-medium">{level.label}</span>
51
51
  ) : (
52
52
  <button
53
53
  type="button"
@@ -55,7 +55,7 @@ export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcru
55
55
  trackStudioEvent("navigation", { action: "breadcrumb", target: level.label });
56
56
  onNavigate(i);
57
57
  }}
58
- className="text-xs text-neutral-500 hover:text-neutral-200 transition-colors"
58
+ className="text-xs text-text-3 hover:text-text-1 transition-colors"
59
59
  >
60
60
  {level.label}
61
61
  </button>
@@ -0,0 +1,7 @@
1
+ import { expect, it } from "vitest";
2
+ import { CompositionBreadcrumb, useCompositionStack } from "@hyperframes/studio";
3
+
4
+ it("exports the composition stack hook beside the breadcrumb it feeds", () => {
5
+ expect(typeof useCompositionStack).toBe("function");
6
+ expect(typeof CompositionBreadcrumb).toBe("function");
7
+ });
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export { DEFAULT_SHORTCUT_SECTIONS } from "./player/components/studioShortcuts";
9
9
  export type { ShortcutHint, ShortcutSection } from "./player/components/studioShortcuts";
10
10
  export { CompositionBreadcrumb } from "./components/nle/CompositionBreadcrumb";
11
11
  export type { CompositionLevel } from "./components/nle/CompositionBreadcrumb";
12
+ export { useCompositionStack } from "./components/nle/useCompositionStack";
12
13
 
13
14
  // Player (preview, timeline, playback controls)
14
15
  export {
@@ -134,9 +134,8 @@ export function TimelineLanes({
134
134
  focusedTargetId,
135
135
  rowGeometry,
136
136
  scrollRef,
137
- onToggleRow: (row) => {
138
- if (row.elementId) toggleClipExpandedTracked(row.elementId);
139
- },
137
+ onToggleRow: (row) => row.elementId && toggleClipExpandedTracked(row.elementId),
138
+ onDrillDown,
140
139
  });
141
140
  return (
142
141
  <div
@@ -49,6 +49,7 @@ export interface TimelineLogicalItem {
49
49
  elementId: string;
50
50
  /** The item's time anchor. Clips use their midpoint; ease controls use the segment midpoint. */
51
51
  time: number;
52
+ element?: TimelineElement;
52
53
  keyframeTarget?: TimelineKeyframeTarget;
53
54
  }
54
55
 
@@ -113,6 +114,7 @@ function clipItems(rowId: string, elements: readonly TimelineElement[]): Timelin
113
114
  rowId,
114
115
  elementId: id,
115
116
  time: element.start + element.duration / 2,
117
+ element,
116
118
  };
117
119
  });
118
120
  }
@@ -3,7 +3,7 @@
3
3
  import React, { act, useRef } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
- import { usePlayerStore } from "../store/playerStore";
6
+ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
7
7
  import { createTimelineRowGeometry } from "./timelineLayout";
8
8
  import type { TimelineLogicalRow } from "./timelineKeyboardNavigation";
9
9
  import { useTimelineKeyboardActor } from "./useTimelineKeyboardActor";
@@ -57,6 +57,7 @@ interface HarnessProps {
57
57
  logicalRows?: readonly TimelineLogicalRow[];
58
58
  rowHeights?: readonly number[];
59
59
  onToggleRow?: (target: TimelineLogicalRow) => void;
60
+ onDrillDown?: (element: TimelineElement) => void;
60
61
  }
61
62
 
62
63
  function Harness({
@@ -64,6 +65,7 @@ function Harness({
64
65
  logicalRows = rows,
65
66
  rowHeights = logicalRows.map(() => 48),
66
67
  onToggleRow = vi.fn(),
68
+ onDrillDown,
67
69
  }: HarnessProps) {
68
70
  const scrollRef = useRef<HTMLDivElement>(null);
69
71
  const keyboard = useTimelineKeyboardActor({
@@ -75,6 +77,7 @@ function Harness({
75
77
  ),
76
78
  scrollRef,
77
79
  onToggleRow,
80
+ onDrillDown,
78
81
  });
79
82
  return (
80
83
  <div ref={scrollRef} onFocus={keyboard.onFocus} onKeyDown={keyboard.onKeyDown}>
@@ -228,6 +231,26 @@ describe("useTimelineKeyboardActor", () => {
228
231
  act(() => root.unmount());
229
232
  });
230
233
 
234
+ it("drills into a composition clip on Enter and leaves a plain clip alone", () => {
235
+ const scene = { id: "scene", tag: "div", start: 0, duration: 2, track: 1 };
236
+ const withElements = rows.map((row, index) => ({
237
+ ...row,
238
+ items: row.items.map((item) => ({
239
+ ...item,
240
+ element: index === 0 ? { ...scene, compositionSrc: "scene.html" } : scene,
241
+ })),
242
+ }));
243
+ const onDrillDown = vi.fn();
244
+ const { host, root } = renderHarness({ logicalRows: withElements, onDrillDown });
245
+ const composition = host.querySelector<HTMLElement>('[data-timeline-focus-id="clip-1"]')!;
246
+ const plain = host.querySelector<HTMLElement>('[data-timeline-focus-id="clip-2"]')!;
247
+ expect(key(plain, "Enter").defaultPrevented).toBe(false);
248
+ expect(onDrillDown).not.toHaveBeenCalled();
249
+ expect(key(composition, "Enter").defaultPrevented).toBe(true);
250
+ expect(onDrillDown).toHaveBeenCalledWith(withElements[0]!.items[0]!.element);
251
+ act(() => root.unmount());
252
+ });
253
+
231
254
  it("collapses expanded rows and leaves native header controls to the browser", () => {
232
255
  const onToggleRow = vi.fn();
233
256
  const expandedRows = [{ ...rows[0]!, expanded: true }, ...rows.slice(1)];
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useMemo, type FocusEvent, type KeyboardEvent, type RefObject } from "react";
2
- import { usePlayerStore } from "../store/playerStore";
2
+ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
3
3
  import type { TimelineRowGeometry } from "./timelineLayout";
4
4
  import {
5
5
  isTimelineNavigationKey,
@@ -14,6 +14,7 @@ interface TimelineKeyboardActorInput {
14
14
  rowGeometry: TimelineRowGeometry;
15
15
  scrollRef: RefObject<HTMLDivElement | null>;
16
16
  onToggleRow: (target: TimelineLogicalRow) => void;
17
+ onDrillDown?: (element: TimelineElement) => void;
17
18
  }
18
19
 
19
20
  function eventTarget(event: FocusEvent | KeyboardEvent): HTMLElement | null {
@@ -70,6 +71,7 @@ export function useTimelineKeyboardActor({
70
71
  rowGeometry,
71
72
  scrollRef,
72
73
  onToggleRow,
74
+ onDrillDown,
73
75
  }: TimelineKeyboardActorInput) {
74
76
  const rovingTargetId =
75
77
  (focusedTargetId && locateTimelineLogicalTarget(logicalRows, focusedTargetId)?.target.id) ??
@@ -130,6 +132,12 @@ export function useTimelineKeyboardActor({
130
132
  openContextMenu(targetElement);
131
133
  return;
132
134
  }
135
+ const composition = located.target.kind === "clip" && located.target.element;
136
+ if (event.key === "Enter" && composition && composition.compositionSrc && onDrillDown) {
137
+ event.preventDefault();
138
+ onDrillDown(composition);
139
+ return;
140
+ }
133
141
  if (
134
142
  (event.key !== "Enter" && event.key !== " ") ||
135
143
  located.target.kind !== "row" ||
@@ -140,7 +148,7 @@ export function useTimelineKeyboardActor({
140
148
  event.preventDefault();
141
149
  onToggleRow(located.target);
142
150
  },
143
- [logicalRowCountByTrack, logicalRows, onToggleRow, rowGeometry, scrollRef],
151
+ [logicalRowCountByTrack, logicalRows, onDrillDown, onToggleRow, rowGeometry, scrollRef],
144
152
  );
145
153
 
146
154
  return { rovingTargetId, onFocus, onKeyDown };