From 9612a54e5242dc87c8f22b64be19f74d0b419939 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Thu, 7 May 2026 01:03:21 -0500 Subject: [PATCH] Apply paper-and-ink design system across all surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 : 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/) 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 --- .gitignore | 3 + drizzle/0014_paper_ink_theme.sql | 7 + drizzle/meta/_journal.json | 7 + eslint.config.mjs | 10 +- src/app/calendar/page.tsx | 13 +- src/app/d/[slug]/page.tsx | 68 +- src/app/d/actions.ts | 3 +- src/app/globals.css | 1227 ++++++++++++++--- src/app/layout.tsx | 111 +- src/app/login/page.tsx | 22 +- src/app/s/[token]/page.tsx | 44 +- src/app/settings/actions.ts | 68 +- src/app/settings/page.tsx | 344 ++++- src/components/app-nav.tsx | 82 -- src/components/app-shell.tsx | 45 + src/components/bottom-nav.tsx | 21 + src/components/brand-mark.tsx | 21 + src/components/dashboard-editor.tsx | 59 +- src/components/edit-dashboard-button.tsx | 6 +- src/components/fab.tsx | 14 + src/components/install-prompt.tsx | 49 +- src/components/nav-icon.tsx | 83 ++ src/components/nav-link.tsx | 55 + src/components/nav-mode-provider.tsx | 29 + src/components/quick-add-fab.tsx | 17 - src/components/quick-add-sheet.tsx | 130 +- src/components/settings-section.tsx | 115 ++ src/components/share/mini-day-card.tsx | 43 + src/components/share/mini-map-card.tsx | 72 + src/components/share/share-banner.tsx | 59 + src/components/share/share-brand-strip.tsx | 29 + src/components/share/share-detail-card.tsx | 42 + src/components/share/share-eyebrow.tsx | 21 + src/components/share/share-frame.tsx | 46 + src/components/sidebar.tsx | 155 +++ src/components/theme-picker.tsx | 173 ++- src/components/topbar-new-button.tsx | 20 + src/components/topbar-search.tsx | 27 + src/components/topbar-title.tsx | 33 + src/components/topbar.tsx | 91 ++ src/components/ui/card.tsx | 25 +- src/hooks/use-theme.ts | 177 ++- src/lib/auth.ts | 7 +- src/lib/dashboard.server.ts | 31 + src/lib/dashboard.ts | 123 +- src/middleware.ts | 9 +- src/modules/_core/manifest.tsx | 18 +- src/modules/_core/schema.ts | 7 +- src/modules/_core/share.ts | 4 + src/modules/_core/themes.ts | 93 +- .../calendar/components/calendar-shell.tsx | 69 +- .../calendar/components/shared-view.tsx | 200 ++- src/modules/calendar/manifest.tsx | 101 +- src/modules/lists/components/list-detail.tsx | 211 ++- src/modules/lists/components/list-widget.tsx | 49 +- src/modules/lists/components/lists-index.tsx | 109 +- src/modules/lists/components/shared-view.tsx | 102 +- src/modules/notes/components/note-editor.tsx | 50 +- src/modules/notes/components/notes-index.tsx | 123 +- src/modules/notes/components/shared-view.tsx | 54 +- src/modules/notes/manifest.tsx | 41 +- 61 files changed, 4173 insertions(+), 894 deletions(-) create mode 100644 drizzle/0014_paper_ink_theme.sql delete mode 100644 src/components/app-nav.tsx create mode 100644 src/components/app-shell.tsx create mode 100644 src/components/bottom-nav.tsx create mode 100644 src/components/brand-mark.tsx create mode 100644 src/components/fab.tsx create mode 100644 src/components/nav-icon.tsx create mode 100644 src/components/nav-link.tsx create mode 100644 src/components/nav-mode-provider.tsx delete mode 100644 src/components/quick-add-fab.tsx create mode 100644 src/components/settings-section.tsx create mode 100644 src/components/share/mini-day-card.tsx create mode 100644 src/components/share/mini-map-card.tsx create mode 100644 src/components/share/share-banner.tsx create mode 100644 src/components/share/share-brand-strip.tsx create mode 100644 src/components/share/share-detail-card.tsx create mode 100644 src/components/share/share-eyebrow.tsx create mode 100644 src/components/share/share-frame.tsx create mode 100644 src/components/sidebar.tsx create mode 100644 src/components/topbar-new-button.tsx create mode 100644 src/components/topbar-search.tsx create mode 100644 src/components/topbar-title.tsx create mode 100644 src/components/topbar.tsx create mode 100644 src/lib/dashboard.server.ts diff --git a/.gitignore b/.gitignore index 1b532ba..a972c33 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ tests/.auth/ # Backups deploy/backups/data/ + +# Design handoff bundle (reference only, not committed) +.design-tmp/ diff --git a/drizzle/0014_paper_ink_theme.sql b/drizzle/0014_paper_ink_theme.sql new file mode 100644 index 0000000..0d828f7 --- /dev/null +++ b/drizzle/0014_paper_ink_theme.sql @@ -0,0 +1,7 @@ +ALTER TABLE "users" ADD COLUMN "theme_palette" text NOT NULL DEFAULT 'clay';--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "theme_font_pair" text NOT NULL DEFAULT 'serif-sans';--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "theme_density" text NOT NULL DEFAULT 'regular';--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "theme_dash_layout" text NOT NULL DEFAULT 'classic';--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "theme_cal_view" text NOT NULL DEFAULT 'month';--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "theme_nav_style" text NOT NULL DEFAULT 'rail-desktop';--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN IF EXISTS "theme"; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 9057e49..05d9d2f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -99,6 +99,13 @@ "when": 1778400000000, "tag": "0013_push_notify_reminders", "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1778600000000, + "tag": "0014_paper_ink_theme", + "breakpoints": true } ] } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index ae2d119..d24b82e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,15 @@ import nextConfig from "eslint-config-next/core-web-vitals"; export default tseslint.config( { - ignores: ["node_modules/**", ".next/**", ".claude/**", "dist/**", "drizzle/**", "public/sw.js"], + ignores: [ + "node_modules/**", + ".next/**", + ".claude/**", + ".design-tmp/**", + "dist/**", + "drizzle/**", + "public/sw.js", + ], }, js.configs.recommended, ...tseslint.configs.recommended, diff --git a/src/app/calendar/page.tsx b/src/app/calendar/page.tsx index 55c3df6..bb5582d 100644 --- a/src/app/calendar/page.tsx +++ b/src/app/calendar/page.tsx @@ -1,5 +1,7 @@ import { CalendarShell } from "@/modules/calendar/components/calendar-shell"; import { listCalendars, listEvents } from "@/modules/calendar/server/queries"; +import { getCurrentSession } from "@/lib/session"; +import type { CalView } from "@/modules/_core/themes"; export default async function CalendarPage() { const now = new Date(); @@ -8,10 +10,17 @@ export default async function CalendarPage() { const to = new Date(now); to.setMonth(to.getMonth() + 10); - const [calendars, events] = await Promise.all([ + const [{ user }, calendars, events] = await Promise.all([ + getCurrentSession(), listCalendars(), listEvents({ from, to, calendarIds: "all" }), ]); - return ; + return ( + + ); } diff --git a/src/app/d/[slug]/page.tsx b/src/app/d/[slug]/page.tsx index 2fc8142..86b27d4 100644 --- a/src/app/d/[slug]/page.tsx +++ b/src/app/d/[slug]/page.tsx @@ -1,13 +1,20 @@ import { notFound } from "next/navigation"; import { Suspense } from "react"; import { getCurrentSession } from "@/lib/session"; -import { parseDashboardLayout, computeDefaultLayout } from "@/lib/dashboard"; +import { parseDashboardLayout } from "@/lib/dashboard"; +import { computeDefaultLayout } from "@/lib/dashboard.server"; import { getWidget, getWidgetMetas } from "@/modules/_core"; import { getDashboardBySlug } from "@/app/d/actions"; import { DashboardEditor } from "@/components/dashboard-editor"; -import { QuickAddFab } from "@/components/quick-add-fab"; import { EditDashboardButton } from "@/components/edit-dashboard-button"; +import { DashboardSwitcher } from "@/components/dashboard-switcher"; +import { DashboardTab } from "@/components/dashboard-tab"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; +import { dashboards } from "@/modules/_core/schema"; +import { asc, eq } from "drizzle-orm"; +import type { DashLayout } from "@/modules/_core/themes"; const smColSpan: Record = { 1: "sm:col-span-1", @@ -24,6 +31,9 @@ const smColSpan: Record = { 12: "sm:col-span-12", }; +const greetingForHour = (h: number) => + h < 5 ? "Up early" : h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening"; + export default async function DashboardPage({ params, searchParams, @@ -52,17 +62,55 @@ export default async function DashboardPage({ ); } + // For dashboard tabs sub-header, pull the user's dashboards (small list). + const session = await auth(); + const userDashboards = session?.user?.id + ? await db + .select({ + id: dashboards.id, + name: dashboards.name, + slug: dashboards.slug, + isDefault: dashboards.isDefault, + position: dashboards.position, + }) + .from(dashboards) + .where(eq(dashboards.userId, session.user.id)) + .orderBy(asc(dashboards.position), asc(dashboards.createdAt)) + : []; + const ctx = { userId: user.id, householdId: household.id }; const placements = [...layout.widgets].sort((a, b) => a.y - b.y || a.x - b.x); + const dashLayout = (user.themeDashLayout as DashLayout) ?? "classic"; + const containerCls = + dashLayout === "glance" ? "max-w-[760px] mx-auto" : dashLayout === "split" ? "" : ""; + const greeting = greetingForHour(new Date().getHours()); + const today = new Date().toLocaleDateString(undefined, { + weekday: "long", + month: "long", + day: "numeric", + }); + const firstName = (user.name ?? "").split(" ")[0] ?? ""; return ( -
-
-

{dashboard.name}

-
- - +
+ {userDashboards.length > 1 && ( +
+ {userDashboards.map((d) => ( + + ))} +
+ )} + +
+
+

+ {greeting} + {firstName && `, ${firstName}`}. +

+

{today}

+
+
@@ -73,8 +121,8 @@ export default async function DashboardPage({ return (
- - {widget.title} + + {widget.title} ,