feat: rewrite settings dialog with tabbed panel - appearance, boards, shortcuts, about

This commit is contained in:
Your Name
2026-02-15 20:25:58 +02:00
parent a7c9c83bb0
commit c2928afb11

View File

@@ -1,4 +1,7 @@
import { Sun, Moon, Monitor } from "lucide-react";
import { useState } from "react";
import {
Sun, Moon, Monitor, RotateCcw,
} from "lucide-react";
import {
Dialog,
DialogContent,
@@ -10,12 +13,15 @@ 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;
@@ -26,13 +32,69 @@ const THEME_OPTIONS: {
{ 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 (
<label className="mb-2 block font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary">
{children}
</label>
);
}
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
const theme = useAppStore((s) => s.settings.theme);
const [tab, setTab] = useState<Tab>("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);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-pylon-surface sm:max-w-md">
<DialogContent className="bg-pylon-surface sm:max-w-lg">
<DialogHeader>
<DialogTitle className="font-heading text-pylon-text">
Settings
@@ -42,18 +104,34 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-5">
{/* Theme section */}
{/* Tab bar */}
<div className="flex gap-1 border-b border-border pb-2">
{TABS.map((t) => (
<Button
key={t.value}
variant={tab === t.value ? "secondary" : "ghost"}
size="sm"
onClick={() => setTab(t.value)}
className="font-mono text-xs"
>
{t.label}
</Button>
))}
</div>
{/* Tab content */}
<div className="flex flex-col gap-5 pt-1">
{tab === "appearance" && (
<>
{/* Theme */}
<div>
<label className="mb-2 block font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary">
Theme
</label>
<SectionLabel>Theme</SectionLabel>
<div className="flex gap-2">
{THEME_OPTIONS.map(({ value, label, icon: Icon }) => (
<Button
key={value}
type="button"
variant={theme === value ? "default" : "outline"}
variant={settings.theme === value ? "default" : "outline"}
size="sm"
onClick={() => setTheme(value)}
className="flex-1 gap-2"
@@ -67,18 +145,139 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
<Separator />
{/* About section */}
{/* UI Zoom */}
<div>
<label className="mb-2 block font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary">
About
</label>
<div className="space-y-1 text-sm text-pylon-text">
<p className="font-semibold">OpenPylon v0.1.0</p>
<div className="mb-2 flex items-center justify-between">
<SectionLabel>UI Zoom</SectionLabel>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-pylon-text-secondary">
{Math.round(settings.uiZoom * 100)}%
</span>
{settings.uiZoom !== 1 && (
<Button
variant="ghost"
size="icon-xs"
onClick={() => setUiZoom(1)}
className="text-pylon-text-secondary hover:text-pylon-text"
>
<RotateCcw className="size-3" />
</Button>
)}
</div>
</div>
<input
type="range"
min="0.75"
max="1.5"
step="0.05"
value={settings.uiZoom}
onChange={(e) => setUiZoom(parseFloat(e.target.value))}
className="w-full accent-pylon-accent"
/>
<div className="mt-1 flex justify-between font-mono text-[10px] text-pylon-text-secondary">
<span>75%</span>
<span>100%</span>
<span>150%</span>
</div>
</div>
<Separator />
{/* Accent Color */}
<div>
<SectionLabel>Accent Color</SectionLabel>
<div className="flex flex-wrap gap-2">
{ACCENT_PRESETS.map(({ hue, label }) => {
const isAchromatic = hue === "0";
const bg = isAchromatic
? "oklch(50% 0 0)"
: `oklch(55% 0.12 ${hue})`;
return (
<button
key={hue}
type="button"
onClick={() => setAccentColor(hue)}
className="size-7 rounded-full transition-transform hover:scale-110"
style={{
backgroundColor: bg,
outline: settings.accentColor === hue ? "2px solid currentColor" : "none",
outlineOffset: "2px",
}}
aria-label={label}
title={label}
/>
);
})}
</div>
</div>
<Separator />
{/* Density */}
<div>
<SectionLabel>Density</SectionLabel>
<div className="flex gap-2">
{DENSITY_OPTIONS.map(({ value, label }) => (
<Button
key={value}
type="button"
variant={settings.density === value ? "default" : "outline"}
size="sm"
onClick={() => setDensity(value)}
className="flex-1"
>
{label}
</Button>
))}
</div>
</div>
</>
)}
{tab === "boards" && (
<div>
<SectionLabel>Default Column Width</SectionLabel>
<div className="flex gap-2">
{WIDTH_OPTIONS.map(({ value, label }) => (
<Button
key={value}
type="button"
variant={settings.defaultColumnWidth === value ? "default" : "outline"}
size="sm"
onClick={() => setDefaultColumnWidth(value)}
className="flex-1"
>
{label}
</Button>
))}
</div>
</div>
)}
{tab === "shortcuts" && (
<div className="flex flex-col gap-1">
{SHORTCUTS.map(({ key, description }) => (
<div key={key} className="flex items-center justify-between py-1">
<span className="text-sm text-pylon-text">{description}</span>
<kbd className="rounded bg-pylon-column px-2 py-0.5 font-mono text-xs text-pylon-text-secondary">
{key}
</kbd>
</div>
))}
</div>
)}
{tab === "about" && (
<div className="space-y-2 text-sm text-pylon-text">
<p className="font-heading text-lg">OpenPylon</p>
<p className="text-pylon-text-secondary">
Local-first Kanban board
v0.1.0 &middot; Local-first Kanban board
</p>
<p className="text-pylon-text-secondary">
Built with Tauri, React, and TypeScript.
</p>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>