diff --git a/src/app/globals.css b/src/app/globals.css index 192d890..cdd3e96 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -32,6 +32,43 @@ html { animation: fadeInUp 0.5s ease-out forwards; } +/* ─── Loading placeholders ─── */ + +/* A light band sweeping left→right, in the spirit of the reveal animations + below — quieter than a pulsing opacity blink, and it reads as "typesetting + in progress" rather than "widget buffering". */ +@keyframes skeletonSweep { + from { background-position: 150% 0; } + to { background-position: -150% 0; } +} + +/* Placeholder for a line of body text — light grey from the charcoal ramp, + one step lighter than --color-border-light so placeholders stay quieter + than the rules and frames around them. */ +.skeleton-bar { + background-color: var(--color-charcoal-200); + background-image: linear-gradient( + 90deg, + transparent 20%, + var(--color-charcoal-100) 50%, + transparent 80% + ); + background-size: 200% 100%; + background-repeat: no-repeat; + animation: skeletonSweep 2s ease-in-out infinite; +} + +/* Heavier weight, for headings and other display type. */ +.skeleton-bar-strong { + background-color: var(--color-charcoal-300); +} + +/* Staggers the sweep down a stack so it travels through the block instead of + every line flashing in unison. */ +.skeleton-delay-1 { animation-delay: 0.15s; } +.skeleton-delay-2 { animation-delay: 0.3s; } +.skeleton-delay-3 { animation-delay: 0.45s; } + /* Draw stroke forward (dashoffset 1 → 0) */ @keyframes drawIn { from { stroke-dashoffset: 1; } diff --git a/src/app/memos/[slug]/MemoSkeleton.tsx b/src/app/memos/[slug]/MemoSkeleton.tsx new file mode 100644 index 0000000..48dfc5d --- /dev/null +++ b/src/app/memos/[slug]/MemoSkeleton.tsx @@ -0,0 +1,153 @@ +// The placeholder a memo route shows while its content streams in. +// +// It mirrors the real layout in page.tsx (hero → signpost → key messages → +// body) at the same widths and spacing, so swapping in the real memo doesn't +// shift anything on screen. +// +// Chrome that's identical on every memo — the Key Messages frame and its +// eyebrow, the numbered indices, the Signpost's rail and Share block — is +// rendered for real rather than faked as grey blocks. Only the parts that +// differ per memo are placeholders, which keeps the page recognisably a memo +// while it loads instead of a generic loading card. + +const KEY_MESSAGES_FILL = { + // The fills the real Key Messages boxes use (see page.tsx). Hardcoded there + // too — they predate the linen ramp. + default: "bg-[#f0e5dc]", + toronto: "bg-[#d7e4f3]", +} as const; + +const DELAYS = ["", "skeleton-delay-1", "skeleton-delay-2", "skeleton-delay-3"]; + +// One placeholder line. `i` staggers the sweep down a stack. +function Line({ + className, + strong = false, + i = 0, +}: { + className: string; + strong?: boolean; + i?: number; +}) { + return ( +
+ ); +} + +export function MemoSkeleton({ + brand = "default", + showBackLink = false, +}: { + brand?: keyof typeof KEY_MESSAGES_FILL; + showBackLink?: boolean; +}) { + return ( +
+ Loading memo… + +