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>
168 lines
4.9 KiB
TypeScript
168 lines
4.9 KiB
TypeScript
import { Calendar as CalendarIcon, MapPin } from "lucide-react";
|
||
import type { CalendarShareData, EventShareData } from "../server/share-queries";
|
||
import { ShareEyebrow } from "@/components/share/share-eyebrow";
|
||
import { MiniDayCard } from "@/components/share/mini-day-card";
|
||
import { MiniMapCard } from "@/components/share/mini-map-card";
|
||
import { ShareDetailCard, ShareRow } from "@/components/share/share-detail-card";
|
||
|
||
function formatTime(d: Date): string {
|
||
return d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
||
}
|
||
|
||
function formatTimeRange(startAt: string, endAt: string, allDay: boolean): string {
|
||
const start = new Date(startAt);
|
||
const end = new Date(endAt);
|
||
if (allDay) return "All day";
|
||
return `${formatTime(start)} – ${formatTime(end)}`;
|
||
}
|
||
|
||
function formatFullDate(d: Date): string {
|
||
return d.toLocaleDateString(undefined, {
|
||
weekday: "long",
|
||
month: "long",
|
||
day: "numeric",
|
||
year: "numeric",
|
||
});
|
||
}
|
||
|
||
export function EventSharedView({ data }: { data: EventShareData }) {
|
||
const start = new Date(data.startAt);
|
||
const end = new Date(data.endAt);
|
||
const time = data.allDay ? "All day" : `${formatTime(start)} – ${formatTime(end)}`;
|
||
|
||
return (
|
||
<>
|
||
<ShareEyebrow>
|
||
<CalendarIcon className="size-3" />
|
||
Event
|
||
</ShareEyebrow>
|
||
<h1
|
||
className="serif"
|
||
style={{
|
||
fontSize: "clamp(28px, 6vw, 38px)",
|
||
lineHeight: 1.15,
|
||
letterSpacing: "-0.02em",
|
||
color: "var(--ink)",
|
||
margin: "0 0 8px",
|
||
textWrap: "pretty",
|
||
}}
|
||
>
|
||
{data.title}
|
||
</h1>
|
||
{data.calendarName && (
|
||
<p
|
||
className="serif"
|
||
style={{
|
||
fontSize: 17,
|
||
color: "var(--ink-soft)",
|
||
margin: "0 0 28px",
|
||
}}
|
||
>
|
||
On the {data.calendarName} calendar.
|
||
</p>
|
||
)}
|
||
|
||
<div
|
||
className="grid gap-3.5 mb-6"
|
||
style={{ gridTemplateColumns: data.location ? "1fr 1fr" : "1fr" }}
|
||
>
|
||
<MiniDayCard date={start} time={time} />
|
||
{data.location && <MiniMapCard name={data.location} />}
|
||
</div>
|
||
|
||
<ShareDetailCard>
|
||
<ShareRow label="When">
|
||
{formatFullDate(start)}
|
||
{!data.allDay && (
|
||
<>
|
||
{" · "}
|
||
{formatTimeRange(data.startAt, data.endAt, data.allDay)}
|
||
</>
|
||
)}
|
||
</ShareRow>
|
||
{data.location && (
|
||
<ShareRow label="Where">
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<MapPin className="size-3.5 text-[var(--ink-mute)]" />
|
||
{data.location}
|
||
</span>
|
||
</ShareRow>
|
||
)}
|
||
<ShareRow label="Calendar">{data.calendarName}</ShareRow>
|
||
</ShareDetailCard>
|
||
|
||
{data.notes && (
|
||
<p
|
||
className="serif"
|
||
style={{
|
||
fontSize: 16,
|
||
lineHeight: 1.65,
|
||
color: "var(--ink-2)",
|
||
background: "var(--paper-2)",
|
||
borderRadius: 8,
|
||
padding: "18px 22px",
|
||
margin: "14px 0 24px",
|
||
textWrap: "pretty",
|
||
whiteSpace: "pre-wrap",
|
||
}}
|
||
>
|
||
{data.notes}
|
||
</p>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
export function CalendarSharedView({ data }: { data: CalendarShareData }) {
|
||
return (
|
||
<>
|
||
<ShareEyebrow>
|
||
<CalendarIcon className="size-3" />
|
||
Calendar
|
||
</ShareEyebrow>
|
||
<h1
|
||
className="serif"
|
||
style={{
|
||
fontSize: "clamp(28px, 6vw, 38px)",
|
||
lineHeight: 1.15,
|
||
letterSpacing: "-0.02em",
|
||
color: "var(--ink)",
|
||
margin: "0 0 8px",
|
||
textWrap: "pretty",
|
||
}}
|
||
>
|
||
{data.name}
|
||
</h1>
|
||
<p className="serif" style={{ fontSize: 17, color: "var(--ink-soft)", margin: "0 0 28px" }}>
|
||
Upcoming events — next 90 days.
|
||
</p>
|
||
|
||
<ShareDetailCard>
|
||
{data.events.length === 0 ? (
|
||
<p className="muted text-[13.5px] py-2">No upcoming events.</p>
|
||
) : (
|
||
data.events.map((event) => {
|
||
const start = new Date(event.startAt);
|
||
return (
|
||
<div key={event.id} className="list-row" style={{ padding: "10px 0" }}>
|
||
<span className="dot" style={{ background: data.color ?? "var(--c-household)" }} />
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-[14px] font-medium text-[var(--ink-2)]">{event.title}</div>
|
||
<div className="muted tnum text-[12px] mt-0.5">
|
||
{start.toLocaleDateString(undefined, {
|
||
month: "short",
|
||
day: "numeric",
|
||
})}
|
||
{!event.allDay && ` · ${formatTime(start)}`}
|
||
{event.location && ` · ${event.location}`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
</ShareDetailCard>
|
||
</>
|
||
);
|
||
}
|