@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/dist/assets/{hyperframes-player-Dd74fv7B.js → hyperframes-player-BK18SggX.js} +1 -1
- package/dist/assets/{index-DFcIBFM8.js → index-Bus1BKtk.js} +1 -1
- package/dist/assets/{index-D9dmtDap.js → index-CZxhjQpw.js} +1 -1
- package/dist/assets/{index-gjbSm-vt.js → index-ClLdmgua.js} +80 -80
- package/dist/assets/{index-BWJqxeaH.css → index-mzZCha_N.css} +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.html +2 -2
- package/dist/index.js +19 -11
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/nle/CompositionBreadcrumb.tsx +5 -5
- package/src/compositionStackExport.test.ts +7 -0
- package/src/index.ts +1 -0
- package/src/player/components/TimelineLanes.tsx +2 -3
- package/src/player/components/timelineKeyboardNavigation.ts +2 -0
- package/src/player/components/useTimelineKeyboardActor.test.tsx +24 -1
- package/src/player/components/useTimelineKeyboardActor.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
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.
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/shader-transitions": "0.8.
|
|
67
|
-
"@hyperframes/studio-server": "0.8.
|
|
68
|
-
"@hyperframes/sdk": "0.8.
|
|
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
|
+
"@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-
|
|
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-
|
|
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-
|
|
48
|
+
{i > 0 && <CaretRight size={10} className="text-text-4 shrink-0" />}
|
|
49
49
|
{isLast ? (
|
|
50
|
-
<span className="text-xs text-
|
|
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-
|
|
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
|
-
|
|
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 };
|