0262ed3d97
- Sidebar bottom-left now shows the signed-in user; clicking opens a menu with Settings and Sign out. New /settings page: account info + change password (POST /auth/change-password, re-verifies current password). Export/restore/ delete are stubbed there for the next pass. - Per-tree default/home person: tree.home_person_id (migration) + TreeUpdate/ Read; the tree and family views open focused on it; the person page gets a "Set as default" control and "Default person" badge. Cleared if that person is deleted. Complements the account-level "this is me" link. - Tree visualization now fills the content area (AppShell drops the max-width column on the /tree route); other pages stay centered. - Audit records are coerced JSON-safe (UUIDs/enums), so PATCHing UUID fields like home_person_id audits cleanly. 50 backend tests pass; migration up/down verified; frontend builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { Menu } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useState } from "react";
|
|
|
|
import { AppSidebar } from "@/components/app-sidebar";
|
|
|
|
/**
|
|
* App chrome: a fixed sidebar on md+, and on small screens a top bar with a
|
|
* hamburger that opens the same sidebar as a slide-in drawer.
|
|
*/
|
|
export function AppShell({ children }: { children: React.ReactNode }) {
|
|
const [open, setOpen] = useState(false);
|
|
const pathname = usePathname();
|
|
// The tree visualization wants the whole canvas; everything else reads better
|
|
// in a centered, max-width column.
|
|
const fullWidth = /^\/trees\/[^/]+\/tree$/.test(pathname);
|
|
|
|
return (
|
|
<div className="flex min-h-screen">
|
|
<aside className="sticky top-0 hidden h-screen w-64 shrink-0 border-r border-[var(--border)] bg-[var(--surface)] md:flex md:flex-col">
|
|
<AppSidebar />
|
|
</aside>
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
{/* Mobile top bar */}
|
|
<div className="flex items-center gap-3 border-b border-[var(--border)] bg-[var(--surface)] px-4 py-3 md:hidden">
|
|
<button
|
|
onClick={() => setOpen(true)}
|
|
aria-label="Open menu"
|
|
className="text-[var(--muted)] hover:text-[var(--foreground)]"
|
|
>
|
|
<Menu className="h-6 w-6" />
|
|
</button>
|
|
<Link href="/" aria-label="Provenance — home">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src="/provenance-logo-plain.svg" alt="Provenance" className="h-6 w-auto" />
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Mobile drawer */}
|
|
{open && (
|
|
<div className="fixed inset-0 z-50 md:hidden">
|
|
<div
|
|
className="absolute inset-0 bg-black/40"
|
|
onClick={() => setOpen(false)}
|
|
aria-hidden
|
|
/>
|
|
<aside className="absolute left-0 top-0 h-full w-72 max-w-[85%] border-r border-[var(--border)] bg-[var(--surface)] shadow-xl">
|
|
<AppSidebar onNavigate={() => setOpen(false)} />
|
|
</aside>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className={
|
|
fullWidth
|
|
? "w-full px-4 py-6 md:px-6"
|
|
: "mx-auto w-full max-w-4xl px-6 py-10 md:px-10"
|
|
}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|