@hyperframes/studio 0.6.31 → 0.6.33
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/index-CSG9kRJg.js +138 -0
- package/dist/assets/index-SKRp8mGz.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +52 -1
- package/src/components/StudioLeftSidebar.tsx +4 -0
- package/src/components/StudioPreviewArea.tsx +29 -1
- package/src/components/StudioRightPanel.tsx +46 -37
- package/src/components/TimelineToolbar.tsx +62 -55
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/manualEditingAvailability.ts +10 -1
- package/src/components/nle/NLELayout.tsx +35 -4
- package/src/components/nle/NLEPreview.test.ts +3 -11
- package/src/components/nle/NLEPreview.tsx +20 -41
- package/src/components/nle/usePreviewBlockDrop.ts +109 -0
- package/src/components/sidebar/BlocksTab.tsx +321 -122
- package/src/components/sidebar/LeftSidebar.tsx +58 -41
- package/src/components/ui/Tooltip.tsx +63 -0
- package/src/components/ui/index.ts +1 -0
- package/src/hooks/useBlockCatalog.ts +5 -1
- package/src/hooks/useDomEditSession.ts +19 -1
- package/src/hooks/useFileManager.ts +3 -0
- package/src/main.tsx +6 -0
- package/src/player/components/PlayerControls.tsx +253 -234
- package/src/player/lib/playbackAdapter.test.ts +3 -3
- package/src/player/lib/playbackAdapter.ts +3 -1
- package/src/utils/blockInstaller.ts +65 -32
- package/src/utils/timelineAssetDrop.test.ts +2 -1
- package/src/utils/timelineAssetDrop.ts +8 -1
- package/dist/assets/index-BWBj8I6Q.css +0 -1
- package/dist/assets/index-Do0kAMcy.js +0 -115
|
@@ -3,20 +3,23 @@ import {
|
|
|
3
3
|
useState,
|
|
4
4
|
useCallback,
|
|
5
5
|
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
6
7
|
forwardRef,
|
|
7
8
|
type ReactNode,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { CompositionsTab } from "./CompositionsTab";
|
|
10
11
|
import { AssetsTab } from "./AssetsTab";
|
|
11
12
|
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
12
|
-
import { BlocksTab } from "./BlocksTab";
|
|
13
|
+
import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab";
|
|
13
14
|
import { FileTree } from "../editor/FileTree";
|
|
14
15
|
import { STUDIO_BLOCKS_PANEL_ENABLED } from "../editor/manualEditingAvailability";
|
|
16
|
+
import { Tooltip } from "../ui";
|
|
15
17
|
|
|
16
18
|
export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
|
|
17
19
|
|
|
18
20
|
export interface LeftSidebarHandle {
|
|
19
21
|
selectTab: (tab: SidebarTab) => void;
|
|
22
|
+
getTab: () => SidebarTab;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
const STORAGE_KEY = "hf-studio-sidebar-tab";
|
|
@@ -53,6 +56,7 @@ interface LeftSidebarProps {
|
|
|
53
56
|
linting?: boolean;
|
|
54
57
|
onToggleCollapse?: () => void;
|
|
55
58
|
onAddBlock?: (blockName: string) => void;
|
|
59
|
+
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
56
60
|
takeoverContent?: ReactNode;
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -82,11 +86,14 @@ export const LeftSidebar = memo(
|
|
|
82
86
|
linting,
|
|
83
87
|
onToggleCollapse,
|
|
84
88
|
onAddBlock,
|
|
89
|
+
onPreviewBlock,
|
|
85
90
|
takeoverContent,
|
|
86
91
|
},
|
|
87
92
|
ref,
|
|
88
93
|
) {
|
|
89
94
|
const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
|
|
95
|
+
const tabRef = useRef(tab);
|
|
96
|
+
tabRef.current = tab;
|
|
90
97
|
|
|
91
98
|
const selectTab = useCallback((t: SidebarTab) => {
|
|
92
99
|
setTab(t);
|
|
@@ -94,7 +101,9 @@ export const LeftSidebar = memo(
|
|
|
94
101
|
trackStudioEvent("tab_switch", { panel: "left_sidebar", tab: t });
|
|
95
102
|
}, []);
|
|
96
103
|
|
|
97
|
-
|
|
104
|
+
const getTab = useCallback(() => tabRef.current, []);
|
|
105
|
+
|
|
106
|
+
useImperativeHandle(ref, () => ({ selectTab, getTab }), [selectTab, getTab]);
|
|
98
107
|
|
|
99
108
|
return (
|
|
100
109
|
<div
|
|
@@ -116,51 +125,59 @@ export const LeftSidebar = memo(
|
|
|
116
125
|
: "1fr 1fr 1fr",
|
|
117
126
|
}}
|
|
118
127
|
>
|
|
119
|
-
<
|
|
120
|
-
type="button"
|
|
121
|
-
onClick={() => selectTab("code")}
|
|
122
|
-
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
123
|
-
tab === "code"
|
|
124
|
-
? "bg-neutral-800 text-white"
|
|
125
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
126
|
-
}`}
|
|
127
|
-
>
|
|
128
|
-
Code
|
|
129
|
-
</button>
|
|
130
|
-
<button
|
|
131
|
-
type="button"
|
|
132
|
-
onClick={() => selectTab("compositions")}
|
|
133
|
-
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
134
|
-
tab === "compositions"
|
|
135
|
-
? "bg-neutral-800 text-white"
|
|
136
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
137
|
-
}`}
|
|
138
|
-
>
|
|
139
|
-
Comps
|
|
140
|
-
</button>
|
|
141
|
-
<button
|
|
142
|
-
type="button"
|
|
143
|
-
onClick={() => selectTab("assets")}
|
|
144
|
-
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
145
|
-
tab === "assets"
|
|
146
|
-
? "bg-neutral-800 text-white"
|
|
147
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
148
|
-
}`}
|
|
149
|
-
>
|
|
150
|
-
Assets
|
|
151
|
-
</button>
|
|
152
|
-
{STUDIO_BLOCKS_PANEL_ENABLED && (
|
|
128
|
+
<Tooltip label="Source code editor" side="bottom">
|
|
153
129
|
<button
|
|
154
130
|
type="button"
|
|
155
|
-
onClick={() => selectTab("
|
|
131
|
+
onClick={() => selectTab("code")}
|
|
156
132
|
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
157
|
-
tab === "
|
|
133
|
+
tab === "code"
|
|
158
134
|
? "bg-neutral-800 text-white"
|
|
159
135
|
: "text-neutral-500 hover:text-neutral-200"
|
|
160
136
|
}`}
|
|
161
137
|
>
|
|
162
|
-
|
|
138
|
+
Code
|
|
163
139
|
</button>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
<Tooltip label="Compositions and sub-compositions" side="bottom">
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => selectTab("compositions")}
|
|
145
|
+
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
146
|
+
tab === "compositions"
|
|
147
|
+
? "bg-neutral-800 text-white"
|
|
148
|
+
: "text-neutral-500 hover:text-neutral-200"
|
|
149
|
+
}`}
|
|
150
|
+
>
|
|
151
|
+
Comps
|
|
152
|
+
</button>
|
|
153
|
+
</Tooltip>
|
|
154
|
+
<Tooltip label="Videos, images, audio, fonts" side="bottom">
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={() => selectTab("assets")}
|
|
158
|
+
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
159
|
+
tab === "assets"
|
|
160
|
+
? "bg-neutral-800 text-white"
|
|
161
|
+
: "text-neutral-500 hover:text-neutral-200"
|
|
162
|
+
}`}
|
|
163
|
+
>
|
|
164
|
+
Assets
|
|
165
|
+
</button>
|
|
166
|
+
</Tooltip>
|
|
167
|
+
{STUDIO_BLOCKS_PANEL_ENABLED && (
|
|
168
|
+
<Tooltip label="Browse blocks and components" side="bottom">
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => selectTab("blocks")}
|
|
172
|
+
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
173
|
+
tab === "blocks"
|
|
174
|
+
? "bg-neutral-800 text-white"
|
|
175
|
+
: "text-neutral-500 hover:text-neutral-200"
|
|
176
|
+
}`}
|
|
177
|
+
>
|
|
178
|
+
Catalog
|
|
179
|
+
</button>
|
|
180
|
+
</Tooltip>
|
|
164
181
|
)}
|
|
165
182
|
</div>
|
|
166
183
|
{onToggleCollapse && (
|
|
@@ -238,8 +255,8 @@ export const LeftSidebar = memo(
|
|
|
238
255
|
</div>
|
|
239
256
|
)}
|
|
240
257
|
|
|
241
|
-
{STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" &&
|
|
242
|
-
<BlocksTab onAddBlock={onAddBlock} />
|
|
258
|
+
{STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && (
|
|
259
|
+
<BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
|
|
243
260
|
)}
|
|
244
261
|
|
|
245
262
|
{/* Lint button pinned at the bottom */}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, type ReactNode } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
|
|
4
|
+
interface TooltipProps {
|
|
5
|
+
label: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
delay?: number;
|
|
8
|
+
side?: "top" | "bottom";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipProps) {
|
|
12
|
+
const [visible, setVisible] = useState(false);
|
|
13
|
+
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|
14
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
15
|
+
const triggerRef = useRef<HTMLSpanElement>(null);
|
|
16
|
+
|
|
17
|
+
const show = useCallback(() => {
|
|
18
|
+
timerRef.current = setTimeout(() => {
|
|
19
|
+
const el = triggerRef.current;
|
|
20
|
+
if (!el) return;
|
|
21
|
+
const child = el.firstElementChild as HTMLElement | null;
|
|
22
|
+
const rect = (child ?? el).getBoundingClientRect();
|
|
23
|
+
if (rect.width === 0 && rect.height === 0) return;
|
|
24
|
+
setPos({
|
|
25
|
+
x: rect.left + rect.width / 2,
|
|
26
|
+
y: side === "top" ? rect.top - 6 : rect.bottom + 6,
|
|
27
|
+
});
|
|
28
|
+
setVisible(true);
|
|
29
|
+
}, delay);
|
|
30
|
+
}, [delay, side]);
|
|
31
|
+
|
|
32
|
+
const hide = useCallback(() => {
|
|
33
|
+
if (timerRef.current) {
|
|
34
|
+
clearTimeout(timerRef.current);
|
|
35
|
+
timerRef.current = null;
|
|
36
|
+
}
|
|
37
|
+
setVisible(false);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<span ref={triggerRef} onPointerEnter={show} onPointerLeave={hide} className="contents">
|
|
43
|
+
{children}
|
|
44
|
+
</span>
|
|
45
|
+
{visible &&
|
|
46
|
+
createPortal(
|
|
47
|
+
<div
|
|
48
|
+
className="fixed z-[200] pointer-events-none"
|
|
49
|
+
style={{
|
|
50
|
+
left: pos.x,
|
|
51
|
+
top: pos.y,
|
|
52
|
+
transform: side === "top" ? "translate(-50%, -100%)" : "translate(-50%, 0)",
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<div 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">
|
|
56
|
+
{label}
|
|
57
|
+
</div>
|
|
58
|
+
</div>,
|
|
59
|
+
document.body,
|
|
60
|
+
)}
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -56,7 +56,11 @@ export function useBlockCatalog() {
|
|
|
56
56
|
if (search.trim()) {
|
|
57
57
|
const q = search.toLowerCase();
|
|
58
58
|
result = result.filter(
|
|
59
|
-
(b) =>
|
|
59
|
+
(b) =>
|
|
60
|
+
b.title.toLowerCase().includes(q) ||
|
|
61
|
+
b.description.toLowerCase().includes(q) ||
|
|
62
|
+
b.category.toLowerCase().includes(q) ||
|
|
63
|
+
b.tags?.some((t) => t.toLowerCase().includes(q)),
|
|
60
64
|
);
|
|
61
65
|
}
|
|
62
66
|
return result;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import type { TimelineElement } from "../player";
|
|
3
3
|
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
4
4
|
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
|
|
@@ -56,6 +56,7 @@ export interface UseDomEditSessionParams {
|
|
|
56
56
|
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
|
57
57
|
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
58
58
|
selectSidebarTab?: (tab: SidebarTab) => void;
|
|
59
|
+
getSidebarTab?: () => SidebarTab;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// ── Hook ──
|
|
@@ -93,6 +94,7 @@ export function useDomEditSession({
|
|
|
93
94
|
setRefreshKey: _setRefreshKey,
|
|
94
95
|
openSourceForSelection,
|
|
95
96
|
selectSidebarTab,
|
|
97
|
+
getSidebarTab,
|
|
96
98
|
}: UseDomEditSessionParams) {
|
|
97
99
|
void _setRefreshKey;
|
|
98
100
|
|
|
@@ -281,6 +283,22 @@ export function useDomEditSession({
|
|
|
281
283
|
applyStudioManualEditsToPreviewRef,
|
|
282
284
|
]);
|
|
283
285
|
|
|
286
|
+
// Auto-reveal source when an element is selected while the Code tab is active.
|
|
287
|
+
// Use a ref for the callback so the effect only fires on selection changes,
|
|
288
|
+
// not when openSourceForSelection is recreated due to editingFile content updates.
|
|
289
|
+
const openSourceRef = useRef(openSourceForSelection);
|
|
290
|
+
openSourceRef.current = openSourceForSelection;
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (!domEditSelection || !openSourceRef.current || !getSidebarTab) return;
|
|
293
|
+
if (!domEditSelection.sourceFile) return;
|
|
294
|
+
if (getSidebarTab() !== "code") return;
|
|
295
|
+
openSourceRef.current(domEditSelection.sourceFile, {
|
|
296
|
+
id: domEditSelection.id,
|
|
297
|
+
selector: domEditSelection.selector,
|
|
298
|
+
selectorIndex: domEditSelection.selectorIndex,
|
|
299
|
+
});
|
|
300
|
+
}, [domEditSelection, getSidebarTab]);
|
|
301
|
+
|
|
284
302
|
return {
|
|
285
303
|
// State
|
|
286
304
|
domEditSelection,
|
|
@@ -122,6 +122,9 @@ export function useFileManager({
|
|
|
122
122
|
const handleFileSelect = useCallback((path: string) => {
|
|
123
123
|
const pid = projectIdRef.current;
|
|
124
124
|
if (!pid) return;
|
|
125
|
+
revealAbortRef.current?.abort();
|
|
126
|
+
revealAbortRef.current = null;
|
|
127
|
+
revealRequestIdRef.current++;
|
|
125
128
|
// Skip fetching binary content for media files — just set the path for preview
|
|
126
129
|
if (isMediaFile(path)) {
|
|
127
130
|
setEditingFile({ path, content: null });
|
package/src/main.tsx
CHANGED
|
@@ -23,6 +23,12 @@ function errorProps(value: unknown): {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
window.addEventListener("error", (event) => {
|
|
26
|
+
if (event.message?.includes("ResizeObserver loop")) {
|
|
27
|
+
event.stopImmediatePropagation();
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
trackStudioEvent("unhandled_error", {
|
|
27
33
|
...errorProps(event.error),
|
|
28
34
|
error_message: event.message,
|