ui: simplify layout settings and add smooth animations

- Remove success notification when applying layouts (keep only errors)
- Add CSS transitions for smooth window resizing/repositioning
- Replace large settings Dialog with compact Popover
- Reduce settings UI from ~220 lines to ~97 lines
- Remove verbose descriptions and preview section
- Make settings match site's minimal UI patterns
- Settings now update live without Save/Cancel buttons
- Create popover.tsx component using Radix UI primitives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2025-12-18 12:55:26 +01:00
parent f2599772f6
commit d624b5b05a
4 changed files with 113 additions and 212 deletions

View File

@@ -26,10 +26,6 @@ export function TabBar() {
createWorkspace();
};
const handleSettingsClick = (e: React.MouseEvent) => {
e.stopPropagation(); // Prevent workspace switch
setSettingsOpen(true);
};
const handleApplyPreset = (presetId: string) => {
const preset = presets.find((p) => p.id === presetId);
@@ -44,9 +40,6 @@ export function TabBar() {
try {
applyPresetLayout(preset);
toast.success(`Layout applied`, {
description: `Applied "${preset.name}" preset to workspace ${activeWorkspace.number}`,
});
} catch (error) {
toast.error(`Failed to apply layout`, {
description:
@@ -152,22 +145,21 @@ export function TabBar() {
</DropdownMenu>
{/* Window/Layout Settings */}
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={handleSettingsClick}
aria-label="Layout settings"
<WorkspaceSettings
open={settingsOpen}
onOpenChange={setSettingsOpen}
>
<SlidersHorizontal className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
aria-label="Layout settings"
>
<SlidersHorizontal className="h-3 w-3" />
</Button>
</WorkspaceSettings>
</div>
</div>
<WorkspaceSettings
open={settingsOpen}
onOpenChange={setSettingsOpen}
/>
</>
);
}

View File

@@ -1,223 +1,97 @@
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "./ui/dialog";
import { Label } from "./ui/Label";
import { Button } from "./ui/button";
Popover,
PopoverContent,
PopoverTrigger,
} from "./ui/popover";
import { useGrimoire } from "@/core/state";
import type { LayoutConfig } from "@/types/app";
import { cn } from "@/lib/utils";
import {
Sparkles,
SplitSquareHorizontal,
SplitSquareVertical,
} from "lucide-react";
import { useGrimoire } from "@/core/state";
import type { LayoutConfig } from "@/types/app";
import { cn } from "@/lib/utils";
interface WorkspaceSettingsProps {
open: boolean;
onOpenChange: (open: boolean) => void;
children: React.ReactNode;
}
export function WorkspaceSettings({
open,
onOpenChange,
children,
}: WorkspaceSettingsProps) {
const { state, updateLayoutConfig } = useGrimoire();
const config = state.layoutConfig;
// Local state for settings
const [insertionMode, setInsertionMode] = useState<LayoutConfig["insertionMode"]>(
state.layoutConfig?.insertionMode || "smart"
);
const [splitPercentage, setSplitPercentage] = useState(
state.layoutConfig?.splitPercentage || 50
);
const [insertionPosition, setInsertionPosition] = useState<LayoutConfig["insertionPosition"]>(
state.layoutConfig?.insertionPosition || "second"
);
const handleSave = () => {
updateLayoutConfig({
insertionMode,
splitPercentage,
insertionPosition,
});
onOpenChange(false);
const setInsertionMode = (mode: LayoutConfig["insertionMode"]) => {
updateLayoutConfig({ insertionMode: mode });
};
const handleReset = () => {
setInsertionMode("smart");
setSplitPercentage(50);
setInsertionPosition("second");
const setSplitPercentage = (value: number) => {
updateLayoutConfig({ splitPercentage: value });
};
const modes: 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 },
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Layout Settings</DialogTitle>
<DialogDescription>
Configure how new windows are inserted into all workspaces.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Insertion Mode */}
<div className="space-y-3">
<Label className="text-sm font-medium">Insertion Mode</Label>
<div className="grid gap-2">
<button
onClick={() => setInsertionMode("smart")}
className={cn(
"flex items-center gap-3 p-3 rounded-md border-2 transition-all",
insertionMode === "smart"
? "border-primary bg-primary/10"
: "border-border hover:border-muted-foreground/50"
)}
>
<Sparkles className="h-5 w-5 text-primary" />
<div className="flex-1 text-left">
<div className="font-medium text-sm">Balanced (auto)</div>
<div className="text-xs text-muted-foreground">
Automatically balances horizontal and vertical splits
</div>
</div>
</button>
<button
onClick={() => setInsertionMode("row")}
className={cn(
"flex items-center gap-3 p-3 rounded-md border-2 transition-all",
insertionMode === "row"
? "border-primary bg-primary/10"
: "border-border hover:border-muted-foreground/50"
)}
>
<SplitSquareHorizontal className="h-5 w-5 text-primary" />
<div className="flex-1 text-left">
<div className="font-medium text-sm">Horizontal (side-by-side)</div>
<div className="text-xs text-muted-foreground">
New windows always split horizontally
</div>
</div>
</button>
<button
onClick={() => setInsertionMode("column")}
className={cn(
"flex items-center gap-3 p-3 rounded-md border-2 transition-all",
insertionMode === "column"
? "border-primary bg-primary/10"
: "border-border hover:border-muted-foreground/50"
)}
>
<SplitSquareVertical className="h-5 w-5 text-primary" />
<div className="flex-1 text-left">
<div className="font-medium text-sm">Vertical (stacked)</div>
<div className="text-xs text-muted-foreground">
New windows always split vertically
</div>
</div>
</button>
</div>
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent align="end" className="w-56 p-3 space-y-3">
<div className="space-y-1.5">
<div className="text-xs font-medium text-muted-foreground">
Insert Mode
</div>
{/* Split Percentage */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">Split Percentage</Label>
<span className="text-sm text-muted-foreground">
{splitPercentage}% / {100 - splitPercentage}%
</span>
</div>
<input
type="range"
min="10"
max="90"
value={splitPercentage}
onChange={(e) => setSplitPercentage(Number(e.target.value))}
className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-primary"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>Existing content gets {splitPercentage}%</span>
<span>New window gets {100 - splitPercentage}%</span>
</div>
</div>
{/* Insertion Position */}
<div className="space-y-3">
<Label className="text-sm font-medium">Insertion Position</Label>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setInsertionPosition("first")}
className={cn(
"p-3 rounded-md border-2 transition-all text-sm",
insertionPosition === "first"
? "border-primary bg-primary/10"
: "border-border hover:border-muted-foreground/50"
)}
>
<div className="font-medium">Left / Top</div>
<div className="text-xs text-muted-foreground mt-1">
New window first
</div>
</button>
<button
onClick={() => setInsertionPosition("second")}
className={cn(
"p-3 rounded-md border-2 transition-all text-sm",
insertionPosition === "second"
? "border-primary bg-primary/10"
: "border-border hover:border-muted-foreground/50"
)}
>
<div className="font-medium">Right / Bottom</div>
<div className="text-xs text-muted-foreground mt-1">
New window second
</div>
</button>
</div>
</div>
{/* Preview */}
<div className="rounded-md border border-border bg-muted/30 p-4">
<div className="text-xs text-muted-foreground mb-2">Preview:</div>
<div className="text-sm">
<span className="font-medium">
{insertionMode === "smart" && "Smart mode"}
{insertionMode === "row" && "Horizontal splits"}
{insertionMode === "column" && "Vertical splits"}
</span>
{" · "}
<span>
{splitPercentage}%/{100 - splitPercentage}% split
</span>
{" · "}
<span>
New window on{" "}
{insertionPosition === "first" ? "left/top" : "right/bottom"}
</span>
</div>
<div className="space-y-1">
{modes.map((mode) => {
const Icon = mode.icon;
const isActive = config.insertionMode === mode.id;
return (
<button
key={mode.id}
onClick={() => setInsertionMode(mode.id)}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded transition-colors",
isActive
? "bg-accent text-accent-foreground"
: "hover:bg-muted"
)}
>
<Icon className="h-3.5 w-3.5" />
<span>{mode.label}</span>
</button>
);
})}
</div>
</div>
{/* Footer */}
<div className="flex justify-between">
<Button variant="outline" onClick={handleReset}>
Reset to Defaults
</Button>
<div className="flex gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave}>Save Changes</Button>
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs">
<span className="font-medium text-muted-foreground">Split</span>
<span className="text-foreground">
{config.splitPercentage}/{100 - config.splitPercentage}
</span>
</div>
<input
type="range"
min="20"
max="80"
value={config.splitPercentage}
onChange={(e) => setSplitPercentage(Number(e.target.value))}
className="w-full h-1.5 bg-muted rounded-lg appearance-none cursor-pointer accent-accent"
/>
</div>
</DialogContent>
</Dialog>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,30 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 rounded-md border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -123,6 +123,11 @@
background: hsl(var(--background));
}
/* Smooth animations for window resizing and repositioning */
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-tile {
transition: width 0.2s ease-out, height 0.2s ease-out, top 0.2s ease-out, left 0.2s ease-out;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window
.mosaic-window-toolbar {