Apply paper-and-ink design system across all surfaces
Release / build-and-push (push) Has been cancelled

Replaces the generic shadcn/ui gray theme + horizontal top-bar shell with
the paper-and-ink language from the Claude Design handoff bundle: warm
off-white paper, near-black ink, Source Serif 4 + Inter, hairline borders,
muted ink accents (clay/indigo/sage/plum/ochre) used functionally for
calendars and share scopes.

Theme switcher expanded from 2 dimensions (theme × mode) to 7: palette ×
mode × fontPair × density × dashLayout × calView × navStyle. All exposed
in Settings → Appearance and persisted on the users row. Pre-paint script
applies all four data-* attributes from localStorage so reload doesn't
flash.

App shell restructured to a CSS-grid driven by data-nav on <html>: sidebar
on desktop, bottom-nav + FAB under 760px. Four desktop nav modes wired
(sidebar/rail/top/fab-only). Topbar gets a search-→-CommandPalette button,
notification bell, "+ New" quick-add, avatar.

Dashboard, calendar, lists, notes, settings, login, public share viewer,
and quick-add sheet all reskinned. Dashboard editor gains a Preset menu
(classic/split/glance) that fills the layout from the registered widgets.
FullCalendar wrapped in .fc-skin and inherits all paper-and-ink tokens via
CSS variable overrides. Public share viewer (/s/<token>) rebuilt around
ShareFrame: expiration banner, brand strip, eyebrow chip, 38px serif
title, mini-day + mini-map cards, share-rows.

Schema: drops users.theme; adds theme_palette, theme_font_pair,
theme_density, theme_dash_layout, theme_cal_view, theme_nav_style with
defaults that match the design (clay / serif-sans / regular / classic /
month / rail-desktop). Migration 0014_paper_ink_theme.

Middleware sets x-pathname so the AppShell server component can render
bare for /s/* and /login without a route-group refactor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ginnoir
2026-05-07 01:03:21 -05:00
co-authored by Claude Opus 4.7
parent e130cda6c5
commit 9612a54e52
61 changed files with 4173 additions and 894 deletions
+155
View File
@@ -0,0 +1,155 @@
import Link from "next/link";
import { eq } from "drizzle-orm";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { getRegistry } from "@/modules/_core/registry";
import { householdMembers, households, users } from "@/modules/_core/schema";
import { BrandMark } from "@/components/brand-mark";
import { NavIcon } from "@/components/nav-icon";
import { NavLink } from "@/components/nav-link";
type Variant = "side" | "top";
const PRIMARY_NAV: Array<{ href: string; label: string; icon: string }> = [
{ href: "/", label: "Dashboard", icon: "home" },
];
const SECONDARY_NAV: Array<{ href: string; label: string; icon: string }> = [
{ href: "/settings#sharing", label: "Share links", icon: "link" },
];
async function getHouseholdInfo(userId: string) {
const [row] = await db
.select({ household: households })
.from(householdMembers)
.innerJoin(households, eq(householdMembers.householdId, households.id))
.where(eq(householdMembers.userId, userId))
.limit(1);
if (!row) return null;
const members = await db
.select({ id: users.id, name: users.name, image: users.image, email: users.email })
.from(householdMembers)
.innerJoin(users, eq(householdMembers.userId, users.id))
.where(eq(householdMembers.householdId, row.household.id))
.limit(6);
return { household: row.household, members };
}
export async function Sidebar({ variant = "side" }: { variant?: Variant }) {
const { modules } = getRegistry();
const moduleNav = modules.flatMap((m) => (m.nav ? [m.nav] : []));
const navItems = [
...PRIMARY_NAV,
...moduleNav.map((n) => ({ href: n.href, label: n.label, icon: n.icon ?? "circle" })),
{ href: "/settings", label: "Settings", icon: "settings" },
];
const session = await auth();
const householdInfo = session?.user?.id ? await getHouseholdInfo(session.user.id) : null;
if (variant === "top") {
return (
<header className="sidebar sidebar-top">
<Link href="/" className="brand">
<BrandMark />
<span className="brand-name">famapp</span>
</Link>
{navItems.map((n) => (
<NavLink
key={n.href}
href={n.href}
label={n.label}
icon={n.icon}
className="!w-auto !h-9"
/>
))}
<div className="ml-auto" />
{householdInfo && (
<span className="badge">
<NavIcon name="people" className="size-3" />
{householdInfo.household.name}
</span>
)}
</header>
);
}
return (
<aside className="sidebar">
<Link href="/" className="brand">
<BrandMark />
<span className="brand-name">famapp</span>
</Link>
{navItems.map((n) => (
<NavLink key={n.href} href={n.href} label={n.label} icon={n.icon} />
))}
<div className="nav-divider" />
{SECONDARY_NAV.map((n) => (
<NavLink key={n.href} href={n.href} label={n.label} icon={n.icon} />
))}
{householdInfo && <HouseholdPill info={householdInfo} />}
</aside>
);
}
function HouseholdPill({
info,
}: {
info: NonNullable<Awaited<ReturnType<typeof getHouseholdInfo>>>;
}) {
const visible = info.members.slice(0, 3);
return (
<div className="household-pill" title={info.household.name}>
<div style={{ display: "flex" }}>
{visible.map((m, i) => {
const initial = (m.name ?? m.email ?? "?").trim()[0]?.toUpperCase() ?? "?";
return (
<span
key={m.id}
className="avatar avatar-sm"
style={{
background: avatarColor(m.id),
marginLeft: i > 0 ? -6 : 0,
boxShadow: "0 0 0 1.5px var(--card)",
}}
title={m.name ?? m.email ?? undefined}
>
{initial}
</span>
);
})}
</div>
<div className="household-text" style={{ minWidth: 0 }}>
<div
style={{
fontWeight: 500,
color: "var(--ink)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{info.household.name}
</div>
<div className="muted" style={{ fontSize: 11 }}>
{info.members.length} {info.members.length === 1 ? "member" : "members"}
</div>
</div>
</div>
);
}
// Stable color from id — uses the 5 functional accents in rotation.
const ACCENT_HUES = [
"var(--c-household)",
"var(--c-private)",
"var(--c-kids)",
"var(--c-work)",
"var(--c-bills)",
];
function avatarColor(id: string): string {
let hash = 0;
for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) & 0xffff;
return ACCENT_HUES[hash % ACCENT_HUES.length] ?? "var(--c-household)";
}