Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@
// collision.
{ "file": "packages/cli/src/commands/*.ts", "exports": ["examples"] },
// Options type for the render-queue hook's public startRender callback —
// consumed structurally by callers (StudioRightPanel), not by import.
// consumed structurally by callers (StudioRightPanels), not by import.
{
"file": "packages/studio/src/components/renders/useRenderQueue.ts",
"exports": ["StartRenderOptions"],
Expand Down Expand Up @@ -457,10 +457,6 @@
// require intrusive middleware changes beyond this PR's scope.
"minLines": 6,
"ignore": [
// FileTree.tsx / LeftSidebar.tsx: pre-existing 8-line structural clone
// (shared sidebar node shape); surfaced by the UX-sweep line shifts.
"packages/studio/src/components/editor/FileTree.tsx",
"packages/studio/src/components/sidebar/LeftSidebar.tsx",
// AWS Lambda and GCP Cloud Run deliberately mirror the same distributed
// rendering lifecycle while retaining provider-specific SDK, storage, and
// retry semantics. The Plan v2 AWS adapter extends that existing symmetry;
Expand Down Expand Up @@ -894,8 +890,7 @@
"packages/studio/src/components/sidebar/AudioRow.tsx",
"packages/studio/src/components/sidebar/BlocksTab.tsx",
"packages/studio/src/components/sidebar/CompositionsTab.tsx",
"packages/studio/src/components/sidebar/LeftSidebar.tsx",
"packages/studio/src/components/StudioRightPanel.tsx",
"packages/studio/src/components/StudioRightPanels.tsx",
"packages/studio/src/components/StudioToast.tsx",
"packages/studio/src/components/ui/Tooltip.tsx",
"packages/studio/src/components/ui/useDialogBehavior.ts",
Expand Down
6 changes: 4 additions & 2 deletions docs/studio/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ npx hyperframes preview

- **Preview** shows the built project. The live frame is in the middle, the
timeline is below, project tools are on the left, and the Inspector is on the
right.
- **Code**, **Comps**, **Assets**, and **Catalog** expose the project source,
right. Drag any panel tab to move it, and use **Window** in the header to
reopen a closed panel or reset the layout. Studio remembers your layout for
each project.
- **Code**, **Compositions**, **Assets**, and **Catalog** expose the project source,
compositions, media, and reusable visuals. **Design**, **Layers**,
**Variables**, and **Renders** control the selected work.

Expand Down
4 changes: 2 additions & 2 deletions docs/studio/source.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ the surfaces without exporting or converting the project.

## Make a focused source edit

1. Open **Code** in the left sidebar.
1. Open **Code** in the left panel column.
2. Select the exact file in the project tree.
3. Find the value responsible for the visible result.
4. Make one focused change.
Expand Down Expand Up @@ -68,7 +68,7 @@ must remain true.

## Run Studio Lint

Choose **Lint** at the bottom of the left sidebar. Studio opens **HyperFrame
Choose **Lint** at the bottom of the left panel column. Studio opens **HyperFrame
Lint Results** with errors, warnings, file locations, and fix hints when they
are available.

Expand Down
90 changes: 42 additions & 48 deletions packages/studio/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore";
import { buildProjectApiPath } from "./utils/projectRouting";
import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
import type { LeftSidebarHandle, SidebarTab } from "./components/sidebar/LeftSidebar";
import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents";
import { useRenderQueue } from "./components/renders/useRenderQueue";
import { usePlayerStore } from "./player";
import { StudioOverlays } from "./components/StudioOverlays";
Expand Down Expand Up @@ -48,9 +48,9 @@ import type { DomEditSelection } from "./components/editor/domEditing";
import { StudioHeader } from "./components/StudioHeader";
import { useGestureCommit } from "./hooks/useGestureCommit";
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
import { StudioLeftPanels } from "./components/StudioLeftPanels";
import { EditorShell } from "./components/EditorShell";
import { StudioRightPanel } from "./components/StudioRightPanel";
import { StudioRightPanels } from "./components/StudioRightPanels";
import { TimelineToolbar } from "./components/TimelineToolbar";
import { StudioPlaybackProvider, StudioShellProvider } from "./contexts/StudioContext";
import { PanelLayoutProvider } from "./contexts/PanelLayoutContext";
Expand All @@ -75,7 +75,6 @@ export function StudioApp() {
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
const leftSidebarRef = useRef<LeftSidebarHandle>(null);
const captionEditMode = useCaptionStore((s) => s.isEditMode);
const captionHasSelection = useCaptionStore((s) => s.selectedSegmentIds.size > 0);
const captionSync = useCaptionSync(projectId);
Expand Down Expand Up @@ -202,6 +201,13 @@ export function StudioApp() {
setRightCollapsed: panelLayout.setRightCollapsed,
setRightPanelTab: panelLayout.setRightPanelTab,
});
const dismissBlockParams = useCallback(() => setActiveBlockParams(null), [setActiveBlockParams]);
const setRightPanelTab = useDismissingTabSetter(panelLayout.setRightPanelTab, dismissBlockParams);
const layout = useMemo(
() => ({ ...panelLayout, setRightPanelTab }),
[panelLayout, setRightPanelTab],
);

const clearDomSelectionRef = useRef<() => void>(() => {});
const domEditSelectionBridgeRef = useRef<DomEditSelection | null>(null);
type DomEditDelete = (s: DomEditSelection, o?: { expandGroup?: boolean }) => Promise<void>;
Expand Down Expand Up @@ -234,7 +240,6 @@ export function StudioApp() {
showToast,
syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
leftSidebarRef,
handleCopy,
handlePaste,
handleCut,
Expand All @@ -248,10 +253,6 @@ export function StudioApp() {
forceReloadSdkSession: sdkHandle.forceReload,
onToggleRecording: () => handleToggleRecordingRef.current(),
});
const sidebarTabRef = useRef({
select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t),
get: () => leftSidebarRef.current?.getTab() ?? "compositions",
});
const domEditSession = useDomEditSession({
projectId,
activeCompPath,
Expand All @@ -264,7 +265,7 @@ export function StudioApp() {
setSelectedTimelineElementId,
setTimelineSelectionSet,
setRightCollapsed: panelLayout.setRightCollapsed,
setRightPanelTab: panelLayout.setRightPanelTab,
setRightPanelTab,
showToast,
isRecordingRef: isGestureRecordingRef,
refreshPreviewDocumentVersion,
Expand All @@ -286,8 +287,6 @@ export function StudioApp() {
reloadPreview,
setRefreshKey,
openSourceForSelection: fileManager.openSourceForSelection,
selectSidebarTab: sidebarTabRef.current.select,
getSidebarTab: sidebarTabRef.current.get,
sdkSession: editFlowSdkSession,
publishSdkSession: sdkHandle.publish,
forceReloadSdkSession: sdkHandle.forceReload,
Expand Down Expand Up @@ -367,20 +366,18 @@ export function StudioApp() {
},
[appHotkeys, resetConsoleErrors, refreshPreviewDocumentVersion],
);
const {
designPanelActive,
inspectorPanelActive,
inspectorButtonActive,
shouldShowMotionPath,
shouldShowSelectedDomBounds,
} = useInspectorState(
panelLayout.rightPanelTab,
panelLayout.rightInspectorPanes,
panelLayout.effectiveRightCollapsed,
isPlaying,
domEditSession.domEditSelection,
gestureState === "recording",
);
const rightPanel = useRightPanelIntent();
const { inspectorPanelActive, shouldShowMotionPath, shouldShowSelectedDomBounds } =
useInspectorState(
rightPanel,
isPlaying,
domEditSession.domEditSelection,
gestureState === "recording",
);
// The dock has no separate "railed by window width" state (it shrinks
// panels, never auto-hides the group), so rightCollapsed is already the
// value that decides whether the panel is actually showing.
const inspectorButtonActive = !panelLayout.rightCollapsed && inspectorPanelActive;
useStudioUrlState({
projectId,
activeCompPath,
Expand All @@ -397,7 +394,7 @@ export function StudioApp() {
applyMarqueeSelection: domEditSession.applyMarqueeSelection,
buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
applyDomSelection: domEditSession.applyDomSelection,
setRightPanelTab: panelLayout.setRightPanelTab,
setRightPanelTab,
initialState: initialUrlStateRef.current,
});
const studioCtxValue = buildStudioContextValue({
Expand Down Expand Up @@ -437,7 +434,7 @@ export function StudioApp() {
return (
<StudioShellProvider value={studioCtxValue}>
<StudioPlaybackProvider value={studioCtxValue}>
<PanelLayoutProvider value={panelLayout}>
<PanelLayoutProvider value={layout}>
<FileManagerProvider value={fileManager}>
<DomEditProvider value={domEditSession}>
<div
Expand Down Expand Up @@ -468,26 +465,23 @@ export function StudioApp() {
)}
<ExternalFileConflictBanner coordinator={externalFileChanges} />
<EditorShell
left={
<StudioLeftSidebar
leftSidebarRef={leftSidebarRef}
onSelectComposition={handleSelectComposition}
onAddBlock={handleAddBlock}
onPreviewBlock={setBlockPreview}
onLint={handleLint}
linting={linting}
lintFindingCount={lintModal?.length ?? findingsByFile.size}
lintFindingsByFile={findingsByFile}
lintHasError={hasLintError}
onAddAssetToTimeline={handleAddAssetAtPlayhead}
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
/>
}
right={
panelLayout.effectiveRightCollapsed ? null : (
<StudioRightPanel
designPanelActive={designPanelActive}
panels={
<>
<StudioLeftPanels
onSelectComposition={handleSelectComposition}
onAddBlock={handleAddBlock}
onPreviewBlock={setBlockPreview}
onLint={handleLint}
linting={linting}
lintFindingCount={lintModal?.length ?? findingsByFile.size}
lintFindingsByFile={findingsByFile}
lintHasError={hasLintError}
onAddAssetToTimeline={handleAddAssetAtPlayhead}
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
/>
<StudioRightPanels
activeBlockParams={activeBlockParams}
onDismissBlockParams={dismissBlockParams}
onCloseBlockParams={() => {
setActiveBlockParams(null);
panelLayout.setRightPanelTab("design");
Expand All @@ -504,7 +498,7 @@ export function StudioApp() {
onAutoGroupCarveSources={timelineEditing.handleAutoGroupCarveSources}
onAddMediaOverlay={handleAddMediaOverlay}
/>
)
</>
}
timelineToolbar={timelineToolbar}
renderClipContent={renderClipContent}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ vi.mock("./nle/NLEContext", () => ({
useNLEContext: () => ({
compositionStack: [],
updateCompositionStack: vi.fn(),
containerRef: { current: null },
}),
}));
vi.mock("./nle/useTimelineEditCallbacks", () => ({
Expand All @@ -53,7 +52,14 @@ vi.mock("./nle/PreviewOverlays", () => ({ PreviewOverlays: () => null }));
vi.mock("./nle/TimelinePane", () => ({ TimelinePane: () => null }));
vi.mock("../captions/components/CaptionTimeline", () => ({ CaptionTimeline: () => null }));

Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
Object.assign(globalThis, {
IS_REACT_ACT_ENVIRONMENT: true,
ResizeObserver: class {
observe() {}
unobserve() {}
disconnect() {}
},
});

afterEach(() => {
document.body.innerHTML = "";
Expand All @@ -69,8 +75,7 @@ describe("EditorShell timeline selection sync", () => {
act(() => {
root.render(
<EditorShell
left={null}
right={null}
panels={null}
timelineToolbar={null}
renderClipContent={() => null}
handleTimelineElementDelete={vi.fn()}
Expand Down
Loading
Loading