Merge pull request #7 from Dvorinka/feat/settings-dialog

feat(frontend): regroup settings dialog, icon renderer tiles
This commit is contained in:
Tomáš Dvořák authored and GitHub committed 2026-09-22 18:05:57 +02:00
commit f4ebdf1f33
6 files changed
+93 -50

No files matched your search

+4
View File
@@ -8,6 +8,10 @@ All notable changes to Dash. Format follows
### Changed
- Settings dialog regrouped into icon-labelled sections (renderer, appearance,
wallpaper/CSS, notifications, auth, backup); renderer tiles now show a
per-style glyph and the theme picker is a segmented control
- Monitoring charts restyled to the Beszel school: titled chart cards with
subtitles, soft area fills on a true time-scaled axis, unit-aware Y ticks,
sorted dotted tooltips, and legend chips on multi-series charts
+79 -49
View File
@@ -1,8 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { Bell, Download, Lock, Upload } from "lucide-react";
import { Bell, Download, HardDrive, Image, LayoutGrid, Lock, Moon, Palette, Sun, Upload, type LucideIcon } from "lucide-react";
import { useBoard } from "@/board/store";
import { api } from "@/api";
import { rendererDescriptions, rendererLabels, rendererList } from "@/renderers";
import { rendererDescriptions, rendererIcons, rendererLabels, rendererList } from "@/renderers";
import { cn } from "@/lib/utils";
import { LOCALES, t } from "@/i18n";
import { Button } from "@/components/ui/button";
@@ -44,6 +44,19 @@ const TRANSPORTS = {
} as const;
type TransportName = keyof typeof TRANSPORTS;
// Group — a labelled settings section. Icon + mono header keeps the dialog
// scannable now that it holds five distinct concerns.
function Group({ icon: Icon, title, children }: { icon: LucideIcon; title: string; children: React.ReactNode }) {
return (
<section className="flex flex-col gap-2.5 border-t border-border/60 pt-4 first:border-0 first:pt-0">
<h3 className="flex items-center gap-1.5 font-mono text-[10.5px] uppercase tracking-[0.12em] text-text-faint">
<Icon size={11} /> {title}
</h3>
{children}
</section>
);
}
export function SettingsDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (o: boolean) => void }) {
const board = useBoard();
const fileRef = useRef<HTMLInputElement>(null);
@@ -143,50 +156,70 @@ export function SettingsDialog({ open, onOpenChange }: { open: boolean; onOpenCh
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-sm">
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>{t("settings.title")}</DialogTitle>
<DialogDescription>{t("settings.desc")}</DialogDescription>
</DialogHeader>
<div className="flex max-h-[70vh] flex-col gap-4 overflow-y-auto pr-1">
<div className="flex flex-col gap-1.5">
<Label>{t("settings.renderer")}</Label>
<div className="grid grid-cols-2 gap-1.5" role="radiogroup" aria-label={t("settings.renderer")}>
{rendererList.map((r) => (
<div className="flex max-h-[72vh] flex-col gap-5 overflow-y-auto pr-1">
<Group icon={LayoutGrid} title={t("settings.renderer")}>
<div className="grid grid-cols-2 gap-2" role="radiogroup" aria-label={t("settings.renderer")}>
{rendererList.map((r) => {
const Icon = rendererIcons[r];
const active = board.renderer === r;
return (
<button
key={r}
type="button"
role="radio"
aria-checked={board.renderer === r}
aria-checked={active}
onClick={() => void board.setSetting("renderer", r)}
className={cn(
"rounded-[8px] border px-3 py-2.5 text-left transition-colors",
board.renderer === r
? "border-border-strong bg-surface-hover"
"flex items-center gap-3 rounded-[9px] border px-3 py-2.5 text-left transition-colors",
active
? "border-accent bg-accent/10"
: "border-border hover:border-border-strong hover:bg-surface-hover",
)}
>
<span className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-[7px] border",
active ? "border-accent/40 bg-accent/15 text-accent" : "border-border bg-bg text-text-faint",
)}>
<Icon size={15} />
</span>
<span className="min-w-0">
<span className="block text-[12.5px] font-medium">{rendererLabels[r]}</span>
<span className="block font-mono text-[10px] text-text-faint">{rendererDescriptions[r]}</span>
<span className="block truncate font-mono text-[10px] text-text-faint">{rendererDescriptions[r]}</span>
</span>
</button>
);
})}
</div>
</Group>
<Group icon={Palette} title={t("settings.appearance")}>
<div className="grid grid-cols-2 gap-3 max-[560px]:grid-cols-1">
<div className="flex flex-col gap-1.5">
<Label>{t("settings.theme")}</Label>
<div className="flex overflow-hidden rounded-[7px] border border-border">
{(["dark", "light"] as const).map((th) => (
<button
key={th}
type="button"
aria-pressed={board.theme === th}
onClick={() => void board.setSetting("theme", th)}
className={cn(
"flex flex-1 items-center justify-center gap-1.5 px-3 py-1.5 text-[12px] transition-colors",
board.theme === th ? "bg-surface-hover text-text" : "text-text-faint hover:text-text",
)}
>
{th === "dark" ? <Moon size={12} /> : <Sun size={12} />}
{th === "dark" ? t("settings.dark") : t("settings.light")}
</button>
))}
</div>
</div>
<div className="flex flex-col gap-1.5">
<Label>{t("settings.theme")}</Label>
<Select value={board.theme} onValueChange={(v) => {
if (v === "dark" || v === "light") void board.setSetting("theme", v);
}}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="dark">{t("settings.dark")}</SelectItem>
<SelectItem value="light">{t("settings.light")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5">
<Label>{t("settings.language")}</Label>
<Select
@@ -201,24 +234,25 @@ export function SettingsDialog({ open, onOpenChange }: { open: boolean; onOpenCh
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="accent">{t("settings.accent")}</Label>
<div className="flex gap-2">
<div className="flex items-center gap-2">
<input
id="accent" type="color" aria-label={t("settings.accent")}
value={/^#[0-9a-fA-F]{6}$/.test(board.accent) ? board.accent : "#3b82f6"}
onChange={(e) => void board.setSetting("accent", e.target.value)}
className="h-8 w-12 cursor-pointer rounded-[6px] border border-border bg-surface p-0.5"
/>
<span className="font-mono text-[11px] text-text-faint">{board.accent || "—"}</span>
<Button type="button" variant="outline" size="sm" onClick={() => void board.setSetting("accent", "")}>
{t("common.reset")}
</Button>
</div>
</div>
</Group>
<div className="flex flex-col gap-1.5">
<Label htmlFor="wallpaper">{t("settings.wallpaper")}</Label>
<Group icon={Image} title={t("settings.wallpaper")}>
<Input
id="wallpaper"
value={board.wallpaper}
@@ -226,22 +260,19 @@ export function SettingsDialog({ open, onOpenChange }: { open: boolean; onOpenCh
placeholder={t("settings.wallpaperPh")}
className="font-mono text-[12px]"
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="custom-css">{t("settings.customCss")}</Label>
<textarea
id="custom-css"
aria-label={t("settings.customCss")}
value={board.customCss}
onChange={(e) => void board.setSetting("customCss", e.target.value)}
placeholder=".item-tile { border-radius: 0; }"
rows={3}
className="w-full resize-y rounded-[7px] border border-border bg-bg px-2.5 py-2 font-mono text-[11.5px] outline-none focus:border-border-strong"
/>
</div>
<p className="text-[11px] text-text-faint">{t("settings.customCssHint")}</p>
</Group>
<div className="flex flex-col gap-1.5">
<Label>{t("settings.notifications")}</Label>
<Group icon={Bell} title={t("settings.notifications")}>
<div className="flex gap-2">
<Select value={transport} onValueChange={(v) => {
// SAFETY: Select only yields TRANSPORTS keys.
@@ -281,13 +312,13 @@ export function SettingsDialog({ open, onOpenChange }: { open: boolean; onOpenCh
{t("settings.notifyFanout")}
</p>
{notifyMsg ? <p className="text-[11px] text-text-faint">{notifyMsg}</p> : null}
</div>
</Group>
<div className="flex flex-col gap-2.5 rounded-lg border border-border p-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-1.5">
<Lock size={12} /> {t("auth.section")}
</Label>
<Group icon={Lock} title={t("auth.section")}>
<div className="flex items-center justify-between rounded-lg border border-border px-3 py-2.5">
<p className="text-[11px] text-text-faint">
{auth.enabled ? `${t("auth.onHint")} ${auth.username ?? ""}` : t("auth.offHint")}
</p>
{auth.enabled ? (
<div className="flex gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => void signOut()}>
@@ -303,12 +334,10 @@ export function SettingsDialog({ open, onOpenChange }: { open: boolean; onOpenCh
</Button>
)}
</div>
<p className="text-[11px] text-text-faint">
{auth.enabled ? `${t("auth.onHint")} ${auth.username ?? ""}` : t("auth.offHint")}
</p>
</div>
</Group>
<div className="flex gap-2 pt-1">
<Group icon={HardDrive} title={t("settings.backup")}>
<div className="flex gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => void exportJson()}>
<Download size={12} /> {t("settings.exportJson")}
</Button>
@@ -326,6 +355,7 @@ export function SettingsDialog({ open, onOpenChange }: { open: boolean; onOpenCh
</div>
<p className="text-[11px] text-text-faint">{t("settings.importHint")}</p>
{msg ? <p className="text-[12px] text-destructive">{msg}</p> : null}
</Group>
</div>
</DialogContent>
</Dialog>
-1
View File
@@ -12,7 +12,6 @@ import {
XAxis,
YAxis,
} from "recharts";
import { cn } from "@/lib/utils";
import { t } from "@/i18n";
/** One chart row: epoch ms plus named series values. */
+1
View File
@@ -109,6 +109,7 @@ export const cs: Partial<Record<Key, string>> = {
"settings.testNotify": "Odeslat testovací notifikaci",
"settings.wallpaperPh": "https://… — prázdné = žádná",
"settings.importFailed": "Import selhal",
"settings.backup": "Záloha a import",
"service.editTitle": "Upravit službu",
"service.newTitle": "Přidat službu",
+1
View File
@@ -108,6 +108,7 @@ export const en = {
"settings.testNotify": "Send test notification",
"settings.wallpaperPh": "https://… — blank for none",
"settings.importFailed": "Import failed",
"settings.backup": "Backup & import",
"service.editTitle": "Edit service",
"service.newTitle": "Add service",
+8
View File
@@ -1,3 +1,4 @@
import { LayoutGrid, LayoutList, Newspaper, SquareTerminal, type LucideIcon } from "lucide-react";
import { bento } from "./bento";
import { cards } from "./cards";
import { editorial } from "./editorial";
@@ -27,3 +28,10 @@ export const rendererDescriptions: Record<RendererName, string> = {
index: "editorial list · masthead",
console: "terminal panels · mono",
};
export const rendererIcons: Record<RendererName, LucideIcon> = {
bento: LayoutGrid,
cards: LayoutList,
index: Newspaper,
console: SquareTerminal,
};