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
+152 -21
View File
@@ -1,7 +1,23 @@
"use client";
import { THEMES, THEME_MODES } from "@/modules/_core/themes";
import type { ThemeId, ThemeMode } from "@/modules/_core/themes";
import {
PALETTES,
THEME_MODES,
FONT_PAIRS,
DENSITIES,
DASH_LAYOUTS,
CAL_VIEWS,
NAV_STYLES,
} from "@/modules/_core/themes";
import type {
Palette,
ThemeMode,
FontPair,
Density,
DashLayout,
CalView,
NavStyle,
} from "@/modules/_core/themes";
import { useTheme } from "@/hooks/use-theme";
import { Label } from "@/components/ui/label";
import {
@@ -15,34 +31,64 @@ import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface Props {
initialTheme?: ThemeId;
initialPalette?: Palette;
initialMode?: ThemeMode;
initialFontPair?: FontPair;
initialDensity?: Density;
initialDashLayout?: DashLayout;
initialCalView?: CalView;
initialNavStyle?: NavStyle;
signedIn?: boolean;
}
export function ThemePicker({
initialTheme = "default",
initialPalette = "clay",
initialMode = "system",
initialFontPair = "serif-sans",
initialDensity = "regular",
initialDashLayout = "classic",
initialCalView = "month",
initialNavStyle = "rail-desktop",
signedIn = false,
}: Props) {
const { theme, mode, setTheme, setMode } = useTheme(initialTheme, initialMode, signedIn);
const t = useTheme(
{
palette: initialPalette,
mode: initialMode,
fontPair: initialFontPair,
density: initialDensity,
dashLayout: initialDashLayout,
calView: initialCalView,
navStyle: initialNavStyle,
},
signedIn,
);
return (
<div className="space-y-4">
<div className="space-y-5">
<div className="space-y-1.5">
<Label>Theme</Label>
<Select value={theme} onValueChange={(v) => setTheme(v as ThemeId)}>
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
{THEMES.map((t) => (
<SelectItem key={t.id} value={t.id}>
{t.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Label>Palette</Label>
<div className="flex gap-2">
{PALETTES.map((p) => (
<button
key={p.id}
type="button"
onClick={() => t.setPalette(p.id)}
aria-pressed={t.palette === p.id}
title={p.label}
className={cn(
"size-7 rounded-md border-[0.5px] cursor-pointer transition-transform",
t.palette === p.id ? "scale-110" : "hover:scale-105",
)}
style={{
background: p.hex,
borderColor: "var(--hair-2)",
outline: t.palette === p.id ? "2px solid var(--ink)" : "none",
outlineOffset: 2,
}}
/>
))}
</div>
</div>
<div className="space-y-1.5">
@@ -53,14 +99,99 @@ export function ThemePicker({
key={m.id}
variant="outline"
size="sm"
className={cn(mode === m.id && "bg-primary text-primary-foreground")}
onClick={() => setMode(m.id)}
className={cn(t.mode === m.id && "bg-primary text-primary-foreground")}
onClick={() => t.setMode(m.id)}
>
{m.label}
</Button>
))}
</div>
</div>
<div className="space-y-1.5">
<Label>Type pairing</Label>
<Select value={t.fontPair} onValueChange={(v) => t.setFontPair(v as FontPair)}>
<SelectTrigger className="w-72">
<SelectValue />
</SelectTrigger>
<SelectContent>
{FONT_PAIRS.map((f) => (
<SelectItem key={f.id} value={f.id}>
{f.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>Density</Label>
<div className="flex gap-1">
{DENSITIES.map((d) => (
<Button
key={d.id}
variant="outline"
size="sm"
className={cn(t.density === d.id && "bg-primary text-primary-foreground")}
onClick={() => t.setDensity(d.id)}
>
{d.label}
</Button>
))}
</div>
</div>
<div className="space-y-1.5">
<Label>Dashboard layout</Label>
<Select value={t.dashLayout} onValueChange={(v) => t.setDashLayout(v as DashLayout)}>
<SelectTrigger className="w-72">
<SelectValue />
</SelectTrigger>
<SelectContent>
{DASH_LAYOUTS.map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>Calendar default</Label>
<div className="flex gap-1">
{CAL_VIEWS.map((c) => (
<Button
key={c.id}
variant="outline"
size="sm"
className={cn(t.calView === c.id && "bg-primary text-primary-foreground")}
onClick={() => t.setCalView(c.id)}
>
{c.label}
</Button>
))}
</div>
</div>
<div className="space-y-1.5">
<Label>Navigation</Label>
<Select value={t.navStyle} onValueChange={(v) => t.setNavStyle(v as NavStyle)}>
<SelectTrigger className="w-72">
<SelectValue />
</SelectTrigger>
<SelectContent>
{NAV_STYLES.map((n) => (
<SelectItem key={n.id} value={n.id}>
{n.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Mobile (under 760px) always uses bottom nav + FAB.
</p>
</div>
</div>
);
}