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
@@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ShareButton } from "@/components/share-button";
import type { CalView } from "@/modules/_core/themes";
import {
Select,
SelectContent,
@@ -43,14 +44,38 @@ type EventDraft = {
remindMinutesBefore: number | null;
};
const DEFAULT_COLOR = "#2563eb";
const DEFAULT_COLOR = "#B85C3C";
const VIEW_MAP: Record<CalView, string> = {
month: "dayGridMonth",
week: "timeGridWeek",
day: "timeGridDay",
};
function withAlpha(hex: string, alpha: number): string {
// Accept #RGB / #RRGGBB / non-hex (return as-is for non-hex e.g. var(--))
if (!hex.startsWith("#")) return hex;
let h = hex.slice(1);
if (h.length === 3)
h = h
.split("")
.map((c) => c + c)
.join("");
if (h.length !== 6) return hex;
const a = Math.round(Math.min(1, Math.max(0, alpha)) * 255)
.toString(16)
.padStart(2, "0");
return `#${h}${a}`;
}
export function CalendarShell({
calendars,
events,
defaultView = "month",
}: {
calendars: CalendarDto[];
events: CalendarEventDto[];
defaultView?: CalView;
}) {
const [calendarRows, setCalendarRows] = useState(calendars);
const [eventRows, setEventRows] = useState(events);
@@ -380,15 +405,22 @@ export function CalendarShell({
</div>
</aside>
<section className="min-w-0 p-4">
<section className="fc-skin min-w-0 p-4">
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
initialView={VIEW_MAP[defaultView]}
headerToolbar={{
left: "prev,next today",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
buttonText={{
today: "Today",
month: "Month",
week: "Week",
day: "Day",
}}
dayHeaderFormat={{ weekday: "short" }}
selectable
editable
eventResizableFromStart
@@ -396,19 +428,26 @@ export function CalendarShell({
eventClick={openExistingEvent}
eventDrop={moveEvent}
eventResize={moveEvent}
events={visibleEvents.map((event) => ({
id: event.id,
title: event.title,
start: event.startAt,
end: event.endAt,
allDay: event.allDay,
backgroundColor:
events={visibleEvents.map((event) => {
const color =
calendarRows.find((calendar) => calendar.id === event.calendarId)?.color ??
DEFAULT_COLOR,
borderColor:
calendarRows.find((calendar) => calendar.id === event.calendarId)?.color ??
DEFAULT_COLOR,
}))}
DEFAULT_COLOR;
return {
id: event.id,
title: event.title,
start: event.startAt,
end: event.endAt,
allDay: event.allDay,
backgroundColor: withAlpha(color, 0.14),
borderColor: color,
textColor: "var(--ink-2)",
extendedProps: { calendarId: event.calendarId, color },
};
})}
eventClassNames={(arg) => {
const id = String(arg.event.extendedProps["calendarId"] ?? "");
return id ? [`fc-cal-${id.slice(0, 8)}`] : [];
}}
height="auto"
/>
</section>