Apply paper-and-ink design system across all surfaces
Release / build-and-push (push) Has been cancelled
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:
co-authored by
Claude Opus 4.7
parent
e130cda6c5
commit
9612a54e52
+152
-21
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user