import { useState, useRef, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { springs, microInteraction } from "@/lib/motion"; import { Sun, Moon, Monitor, RotateCcw, } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { useAppStore } from "@/stores/app-store"; import type { AppSettings } from "@/types/settings"; import type { ColumnWidth } from "@/types/board"; interface SettingsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } type Tab = "appearance" | "boards" | "shortcuts" | "about"; const THEME_OPTIONS: { value: AppSettings["theme"]; label: string; icon: typeof Sun; }[] = [ { value: "light", label: "Light", icon: Sun }, { value: "dark", label: "Dark", icon: Moon }, { value: "system", label: "System", icon: Monitor }, ]; const ACCENT_PRESETS: { hue: string; label: string }[] = [ { hue: "160", label: "Teal" }, { hue: "240", label: "Blue" }, { hue: "300", label: "Purple" }, { hue: "350", label: "Pink" }, { hue: "25", label: "Red" }, { hue: "55", label: "Orange" }, { hue: "85", label: "Yellow" }, { hue: "130", label: "Lime" }, { hue: "200", label: "Cyan" }, { hue: "0", label: "Slate" }, ]; const DENSITY_OPTIONS: { value: AppSettings["density"]; label: string; }[] = [ { value: "compact", label: "Compact" }, { value: "comfortable", label: "Comfortable" }, { value: "spacious", label: "Spacious" }, ]; const WIDTH_OPTIONS: { value: ColumnWidth; label: string }[] = [ { value: "narrow", label: "Narrow" }, { value: "standard", label: "Standard" }, { value: "wide", label: "Wide" }, ]; const SHORTCUTS: { key: string; description: string; category: string }[] = [ { key: "Ctrl+K", description: "Open command palette", category: "Navigation" }, { key: "Ctrl+Z", description: "Undo", category: "Board" }, { key: "Ctrl+Shift+Z", description: "Redo", category: "Board" }, { key: "?", description: "Keyboard shortcuts", category: "Navigation" }, { key: "Escape", description: "Close modal / cancel", category: "Navigation" }, ]; const TABS: { value: Tab; label: string }[] = [ { value: "appearance", label: "Appearance" }, { value: "boards", label: "Boards" }, { value: "shortcuts", label: "Shortcuts" }, { value: "about", label: "About" }, ]; function SectionLabel({ children }: { children: React.ReactNode }) { return ( ); } export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { const [tab, setTab] = useState("appearance"); const settings = useAppStore((s) => s.settings); const setTheme = useAppStore((s) => s.setTheme); const setAccentColor = useAppStore((s) => s.setAccentColor); const setUiZoom = useAppStore((s) => s.setUiZoom); const setDensity = useAppStore((s) => s.setDensity); const setDefaultColumnWidth = useAppStore((s) => s.setDefaultColumnWidth); const roRef = useRef(null); const [height, setHeight] = useState("auto"); // Callback ref: sets up ResizeObserver when dialog content mounts in portal const contentRef = useCallback((node: HTMLDivElement | null) => { if (roRef.current) { roRef.current.disconnect(); roRef.current = null; } if (node) { const measure = () => setHeight(node.getBoundingClientRect().height); measure(); roRef.current = new ResizeObserver(measure); roRef.current.observe(node); } }, []); return ( 0 ? height : "auto" }} initial={false} transition={{ type: "spring", stiffness: 500, damping: 30 }} className="overflow-hidden" >
Settings Configure your OpenPylon preferences. {/* Tab bar */}
{TABS.map((t) => ( ))}
{/* Tab content — entire dialog height animates between tabs */} {tab === "appearance" && ( <> {/* Theme */}
Theme
{THEME_OPTIONS.map(({ value, label, icon: Icon }) => ( ))}
{/* UI Zoom */}
UI Zoom
{Math.round(settings.uiZoom * 100)}% {settings.uiZoom !== 1 && ( )}
setUiZoom(parseFloat(e.target.value))} className="w-full accent-pylon-accent" />
75% 100% 150%
{/* Accent Color */}
Accent Color
{ACCENT_PRESETS.map(({ hue, label }) => { const isAchromatic = hue === "0"; const bg = isAchromatic ? "oklch(50% 0 0)" : `oklch(55% 0.12 ${hue})`; return ( setAccentColor(hue)} className="size-7 rounded-full" style={{ backgroundColor: bg, outline: settings.accentColor === hue ? "2px solid currentColor" : "none", outlineOffset: "2px", }} whileHover={microInteraction.hover} whileTap={microInteraction.tap} transition={springs.snappy} aria-label={label} title={label} /> ); })}
{/* Density */}
Density
{DENSITY_OPTIONS.map(({ value, label }) => ( ))}
)} {tab === "boards" && (
Default Column Width
{WIDTH_OPTIONS.map(({ value, label }) => ( ))}
)} {tab === "shortcuts" && (
{SHORTCUTS.map(({ key, description }) => (
{description} {key}
))}
)} {tab === "about" && (

OpenPylon

v0.1.0 · Local-first Kanban board

Built with Tauri, React, and TypeScript.

)}
); }