import { SlidersHorizontal, Grid2X2, Columns2, Split, Sparkles, SplitSquareHorizontal, SplitSquareVertical, } from "lucide-react"; import { Button } from "./ui/button"; import { Slider } from "./ui/slider"; import { useGrimoire } from "@/core/state"; import { getAllPresets } from "@/lib/layout-presets"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { toast } from "sonner"; import type { LayoutConfig } from "@/types/app"; import { useState } from "react"; export function LayoutControls() { const { state, applyPresetLayout, updateLayoutConfig } = useGrimoire(); const { workspaces, activeWorkspaceId, layoutConfig } = state; // Local state for immediate slider feedback (debounced persistence) const [localSplitPercentage, setLocalSplitPercentage] = useState< number | null >(null); const activeWorkspace = workspaces[activeWorkspaceId]; const windowCount = activeWorkspace?.windowIds.length || 0; const presets = getAllPresets(); // Early return if no active workspace or layout config if (!activeWorkspace || !layoutConfig) { return null; } const handleApplyPreset = (presetId: string) => { const preset = presets.find((p) => p.id === presetId); if (!preset) return; if (windowCount < preset.minSlots) { toast.error(`Not enough windows`, { description: `Preset "${preset.name}" requires at least ${preset.minSlots} windows, but only ${windowCount} available.`, }); return; } if (preset.maxSlots && windowCount > preset.maxSlots) { toast.error(`Too many windows`, { description: `Preset "${preset.name}" supports maximum ${preset.maxSlots} windows, but ${windowCount} available.`, }); return; } try { // Enable animations for smooth layout transition document.body.classList.add("animating-layout"); applyPresetLayout(preset); // Remove animation class after transition completes setTimeout(() => { document.body.classList.remove("animating-layout"); }, 180); } catch (error) { document.body.classList.remove("animating-layout"); toast.error(`Failed to apply layout`, { description: error instanceof Error ? error.message : "Unknown error occurred", }); } }; const getPresetIcon = (presetId: string) => { switch (presetId) { case "side-by-side": return ; case "main-sidebar": return ; case "grid": return ; default: return ; } }; const insertionModes: Array<{ id: LayoutConfig["insertionMode"]; label: string; icon: React.ComponentType<{ className?: string }>; }> = [ { id: "smart", label: "Balanced", icon: Sparkles }, { id: "row", label: "Horizontal", icon: SplitSquareHorizontal }, { id: "column", label: "Vertical", icon: SplitSquareVertical }, ]; // Current split percentage (local state during drag, global state otherwise) const displayedSplitPercentage = localSplitPercentage ?? layoutConfig.splitPercentage; return ( {/* Layouts Section */}
Layout Presets
{presets.map((preset) => { const canApply = windowCount >= preset.minSlots; return ( handleApplyPreset(preset.id)} disabled={!canApply} className="flex items-center gap-3 cursor-pointer" >
{getPresetIcon(preset.id)}
{preset.name}
); })} {/* Placement Section */}
Placement
Window insertion
{insertionModes.map((mode) => { const Icon = mode.icon; const isActive = layoutConfig.insertionMode === mode.id; return ( updateLayoutConfig({ insertionMode: mode.id })} className="flex items-center gap-2 cursor-pointer" > {mode.label} {isActive && (
)} ); })} {/* Split Ratio Section */}
Split Ratio {displayedSplitPercentage}/{100 - displayedSplitPercentage}
Default split for new windows
setLocalSplitPercentage(value)} onValueCommit={([value]) => { updateLayoutConfig({ splitPercentage: value }); setLocalSplitPercentage(null); // Clear local state after persist }} min={20} max={80} step={1} className="w-full" />
); }