Group Settings panel into accordion sections

Multiple-open accordion with per-section state persisted to
localStorage. Sections: General, Backends, Container, Git/SSH,
Tools, Updates. General is open by default; the rest are collapsed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 08:42:18 -07:00
co-authored by Claude Opus 4.7
parent 7acc8b8d39
commit 9b78b4bc62
2 changed files with 210 additions and 137 deletions
@@ -0,0 +1,65 @@
import { useEffect, useState, type ReactNode } from "react";
interface Props {
id: string;
title: string;
defaultOpen?: boolean;
storageKey?: string;
children: ReactNode;
}
function loadOpenState(storageKey: string, fallback: boolean): boolean {
try {
const stored = localStorage.getItem(storageKey);
if (stored === null) return fallback;
return stored === "1";
} catch {
return fallback;
}
}
function persistOpenState(storageKey: string, open: boolean) {
try {
localStorage.setItem(storageKey, open ? "1" : "0");
} catch {
// ignore
}
}
export default function AccordionSection({ id, title, defaultOpen = true, storageKey, children }: Props) {
const key = storageKey ?? `triple-c.accordion.${id}`;
const [open, setOpen] = useState<boolean>(() => loadOpenState(key, defaultOpen));
useEffect(() => {
persistOpenState(key, open);
}, [key, open]);
return (
<div className="border border-[var(--border-color)] rounded">
<button
type="button"
onClick={() => setOpen(o => !o)}
aria-expanded={open}
className="w-full flex items-center justify-between px-3 py-2 text-left text-sm font-medium text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors"
>
<span>{title}</span>
<svg
className={`w-4 h-4 text-[var(--text-secondary)] transition-transform ${open ? "rotate-90" : ""}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
{open && (
<div className="px-3 py-3 border-t border-[var(--border-color)] space-y-4">
{children}
</div>
)}
</div>
);
}