feat: Settings panel

This commit is contained in:
2026-03-11 14:12:48 +01:00
parent 9c0161b21d
commit 581cfca506
3 changed files with 129 additions and 28 deletions

View File

@@ -1,5 +1,5 @@
/**
* Menubar for the canvas page: Project (Import/Export), Edit (Undo/Redo, Duplicate/Copy/Paste, Rename), View (Fit View, Theme).
* Menubar for the canvas page: Project (Import/Export), Edit (Undo/Redo, Duplicate/Copy/Paste, Rename), View (Fit View, Minimap).
*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -17,9 +17,8 @@ import {
MenubarCheckboxItem,
} from '@/components/ui/menubar'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { useTheme } from '@/lib/themeContext'
import { usePlatform } from '@/app/platform/platformContext'
import { ArrowLeft, ClipboardPaste, Copy, CopyPlus, Download, FolderOpen, Moon, Pencil, Redo2, Sun, Undo2 } from 'lucide-react'
import { ArrowLeft, ClipboardPaste, Copy, CopyPlus, Download, FolderOpen, Pencil, Redo2, Undo2 } from 'lucide-react'
import { Input } from '@/components/ui/input'
export type CanvasMenubarProps = {
@@ -63,7 +62,6 @@ export function CanvasMenubar({
showMinimap = false,
onToggleMinimap,
}: CanvasMenubarProps) {
const { theme, setTheme } = useTheme()
const { projectId } = useParams<{ projectId: string }>()
const { projects, renameProject } = usePlatform()
const projectName = useMemo(
@@ -235,28 +233,6 @@ export function CanvasMenubar({
</span>
</MenubarItem>
)}
{onFitView && <MenubarSeparator />}
<MenubarSub>
<MenubarSubTrigger>Theme</MenubarSubTrigger>
<MenubarSubContent>
<MenubarCheckboxItem
checked={theme === 'light'}
onCheckedChange={() => setTheme('light')}
className="gap-2"
>
<Sun className="h-4 w-4" />
Light
</MenubarCheckboxItem>
<MenubarCheckboxItem
checked={theme === 'dark'}
onCheckedChange={() => setTheme('dark')}
className="gap-2"
>
<Moon className="h-4 w-4" />
Dark
</MenubarCheckboxItem>
</MenubarSubContent>
</MenubarSub>
{onToggleMinimap != null && (
<>
<MenubarSeparator />

View File

@@ -17,12 +17,13 @@ import {
SidebarRail,
SidebarTrigger,
} from '@/components/ui/sidebar'
import { Plus, ListTodo } from 'lucide-react'
import { Plus, ListTodo, Settings } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSidebar } from '@/components/ui/sidebar'
import { usePlatform } from './platformContext'
import { getProjectIcon } from '../../lib/iconMap'
import { NewProjectDialog } from './NewProjectDialog'
import { SettingsDialog } from './SettingsDialog'
import type { Project } from './types'
export function AppSidebar() {
@@ -178,7 +179,20 @@ export function AppSidebar() {
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="relative z-10" />
<SidebarFooter className="relative z-10">
<SidebarMenu>
<SidebarMenuItem>
<SettingsDialog
trigger={
<SidebarMenuButton tooltip="Settings" className="w-full">
<Settings className="size-4" />
<span className="group-data-[collapsible=icon]:hidden">Settings</span>
</SidebarMenuButton>
}
/>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail className="relative z-10" />
</Sidebar>
</>

View File

@@ -0,0 +1,111 @@
/**
* Settings dialog with sidebar-style nav (Appearance, AI). Reference: shadcn sidebar-13.
*/
import React, { useState } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useTheme } from '@/lib/themeContext'
import type { Theme } from '@/lib/themeContext'
import { Sun, Sparkles } from 'lucide-react'
type SettingsSection = 'appearance' | 'ai'
export function SettingsDialog({
trigger,
}: {
trigger: React.ReactNode
}) {
const [open, setOpen] = useState(false)
const [section, setSection] = useState<SettingsSection>('appearance')
const { theme, setTheme } = useTheme()
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent
className="flex h-[min(85vh,28rem)] max-w-2xl p-0 gap-0 overflow-hidden"
aria-describedby={undefined}
>
<DialogHeader className="sr-only">
<DialogTitle>Settings</DialogTitle>
</DialogHeader>
<div className="flex flex-1 min-h-0 w-full">
<nav
className="flex w-44 shrink-0 flex-col gap-1 border-r bg-muted/30 p-2"
aria-label="Settings sections"
>
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
Settings
</div>
<Button
variant={section === 'appearance' ? 'secondary' : 'ghost'}
size="sm"
className="justify-start gap-2"
onClick={() => setSection('appearance')}
>
<Sun className="size-4 shrink-0 opacity-70" />
Appearance
</Button>
<Button
variant={section === 'ai' ? 'secondary' : 'ghost'}
size="sm"
className="justify-start gap-2"
onClick={() => setSection('ai')}
>
<Sparkles className="size-4 shrink-0 opacity-70" />
AI
</Button>
</nav>
<div className="flex-1 min-h-0 overflow-auto p-4">
{section === 'appearance' && (
<div className="space-y-6">
<h3 className="text-sm font-medium text-muted-foreground">Appearance</h3>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
<label htmlFor="settings-theme" className="text-sm font-medium">
Theme
</label>
<Select value={theme} onValueChange={(v) => setTheme(v as Theme)}>
<SelectTrigger id="settings-theme" className="w-full min-w-[8rem] sm:w-40">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
{section === 'ai' && (
<div className="space-y-6">
<h3 className="text-sm font-medium text-muted-foreground">AI</h3>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
<span className="text-sm font-medium text-muted-foreground/80">
AI settings
</span>
<span className="text-xs text-muted-foreground sm:text-right">
Coming soon
</span>
</div>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}