DS1 (design): speed + trust bugs
Fixes the three DESIGN-SPEC Part 4 + #17 audit findings. 1. React #418 hydration mismatch (landing → dashboard entry). The `maybeSignedIn` value was computed in a useState INITIALIZER that reads localStorage during render: server (no window) → false → emits the marketing tree; a signed-in visitor's first CLIENT render → true → emits the loading placeholder. Whole-subtree server/client mismatch → React discarded and re-rendered the page. Deferred behind a mounted flag so the first client render matches the server; the stored-session check flips post-mount. SSR HTML is no longer discarded. 2. Loading walls → skeletons. New tokenized Skeleton primitive (.vyndr-skeleton, reduced-motion-safe via the global rule). Swapped into every text-wall loader: dashboard slate load ("Loading the slate…"), /desk ("Assembling the pack…"), /ledger ("Loading…"), scan ("Loading the model…"), and the landing redirect placeholder. No bare text loader remains. 3. scan→ledger persistence. Root cause: the scan page read its bearer token from localStorage['sb-token'] — a key written ONLY by the OAuth callback — so email/password users posted /api/scan anonymously and the ledger write (gated on an authed user) was silently skipped. Now uses the authoritative session.access_token (matching the ledger read path). Extracted the row builder to web/src/lib/ledgerRow.js (shared, testable). Tests: +17 (scanLedgerPersistence write→mine round-trip + scope + idempotency; ds1SpeedTrust hydration/skeleton/persistence source invariants). Full suite 233 suites / 2793 green; web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* DS1 (DESIGN-SPEC §4 — "kill loading walls") — the single skeleton primitive.
|
||||
*
|
||||
* Governing law: premium = SPEED + RESTRAINT. A text-wall loader ("Loading…")
|
||||
* reads cheap; a layout-matched skeleton reads instant. This is a tokenized
|
||||
* dark shimmer block (`.vyndr-skeleton` in globals.css) — the sweep is chrome,
|
||||
* never data, and the global prefers-reduced-motion rule freezes it to a
|
||||
* still, visible surface.
|
||||
*/
|
||||
export function Skeleton({
|
||||
height = 16,
|
||||
width = '100%',
|
||||
radius = 8,
|
||||
style,
|
||||
className = '',
|
||||
}: {
|
||||
height?: number | string;
|
||||
width?: number | string;
|
||||
radius?: number | string;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={`vyndr-skeleton ${className}`.trim()}
|
||||
style={{ height, width, borderRadius: radius, ...style }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A row of card-shaped skeletons — matches horizontal-scroll grade/slate rows.
|
||||
*/
|
||||
export function SkeletonCards({ count = 4, height = 110, minWidth = 200 }: { count?: number; height?: number; minWidth?: number }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 12 }} aria-hidden="true">
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<Skeleton key={i} height={height} width={minWidth} radius={16} style={{ minWidth, flex: '0 0 auto' }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A stack of full-width row skeletons — matches list/grid loading states
|
||||
* (the slate, the ledger, the desk pack).
|
||||
*/
|
||||
export function SkeletonList({ count = 4, height = 80, gap = 12 }: { count?: number; height?: number; gap?: number }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gap }} aria-hidden="true">
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<Skeleton key={i} height={height} radius={16} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Skeleton;
|
||||
@@ -34,6 +34,9 @@ export { default as BookWordmark } from './BookWordmark';
|
||||
export { default as SearchModal } from './SearchModal';
|
||||
export { DotStrip, LineSparkline } from './StatStrip';
|
||||
|
||||
// DS1 (§4) — skeleton loaders replace text-wall "Loading…" states everywhere.
|
||||
export { default as Skeleton, SkeletonCards, SkeletonList } from './Skeleton';
|
||||
|
||||
export {
|
||||
GRADE_COLORS,
|
||||
GRADE_HEX,
|
||||
|
||||
Reference in New Issue
Block a user