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
+24
-20
@@ -1,14 +1,17 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { resolveShareToken } from "@/modules/_core/share";
|
||||
import { getEntityType } from "@/modules/_core/registry";
|
||||
import { isRateLimited, recordFailure } from "@/lib/rate-limit";
|
||||
import { db } from "@/lib/db";
|
||||
import { users } from "@/modules/_core/schema";
|
||||
import { ShareFrame } from "@/components/share/share-frame";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
// Rate-limit prefix length — must match the value used in middleware.
|
||||
const RL_PREFIX_LEN = 8;
|
||||
|
||||
export default async function SharePage({ params }: { params: Promise<{ token: string }> }) {
|
||||
@@ -20,19 +23,12 @@ export default async function SharePage({ params }: { params: Promise<{ token: s
|
||||
"0.0.0.0";
|
||||
const rlKey = `${ip}:${token.slice(0, RL_PREFIX_LEN)}`;
|
||||
|
||||
// Secondary rate-limit check in the Node.js runtime (failure-only bucket).
|
||||
// The primary 429 enforcement lives in src/middleware.ts which counts all
|
||||
// requests in the Edge runtime. This page tracks only failed token lookups,
|
||||
// providing accurate per-failure accounting. The two buckets are independent
|
||||
// (separate module instances across runtimes); a shared Redis store would
|
||||
// unify them for multi-replica deployments.
|
||||
if (isRateLimited(rlKey)) {
|
||||
return <ShareRateLimitError />;
|
||||
}
|
||||
|
||||
const resolved = await resolveShareToken(token);
|
||||
if (!resolved) {
|
||||
// Only failed lookups increment the failure bucket.
|
||||
recordFailure(rlKey);
|
||||
return <ShareError />;
|
||||
}
|
||||
@@ -48,24 +44,32 @@ export default async function SharePage({ params }: { params: Promise<{ token: s
|
||||
return <ShareError />;
|
||||
}
|
||||
|
||||
// Best-effort lookup of the creator's display name. Doesn't reveal email.
|
||||
const [creator] = resolved.createdBy
|
||||
? await db
|
||||
.select({ name: users.name })
|
||||
.from(users)
|
||||
.where(eq(users.id, resolved.createdBy))
|
||||
.limit(1)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="border-b bg-background px-4 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Shared via famapp
|
||||
{resolved.capabilities.write ? " · You can edit this" : " · View only"}
|
||||
</p>
|
||||
</div>
|
||||
<ShareFrame
|
||||
expiresAt={resolved.expiresAt}
|
||||
capabilities={resolved.capabilities}
|
||||
token={token}
|
||||
sharedByName={creator?.name ?? null}
|
||||
>
|
||||
{entityReg.renderSharedView({ data, capabilities: resolved.capabilities, token })}
|
||||
</div>
|
||||
</ShareFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function ShareRateLimitError() {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<h1 className="text-xl font-semibold">Too many requests</h1>
|
||||
<p className="max-w-sm text-sm text-muted-foreground">
|
||||
<h1 className="serif text-[28px] font-medium tracking-tight">Too many requests</h1>
|
||||
<p className="max-w-sm text-[13.5px] muted">
|
||||
You have made too many requests in a short period. Please wait a minute and try again.
|
||||
</p>
|
||||
</div>
|
||||
@@ -75,8 +79,8 @@ function ShareRateLimitError() {
|
||||
function ShareError({ message }: { message?: string }) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<h1 className="text-xl font-semibold">Link not found</h1>
|
||||
<p className="max-w-sm text-sm text-muted-foreground">
|
||||
<h1 className="serif text-[28px] font-medium tracking-tight">Link not found</h1>
|
||||
<p className="max-w-sm text-[13.5px] muted">
|
||||
{message ??
|
||||
"This share link may have expired or been revoked. Ask the sender for a new link."}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user