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 {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -10,12 +13,15 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useAppStore } from "@/stores/app-store";
|
import { useAppStore } from "@/stores/app-store";
|
||||||
import type { AppSettings } from "@/types/settings";
|
import type { AppSettings } from "@/types/settings";
|
||||||
|
import type { ColumnWidth } from "@/types/board";
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Tab = "appearance" | "boards" | "shortcuts" | "about";
|
||||||
|
|
||||||
const THEME_OPTIONS: {
|
const THEME_OPTIONS: {
|
||||||
value: AppSettings["theme"];
|
value: AppSettings["theme"];
|
||||||
label: string;
|
label: string;
|
||||||
@@ -26,13 +32,69 @@ const THEME_OPTIONS: {
|
|||||||
{ value: "system", label: "System", icon: Monitor },
|
{ 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) {
|
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 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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="bg-pylon-surface sm:max-w-md">
|
<DialogContent className="bg-pylon-surface sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="font-heading text-pylon-text">
|
<DialogTitle className="font-heading text-pylon-text">
|
||||||
Settings
|
Settings
|
||||||
@@ -42,43 +104,180 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-5">
|
{/* Tab bar */}
|
||||||
{/* Theme section */}
|
<div className="flex gap-1 border-b border-border pb-2">
|
||||||
<div>
|
{TABS.map((t) => (
|
||||||
<label className="mb-2 block font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary">
|
<Button
|
||||||
Theme
|
key={t.value}
|
||||||
</label>
|
variant={tab === t.value ? "secondary" : "ghost"}
|
||||||
<div className="flex gap-2">
|
size="sm"
|
||||||
{THEME_OPTIONS.map(({ value, label, icon: Icon }) => (
|
onClick={() => setTab(t.value)}
|
||||||
<Button
|
className="font-mono text-xs"
|
||||||
key={value}
|
>
|
||||||
type="button"
|
{t.label}
|
||||||
variant={theme === value ? "default" : "outline"}
|
</Button>
|
||||||
size="sm"
|
))}
|
||||||
onClick={() => setTheme(value)}
|
</div>
|
||||||
className="flex-1 gap-2"
|
|
||||||
>
|
{/* Tab content */}
|
||||||
<Icon className="size-4" />
|
<div className="flex flex-col gap-5 pt-1">
|
||||||
{label}
|
{tab === "appearance" && (
|
||||||
</Button>
|
<>
|
||||||
|
{/* Theme */}
|
||||||
|
<div>
|
||||||
|
<SectionLabel>Theme</SectionLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{THEME_OPTIONS.map(({ value, label, icon: Icon }) => (
|
||||||
|
<Button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
variant={settings.theme === value ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setTheme(value)}
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* UI Zoom */}
|
||||||
|
<div>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<Separator />
|
{tab === "about" && (
|
||||||
|
<div className="space-y-2 text-sm text-pylon-text">
|
||||||
{/* About section */}
|
<p className="font-heading text-lg">OpenPylon</p>
|
||||||
<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>
|
|
||||||
<p className="text-pylon-text-secondary">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user