Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 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
4 changes: 1 addition & 3 deletions packages/studio/src/components/EditorShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,7 @@ function EditorShellBody({
return (
<div
ref={containerRef}
// Shell canvas is a step LIGHTER than the near-black panel cards so the
// gaps between panels read as visible seams (CapCut-style).
className="flex flex-col flex-1 min-h-0 bg-panel-surface"
className="flex flex-col flex-1 min-h-0"
onKeyDown={handleKeyDown}
tabIndex={-1}
>
Expand Down
2 changes: 1 addition & 1 deletion packages/studio/src/components/StudioLeftPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface StudioLeftPanelsProps {

function PanelColumn({ footer, children }: { footer: ReactNode; children: ReactNode }) {
return (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-neutral-950">
<div className="flex h-full min-h-0 flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 flex-col">{children}</div>
{footer}
</div>
Expand Down
8 changes: 8 additions & 0 deletions packages/studio/src/components/dock/Dock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
} from "dockview-react";
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
import { installDockAccessibility } from "./dockAccessibility";
import { DockStripActions } from "./DockStripActions";
import { DockTab } from "./DockTab";
import { installTabFill } from "./dockTabFill";
import { addRegisteredPanel, applySideMinimums, buildEditLayout } from "./dockLayout";
import { DOCK_PANEL_COMPONENT } from "./dockLayoutSchema";
import { useDockLayoutStore, type DockController, type DockSnapshot } from "./dockLayoutStore";
Expand Down Expand Up @@ -150,6 +153,7 @@ function Root({ projectId, children }: { projectId: string | null; children: Rea
applySideMinimums(api);
const root = api.groups[0]?.element.closest<HTMLElement>(".hf-dock");
const disposeAccessibility = root ? installDockAccessibility(api, root) : () => {};
const disposeTabFill = root ? installTabFill(api, root) : () => {};
// The dock spans the window (buildEditLayout sizes against it too); its own box lags a resize.
const resizeObserver = new ResizeObserver(() => applySideMinimums(api, window.innerWidth));
if (root) resizeObserver.observe(root);
Expand Down Expand Up @@ -189,6 +193,7 @@ function Root({ projectId, children }: { projectId: string | null; children: Rea
clearTimeout(timer);
for (const subscription of subscriptions) subscription.dispose();
disposeAccessibility();
disposeTabFill();
resizeObserver.disconnect();
useDockLayoutStore.getState().detach();
};
Expand All @@ -202,8 +207,11 @@ function Root({ projectId, children }: { projectId: string | null; children: Rea
key={projectId ?? ""}
className="hf-dock flex-1 min-h-0"
components={COMPONENTS}
defaultTabComponent={DockTab}
rightHeaderActionsComponent={DockStripActions}
defaultRenderer="always"
disableFloatingGroups
disableTabsOverflowList
onReady={onReady}
/>
{children}
Expand Down
63 changes: 63 additions & 0 deletions packages/studio/src/components/dock/DockStripActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useCallback, useSyncExternalStore } from "react";
import { CornersIn, CornersOut, DotsThree, X } from "@phosphor-icons/react";
import type { IDockviewHeaderActionsProps } from "dockview-react";
import { IconButton, Menu, MenuCheckboxItem, MenuItem, MenuSeparator } from "../ui";
import { useDockLayoutStore } from "./dockLayoutStore";
import { PANEL_DEFINITIONS, isPanelId, panelsInZone } from "./panelRegistry";

/** Panel menu, maximise and close group, drawn on the active group's strip only. */
export function DockStripActions({
api,
containerApi,
panels,
isGroupActive,
}: IDockviewHeaderActionsProps) {
const subscribe = useCallback(
(onChange: () => void) => {
const subscription = containerApi.onDidMaximizedGroupChange(onChange);
return () => subscription.dispose();
},
[containerApi],
);
const maximized = useSyncExternalStore(subscribe, () => api.isMaximized());
const openPanels = useDockLayoutStore((state) => state.openPanels);
const togglePanel = useDockLayoutStore((state) => state.togglePanel);
const resetLayout = useDockLayoutStore((state) => state.resetLayout);
if (!isGroupActive) return null;

const first = panels.find((panel) => isPanelId(panel.id))?.id;
const menuPanels = isPanelId(first) ? panelsInZone(PANEL_DEFINITIONS[first].zone) : [];
return (
<div className="hf-dock-strip-actions">
<Menu
align="end"
aria-label="Panel menu"
trigger={<IconButton size="sm" aria-label="Panel menu" icon={<DotsThree size={16} />} />}
>
{menuPanels.map((id) => (
<MenuCheckboxItem
key={id}
checked={openPanels.has(id)}
onCheckedChange={() => togglePanel(id)}
>
{PANEL_DEFINITIONS[id].title}
</MenuCheckboxItem>
))}
<MenuSeparator />
<MenuItem onClick={resetLayout}>Reset layout</MenuItem>
</Menu>
<IconButton
size="sm"
aria-label={maximized ? "Restore panel" : "Maximize panel"}
icon={maximized ? <CornersIn size={16} /> : <CornersOut size={16} />}
onClick={() => (maximized ? api.exitMaximized() : api.maximize())}
/>
<IconButton
size="sm"
aria-label="Close group"
icon={<X size={16} />}
onClick={() => api.close()}
/>
</div>
);
}
68 changes: 68 additions & 0 deletions packages/studio/src/components/dock/DockTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useCallback, useSyncExternalStore } from "react";
import {
BracketsCurly,
ChartBarHorizontal,
Code,
FilmSlate,
Image,
Layout,
Monitor,
Presentation,
SlidersHorizontal,
SquaresFour,
Stack,
X,
type Icon,
} from "@phosphor-icons/react";
import type { IDockviewPanelHeaderProps } from "dockview-react";
import { isPanelId, type PanelId } from "./panelRegistry";

const TAB_ICONS: Record<PanelId, Icon> = {
preview: Monitor,
timeline: ChartBarHorizontal,
compositions: Layout,
assets: Image,
code: Code,
catalog: SquaresFour,
design: SlidersHorizontal,
layers: Stack,
renders: FilmSlate,
variables: BracketsCurly,
slideshow: Presentation,
};

/**
* A dock tab. dock.css shows the type icon and close glyph on the shown tab only, keyed on
* dockview's own tab class, so they swap in the frame the tab changes rather than a render later.
*/
export function DockTab({ api }: IDockviewPanelHeaderProps) {
const subscribe = useCallback(
(onChange: () => void) => {
const subscription = api.onDidTitleChange(onChange);
return () => subscription.dispose();
},
[api],
);
const title = useSyncExternalStore(subscribe, () => api.title ?? "");
const TypeIcon = isPanelId(api.id) ? TAB_ICONS[api.id] : null;
return (
<div className="hf-dock-tab">
{TypeIcon ? <TypeIcon className="hf-dock-tab-icon" size={14} aria-hidden /> : null}
<span className="hf-dock-tab-label">{title}</span>
{/* Same shape as dockview's own close control: a tab cannot hold a focusable button. */}
<div
role="button"
tabIndex={-1}
aria-label={`Close ${title}`}
className="hf-dock-tab-close"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
api.close();
}}
>
<X size={12} aria-hidden />
</div>
</div>
);
}
Loading
Loading