feat(nav): Settings as a page module (Design B)
Move Settings out of the right-side overlay into a left-nav page (page='settings'), so it opens where you're already looking — no cross-screen mouse travel. The Settings rail item now sets the page (with active highlight) instead of opening a dialog; the panel is refactored to an in-flow .glass card (keeps the frosted look over the ambient backdrop), with the page title shown in the header. Removed the overlay + Esc/backdrop close path.
This commit is contained in:
parent
b82016c818
commit
c5330e02f5
5 changed files with 71 additions and 102 deletions
|
|
@ -23,13 +23,15 @@ const TABS: { id: TabId; icon: typeof Monitor }[] = [
|
|||
{ id: "account", icon: User },
|
||||
];
|
||||
|
||||
// Settings as a page (Design B): rendered in the main content area, not an overlay. It keeps
|
||||
// the frosted `.glass` surface so it still refracts the ambient backdrop. The page title is
|
||||
// shown by the Header; the tab rail stays as in-page sub-navigation.
|
||||
export default function SettingsPanel({
|
||||
me,
|
||||
theme,
|
||||
setTheme,
|
||||
view,
|
||||
setView,
|
||||
onClose,
|
||||
onOpenWizard,
|
||||
}: {
|
||||
me: Me;
|
||||
|
|
@ -37,92 +39,53 @@ export default function SettingsPanel({
|
|||
setTheme: (t: ThemePrefs) => void;
|
||||
view: "grid" | "list";
|
||||
setView: (v: "grid" | "list") => void;
|
||||
onClose: () => void;
|
||||
onOpenWizard: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = useState<TabId>("appearance");
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setClosing(true);
|
||||
setTimeout(onClose, 190);
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") close();
|
||||
}
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [close]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 flex justify-end">
|
||||
<div
|
||||
className={`absolute inset-0 bg-black/50 ${
|
||||
closing ? "animate-[overlayOut_0.19s_ease_forwards]" : "animate-[overlayIn_0.2s_ease]"
|
||||
}`}
|
||||
onClick={close}
|
||||
/>
|
||||
<div
|
||||
className={`glass relative self-start m-3 w-[min(94vw,520px)] max-h-[calc(100vh-1.5rem)] rounded-2xl overflow-hidden flex flex-col ${
|
||||
closing
|
||||
? "animate-[panelOut_0.19s_ease-in_forwards]"
|
||||
: "animate-[panelIn_0.26s_cubic-bezier(0.22,1,0.36,1)]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 h-14 border-b border-border/60 shrink-0">
|
||||
<div className="text-base font-semibold">{t("settings.title")}</div>
|
||||
<button
|
||||
onClick={close}
|
||||
className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card/60 transition"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Vertical tab rail — never wraps; active is a clear accent pill. */}
|
||||
<nav className="w-32 sm:w-36 shrink-0 border-r border-border/60 p-2 flex flex-col gap-1">
|
||||
{TABS.map((tabItem) => {
|
||||
const active = tab === tabItem.id;
|
||||
return (
|
||||
<button
|
||||
key={tabItem.id}
|
||||
onClick={() => setTab(tabItem.id)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left transition ${
|
||||
active
|
||||
? "bg-accent text-accent-fg font-medium shadow-md shadow-accent/20"
|
||||
: "text-muted hover:text-fg hover:bg-card/60"
|
||||
}`}
|
||||
>
|
||||
<tabItem.icon className="w-4 h-4 shrink-0" />
|
||||
{t(`settings.tabs.${tabItem.id}`)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* All tabs stacked in one grid cell so the panel sizes to the tallest tab
|
||||
(stable height, no jump on switch); the active one is shown on top. */}
|
||||
<div className="grid flex-1 overflow-y-auto">
|
||||
{TABS.map((tabItem) => (
|
||||
<div
|
||||
<div className="p-4 max-w-3xl w-full mx-auto">
|
||||
<div className="glass rounded-2xl overflow-hidden flex">
|
||||
{/* Vertical tab rail — never wraps; active is a clear accent pill. */}
|
||||
<nav className="w-32 sm:w-36 shrink-0 border-r border-border/60 p-2 flex flex-col gap-1">
|
||||
{TABS.map((tabItem) => {
|
||||
const active = tab === tabItem.id;
|
||||
return (
|
||||
<button
|
||||
key={tabItem.id}
|
||||
className={`[grid-area:1/1] p-4 ${
|
||||
tab === tabItem.id ? "" : "invisible pointer-events-none"
|
||||
onClick={() => setTab(tabItem.id)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left transition ${
|
||||
active
|
||||
? "bg-accent text-accent-fg font-medium shadow-md shadow-accent/20"
|
||||
: "text-muted hover:text-fg hover:bg-card/60"
|
||||
}`}
|
||||
>
|
||||
{tabItem.id === "appearance" && (
|
||||
<Appearance theme={theme} setTheme={setTheme} view={view} setView={setView} />
|
||||
)}
|
||||
{tabItem.id === "notifications" && <Notifications />}
|
||||
{tabItem.id === "sync" && <Sync me={me} />}
|
||||
{tabItem.id === "account" && <Account me={me} onOpenWizard={onOpenWizard} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<tabItem.icon className="w-4 h-4 shrink-0" />
|
||||
{t(`settings.tabs.${tabItem.id}`)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* All tabs stacked in one grid cell so the panel sizes to the tallest tab
|
||||
(stable height, no jump on switch); the active one is shown on top. */}
|
||||
<div className="grid flex-1 min-w-0">
|
||||
{TABS.map((tabItem) => (
|
||||
<div
|
||||
key={tabItem.id}
|
||||
className={`[grid-area:1/1] p-4 ${
|
||||
tab === tabItem.id ? "" : "invisible pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
{tabItem.id === "appearance" && (
|
||||
<Appearance theme={theme} setTheme={setTheme} view={view} setView={setView} />
|
||||
)}
|
||||
{tabItem.id === "notifications" && <Notifications />}
|
||||
{tabItem.id === "sync" && <Sync me={me} />}
|
||||
{tabItem.id === "account" && <Account me={me} onOpenWizard={onOpenWizard} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue