Files
justin 0262ed3d97 Account menu + Settings (change password); per-tree home person; full-width tree
- 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>
2026-06-07 11:05:04 -04:00

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>
);
}