feat: rewrite settings dialog with tabbed panel — appearance, boards, shortcuts, about
This commit is contained in:
@@ -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 · Local-first Kanban board
|
||||
</p>
|
||||
<p className="text-pylon-text-secondary">
|
||||
Built with Tauri, React, and TypeScript.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user