feat: add settings dialog with theme selection
This commit is contained in:
23
src/App.tsx
23
src/App.tsx
@@ -1,19 +1,37 @@
|
|||||||
import { useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useAppStore } from "@/stores/app-store";
|
import { useAppStore } from "@/stores/app-store";
|
||||||
import { AppShell } from "@/components/layout/AppShell";
|
import { AppShell } from "@/components/layout/AppShell";
|
||||||
import { BoardList } from "@/components/boards/BoardList";
|
import { BoardList } from "@/components/boards/BoardList";
|
||||||
import { BoardView } from "@/components/board/BoardView";
|
import { BoardView } from "@/components/board/BoardView";
|
||||||
import { CommandPalette } from "@/components/command-palette/CommandPalette";
|
import { CommandPalette } from "@/components/command-palette/CommandPalette";
|
||||||
|
import { SettingsDialog } from "@/components/settings/SettingsDialog";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const initialized = useAppStore((s) => s.initialized);
|
const initialized = useAppStore((s) => s.initialized);
|
||||||
const init = useAppStore((s) => s.init);
|
const init = useAppStore((s) => s.init);
|
||||||
const view = useAppStore((s) => s.view);
|
const view = useAppStore((s) => s.view);
|
||||||
|
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
init();
|
init();
|
||||||
}, [init]);
|
}, [init]);
|
||||||
|
|
||||||
|
// Listen for custom event to open settings from TopBar or command palette
|
||||||
|
useEffect(() => {
|
||||||
|
function handleOpenSettings() {
|
||||||
|
setSettingsOpen(true);
|
||||||
|
}
|
||||||
|
document.addEventListener("open-settings-dialog", handleOpenSettings);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("open-settings-dialog", handleOpenSettings);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOpenSettings = useCallback(() => {
|
||||||
|
setSettingsOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center bg-pylon-bg">
|
<div className="flex h-screen items-center justify-center bg-pylon-bg">
|
||||||
@@ -29,7 +47,8 @@ export default function App() {
|
|||||||
<AppShell>
|
<AppShell>
|
||||||
{view.type === "board-list" ? <BoardList /> : <BoardView />}
|
{view.type === "board-list" ? <BoardList /> : <BoardView />}
|
||||||
</AppShell>
|
</AppShell>
|
||||||
<CommandPalette onOpenSettings={() => {}} />
|
<CommandPalette onOpenSettings={handleOpenSettings} />
|
||||||
|
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
86
src/components/settings/SettingsDialog.tsx
Normal file
86
src/components/settings/SettingsDialog.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Sun, Moon, Monitor } 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";
|
||||||
|
|
||||||
|
interface SettingsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
||||||
|
const theme = useAppStore((s) => s.settings.theme);
|
||||||
|
const setTheme = useAppStore((s) => s.setTheme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="bg-pylon-surface sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="font-heading text-pylon-text">
|
||||||
|
Settings
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-pylon-text-secondary">
|
||||||
|
Configure your OpenPylon preferences.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{/* Theme section */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary">
|
||||||
|
Theme
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{THEME_OPTIONS.map(({ value, label, icon: Icon }) => (
|
||||||
|
<Button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
variant={theme === value ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setTheme(value)}
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* About section */}
|
||||||
|
<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">
|
||||||
|
Local-first Kanban board
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user