feat: add panel presets

This commit is contained in:
enkeii64 2025-08-04 17:38:02 +10:00
parent ae54aff1a9
commit 3a6bc6ebf7
10 changed files with 750 additions and 262 deletions

View File

@ -30,6 +30,8 @@ export default function Editor() {
setTimeline,
propertiesPanel,
setPropertiesPanel,
activePreset,
resetCounter,
} = usePanelStore();
const {
@ -153,72 +155,301 @@ export default function Editor() {
<div className="h-screen w-screen flex flex-col bg-background overflow-hidden">
<EditorHeader />
<div className="flex-1 min-h-0 min-w-0">
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
{activePreset === "media" ? (
<ResizablePanelGroup
key={`media-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
>
{/* Main content area */}
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem] px-3"
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
{/* Tools Panel */}
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={100 - toolsPanel}
minSize={60}
className="min-w-0 min-h-0"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<MediaPanel />
</ResizablePanel>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-w-0 min-h-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizableHandle withHandle />
{/* Preview Area */}
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-w-0 min-h-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
{/* Timeline */}
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0 px-3 pb-3"
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
) : activePreset === "inspector" ? (
<ResizablePanelGroup
key={`inspector-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
<ResizablePanel
defaultSize={100 - propertiesPanel}
minSize={60}
className="min-w-0 min-h-0"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-w-0 min-h-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
) : activePreset === "vertical-preview" ? (
<ResizablePanelGroup
key={`vertical-preview-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
>
<ResizablePanel
defaultSize={100 - previewPanel}
minSize={60}
className="min-w-0 min-h-0"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-w-0 min-h-0"
>
<PreviewPanel />
</ResizablePanel>
</ResizablePanelGroup>
) : (
<ResizablePanelGroup
key={`default-${activePreset}-${resetCounter}`}
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
{/* Main content area */}
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem] px-3"
>
{/* Tools Panel */}
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle />
{/* Preview Area */}
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-w-0 min-h-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
{/* Timeline */}
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0 px-3 pb-3"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
)}
</div>
<Onboarding />
</div>

View File

@ -29,6 +29,7 @@ import { useRouter } from "next/navigation";
import { FaDiscord } from "react-icons/fa6";
import { useTheme } from "next-themes";
import { usePlaybackStore } from "@/stores/playback-store";
import { PanelPresetSelector } from "./panel-preset-selector";
export function EditorHeader() {
const { getTotalDuration } = useTimelineStore();
@ -147,6 +148,7 @@ export function EditorHeader() {
const rightContent = (
<nav className="flex items-center gap-2">
<PanelPresetSelector />
<KeyboardShortcutsHelp />
<Button
size="sm"

View File

@ -20,7 +20,7 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
useEffect(() => {
let mounted = true;
let ws = wavesurfer.current;
const initWaveSurfer = async () => {
if (!waveformRef.current || !audioUrl) return;
@ -90,7 +90,7 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
const wsToDestroy = ws;
// Detach from ref immediately
wavesurfer.current = null;
// Wait a tick to destroy so any pending operations can complete
requestAnimationFrame(() => {
try {
@ -111,13 +111,13 @@ const AudioWaveform: React.FC<AudioWaveformProps> = ({
return () => {
// Mark component as unmounted
mounted = false;
// Store reference to current wavesurfer instance
const wsToDestroy = wavesurfer.current;
// Immediately clear the ref to prevent accessing it after unmount
wavesurfer.current = null;
// If we have an instance to clean up, do it safely
if (wsToDestroy) {
// Delay destruction to avoid race conditions

View File

@ -77,7 +77,9 @@ export function TabBar() {
<div
className={cn(
"flex flex-col gap-0.5 items-center cursor-pointer opacity-100 hover:opacity-75",
activeTab === tabKey ? "text-primary !opacity-100" : "text-muted-foreground"
activeTab === tabKey
? "text-primary !opacity-100"
: "text-muted-foreground"
)}
onClick={() => setActiveTab(tabKey)}
key={tabKey}

View File

@ -350,18 +350,20 @@ export function PreviewPanel() {
style={{
left: `${
50 +
((dragState.isDragging && dragState.elementId === element.id
? dragState.currentX
: element.x) /
canvasSize.width) *
(
(dragState.isDragging && dragState.elementId === element.id
? dragState.currentX
: element.x) / canvasSize.width
) *
100
}%`,
top: `${
50 +
((dragState.isDragging && dragState.elementId === element.id
? dragState.currentY
: element.y) /
canvasSize.height) *
(
(dragState.isDragging && dragState.elementId === element.id
? dragState.currentY
: element.y) / canvasSize.height
) *
100
}%`,
transform: `translate(-50%, -50%) rotate(${element.rotation}deg) scale(${scaleRatio})`,

View File

@ -94,9 +94,7 @@ const EditableShortcutKey = ({
variant="text"
size="sm"
className={`inline-flex font-sans text-xs rounded px-2 min-w-6 min-h-6 leading-none items-center justify-center shadow-xs border mr-1 cursor-pointer hover:bg-opacity-80 ${
isRecording
? "border-primary bg-primary/10"
: "border bg-accent"
isRecording ? "border-primary bg-primary/10" : "border bg-accent"
}`}
onClick={handleClick}
title={

View File

@ -0,0 +1,91 @@
"use client";
import { Button } from "./ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { ChevronDown, RotateCcw, LayoutPanelTop } from "lucide-react";
import { usePanelStore, type PanelPreset } from "@/stores/panel-store";
const PRESET_LABELS: Record<PanelPreset, string> = {
default: "Default",
media: "Media",
inspector: "Inspector",
"vertical-preview": "Vertical Preview",
};
const PRESET_DESCRIPTIONS: Record<PanelPreset, string> = {
default: "Media, preview, and inspector on top row, timeline on bottom",
media: "Full height media on left, preview and inspector on top row",
inspector: "Full height inspector on right, media and preview on top row",
"vertical-preview": "Full height preview on right for vertical videos",
};
export function PanelPresetSelector() {
const { activePreset, setActivePreset, resetPreset } = usePanelStore();
const handlePresetChange = (preset: PanelPreset) => {
setActivePreset(preset);
};
const handleResetPreset = (preset: PanelPreset, event: React.MouseEvent) => {
event.stopPropagation();
resetPreset(preset);
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="secondary"
size="sm"
className="h-8 px-2 flex items-center gap-1 text-xs"
title="Panel Presets"
>
<LayoutPanelTop className="h-4 w-4" />
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64">
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
Panel Presets
</div>
<DropdownMenuSeparator />
{(Object.keys(PRESET_LABELS) as PanelPreset[]).map((preset) => (
<DropdownMenuItem
key={preset}
onClick={() => handlePresetChange(preset)}
className="flex items-start justify-between gap-2 py-2 cursor-pointer"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">
{PRESET_LABELS[preset]}
</span>
{activePreset === preset && (
<div className="h-2 w-2 rounded-full bg-primary" />
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5 leading-tight">
{PRESET_DESCRIPTIONS[preset]}
</p>
</div>
<Button
variant="secondary"
size="icon"
className="h-6 w-6 shrink-0 opacity-60 hover:opacity-100"
onClick={(e) => handleResetPreset(preset, e)}
title={`Reset ${PRESET_LABELS[preset]} preset`}
>
<RotateCcw className="h-3 w-3" />
</Button>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,137 +1,137 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
import { formatTimeCode, parseTimeCode } from "@/lib/time";
interface EditableTimecodeProps {
time: number;
duration?: number;
format?: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF";
fps?: number;
onTimeChange?: (time: number) => void;
className?: string;
disabled?: boolean;
}
export function EditableTimecode({
time,
duration,
format = "HH:MM:SS:FF",
fps = 30,
onTimeChange,
className,
disabled = false,
}: EditableTimecodeProps) {
const [isEditing, setIsEditing] = useState(false);
const [inputValue, setInputValue] = useState("");
const [hasError, setHasError] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const enterPressedRef = useRef(false);
const formattedTime = formatTimeCode(time, format, fps);
const startEditing = () => {
if (disabled) return;
setIsEditing(true);
setInputValue(formattedTime);
setHasError(false);
enterPressedRef.current = false;
};
const cancelEditing = () => {
setIsEditing(false);
setInputValue("");
setHasError(false);
enterPressedRef.current = false;
};
const applyEdit = () => {
const parsedTime = parseTimeCode(inputValue, format, fps);
if (parsedTime === null) {
setHasError(true);
return;
}
// Clamp time to valid range
const clampedTime = Math.max(
0,
duration ? Math.min(duration, parsedTime) : parsedTime
);
onTimeChange?.(clampedTime);
setIsEditing(false);
setInputValue("");
setHasError(false);
enterPressedRef.current = false;
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
enterPressedRef.current = true;
applyEdit();
} else if (e.key === "Escape") {
e.preventDefault();
cancelEditing();
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
setHasError(false);
};
const handleBlur = () => {
// Only apply edit if Enter wasn't pressed (to avoid double processing)
if (!enterPressedRef.current && isEditing) {
applyEdit();
}
};
// Focus input when entering edit mode
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
if (isEditing) {
return (
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={cn(
"text-xs font-mono bg-transparent border-none outline-none",
"focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded",
"tabular-nums text-primary",
hasError && "text-destructive focus:border-destructive",
className
)}
style={{ width: `${formattedTime.length + 1}ch` }}
placeholder={formattedTime}
/>
);
}
return (
<span
onClick={startEditing}
className={cn(
"text-xs font-mono tabular-nums text-primary cursor-pointer",
"hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors",
disabled && "cursor-default hover:bg-transparent",
className
)}
title={disabled ? undefined : "Click to edit time"}
>
{formattedTime}
</span>
);
}
"use client";
import { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
import { formatTimeCode, parseTimeCode } from "@/lib/time";
interface EditableTimecodeProps {
time: number;
duration?: number;
format?: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF";
fps?: number;
onTimeChange?: (time: number) => void;
className?: string;
disabled?: boolean;
}
export function EditableTimecode({
time,
duration,
format = "HH:MM:SS:FF",
fps = 30,
onTimeChange,
className,
disabled = false,
}: EditableTimecodeProps) {
const [isEditing, setIsEditing] = useState(false);
const [inputValue, setInputValue] = useState("");
const [hasError, setHasError] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const enterPressedRef = useRef(false);
const formattedTime = formatTimeCode(time, format, fps);
const startEditing = () => {
if (disabled) return;
setIsEditing(true);
setInputValue(formattedTime);
setHasError(false);
enterPressedRef.current = false;
};
const cancelEditing = () => {
setIsEditing(false);
setInputValue("");
setHasError(false);
enterPressedRef.current = false;
};
const applyEdit = () => {
const parsedTime = parseTimeCode(inputValue, format, fps);
if (parsedTime === null) {
setHasError(true);
return;
}
// Clamp time to valid range
const clampedTime = Math.max(
0,
duration ? Math.min(duration, parsedTime) : parsedTime
);
onTimeChange?.(clampedTime);
setIsEditing(false);
setInputValue("");
setHasError(false);
enterPressedRef.current = false;
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
enterPressedRef.current = true;
applyEdit();
} else if (e.key === "Escape") {
e.preventDefault();
cancelEditing();
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
setHasError(false);
};
const handleBlur = () => {
// Only apply edit if Enter wasn't pressed (to avoid double processing)
if (!enterPressedRef.current && isEditing) {
applyEdit();
}
};
// Focus input when entering edit mode
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
if (isEditing) {
return (
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={cn(
"text-xs font-mono bg-transparent border-none outline-none",
"focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded",
"tabular-nums text-primary",
hasError && "text-destructive focus:border-destructive",
className
)}
style={{ width: `${formattedTime.length + 1}ch` }}
placeholder={formattedTime}
/>
);
}
return (
<span
onClick={startEditing}
className={cn(
"text-xs font-mono tabular-nums text-primary cursor-pointer",
"hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors",
disabled && "cursor-default hover:bg-transparent",
className
)}
title={disabled ? undefined : "Click to edit time"}
>
{formattedTime}
</span>
);
}

View File

@ -1,32 +1,32 @@
// These are the gradients from Syntax UI (https://syntaxui.com/effects/gradients)
export const syntaxUIGradients = [
// Cyan to Blue gradients
"linear-gradient(to right, #22d3ee, #0ea5e9, #0284c7)",
"linear-gradient(to right, #bfdbfe, #a5f3fc)",
"linear-gradient(to right, #22d3ee, #0ea5e9, #0284c7)",
// Purple gradients
"linear-gradient(to right, #e9d5ff, #d8b4fe, #c084fc)",
"linear-gradient(to right, #c4b5fd, #a78bfa, #8b5cf6)",
// Blue gradients
"linear-gradient(to right, #93c5fd, #60a5fa, #3b82f6)",
"linear-gradient(to right, #93c5fd, #60a5fa, #3b82f6)",
// Green gradients
"linear-gradient(to right, #6ee7b7, #34d399, #10b981)",
"linear-gradient(to right, #d1fae5, #a7f3d0, #6ee7b7)",
// Red gradient
"linear-gradient(to right, #fca5a5, #f87171, #ef4444)",
// Yellow/Orange gradient
"linear-gradient(to right, #fde68a, #fbbf24, #f59e0b)",
// Pink gradient
"linear-gradient(to right, #fbcfe8, #f9a8d4, #f472b6)",
// Neon radial gradient
"radial-gradient(circle at bottom left, #ff00ff, #00ffff)",
];
// These are the gradients from Syntax UI (https://syntaxui.com/effects/gradients)
export const syntaxUIGradients = [
// Cyan to Blue gradients
"linear-gradient(to right, #22d3ee, #0ea5e9, #0284c7)",
"linear-gradient(to right, #bfdbfe, #a5f3fc)",
"linear-gradient(to right, #22d3ee, #0ea5e9, #0284c7)",
// Purple gradients
"linear-gradient(to right, #e9d5ff, #d8b4fe, #c084fc)",
"linear-gradient(to right, #c4b5fd, #a78bfa, #8b5cf6)",
// Blue gradients
"linear-gradient(to right, #93c5fd, #60a5fa, #3b82f6)",
"linear-gradient(to right, #93c5fd, #60a5fa, #3b82f6)",
// Green gradients
"linear-gradient(to right, #6ee7b7, #34d399, #10b981)",
"linear-gradient(to right, #d1fae5, #a7f3d0, #6ee7b7)",
// Red gradient
"linear-gradient(to right, #fca5a5, #f87171, #ef4444)",
// Yellow/Orange gradient
"linear-gradient(to right, #fde68a, #fbbf24, #f59e0b)",
// Pink gradient
"linear-gradient(to right, #fbcfe8, #f9a8d4, #f472b6)",
// Neon radial gradient
"radial-gradient(circle at bottom left, #ff00ff, #00ffff)",
];

View File

@ -1,21 +1,51 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
const DEFAULT_PANEL_SIZES = {
toolsPanel: 45,
previewPanel: 75,
propertiesPanel: 20,
mainContent: 70,
timeline: 30,
} as const;
export type PanelPreset = "default" | "media" | "inspector" | "vertical-preview";
interface PanelState {
// Panel sizes as percentages
interface PanelSizes {
toolsPanel: number;
previewPanel: number;
propertiesPanel: number;
mainContent: number;
timeline: number;
}
const PRESET_CONFIGS: Record<PanelPreset, PanelSizes> = {
default: {
toolsPanel: 25,
previewPanel: 50,
propertiesPanel: 25,
mainContent: 70,
timeline: 30,
},
media: {
toolsPanel: 30,
previewPanel: 45,
propertiesPanel: 25,
mainContent: 100,
timeline: 25,
},
inspector: {
toolsPanel: 25,
previewPanel: 50,
propertiesPanel: 25,
mainContent: 100,
timeline: 25,
},
"vertical-preview": {
toolsPanel: 25,
previewPanel: 40,
propertiesPanel: 35,
mainContent: 100,
timeline: 25,
},
};
interface PanelState extends PanelSizes {
activePreset: PanelPreset;
presetCustomSizes: Record<PanelPreset, Partial<PanelSizes>>;
resetCounter: number;
mediaViewMode: "grid" | "list";
@ -26,26 +56,158 @@ interface PanelState {
setMainContent: (size: number) => void;
setTimeline: (size: number) => void;
setMediaViewMode: (mode: "grid" | "list") => void;
// Preset actions
setActivePreset: (preset: PanelPreset) => void;
resetPreset: (preset: PanelPreset) => void;
getCurrentPresetSizes: () => PanelSizes;
}
export const usePanelStore = create<PanelState>()(
persist(
(set) => ({
// Default sizes - optimized for responsiveness
...DEFAULT_PANEL_SIZES,
(set, get) => ({
...PRESET_CONFIGS.default,
activePreset: "default" as PanelPreset,
presetCustomSizes: {
default: {},
media: {},
inspector: {},
"vertical-preview": {},
},
resetCounter: 0,
mediaViewMode: "grid" as const,
// Actions
setToolsPanel: (size) => set({ toolsPanel: size }),
setPreviewPanel: (size) => set({ previewPanel: size }),
setPropertiesPanel: (size) => set({ propertiesPanel: size }),
setMainContent: (size) => set({ mainContent: size }),
setTimeline: (size) => set({ timeline: size }),
setToolsPanel: (size) => {
const { activePreset, presetCustomSizes } = get();
set({
toolsPanel: size,
presetCustomSizes: {
...presetCustomSizes,
[activePreset]: {
...presetCustomSizes[activePreset],
toolsPanel: size,
},
},
});
},
setPreviewPanel: (size) => {
const { activePreset, presetCustomSizes } = get();
set({
previewPanel: size,
presetCustomSizes: {
...presetCustomSizes,
[activePreset]: {
...presetCustomSizes[activePreset],
previewPanel: size,
},
},
});
},
setPropertiesPanel: (size) => {
const { activePreset, presetCustomSizes } = get();
set({
propertiesPanel: size,
presetCustomSizes: {
...presetCustomSizes,
[activePreset]: {
...presetCustomSizes[activePreset],
propertiesPanel: size,
},
},
});
},
setMainContent: (size) => {
const { activePreset, presetCustomSizes } = get();
set({
mainContent: size,
presetCustomSizes: {
...presetCustomSizes,
[activePreset]: {
...presetCustomSizes[activePreset],
mainContent: size,
},
},
});
},
setTimeline: (size) => {
const { activePreset, presetCustomSizes } = get();
set({
timeline: size,
presetCustomSizes: {
...presetCustomSizes,
[activePreset]: {
...presetCustomSizes[activePreset],
timeline: size,
},
},
});
},
setMediaViewMode: (mode) => set({ mediaViewMode: mode }),
setActivePreset: (preset) => {
const {
activePreset: currentPreset,
presetCustomSizes,
toolsPanel,
previewPanel,
propertiesPanel,
mainContent,
timeline
} = get();
const updatedPresetCustomSizes = {
...presetCustomSizes,
[currentPreset]: {
toolsPanel,
previewPanel,
propertiesPanel,
mainContent,
timeline,
},
};
const defaultSizes = PRESET_CONFIGS[preset];
const customSizes = updatedPresetCustomSizes[preset] || {};
const finalSizes = { ...defaultSizes, ...customSizes };
set({
activePreset: preset,
presetCustomSizes: updatedPresetCustomSizes,
...finalSizes,
});
},
resetPreset: (preset) => {
const { presetCustomSizes, activePreset, resetCounter } = get();
const defaultSizes = PRESET_CONFIGS[preset];
const newPresetCustomSizes = {
...presetCustomSizes,
[preset]: {},
};
const updates: Partial<PanelState> = {
presetCustomSizes: newPresetCustomSizes,
resetCounter: resetCounter + 1,
};
if (preset === activePreset) {
Object.assign(updates, defaultSizes);
}
set(updates);
},
getCurrentPresetSizes: () => {
const { toolsPanel, previewPanel, propertiesPanel, mainContent, timeline } = get();
return { toolsPanel, previewPanel, propertiesPanel, mainContent, timeline };
},
}),
{
name: "panel-sizes",
}
)
);
export { PRESET_CONFIGS };