From 8f7733049e7d8c11c917549282ed530e5ea1e25e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 00:25:58 +0000 Subject: [PATCH 1/3] perf(ui): content-first detail pages + loading/animation quick wins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Site-audit quick wins across perceived speed, loading, and motion. No behaviour change to RAG/retrieval or clinical content. Content-first detail pages (was: spinner/skeleton-first): - Medications, Services and Forms detail pages now render the public fixture record server-side as a fallback and swap in the owner-aware live record in place, reusing the differentials `fallbackRecord` pattern. SSR and the initial client render both start from the fallback, so there is no hydration mismatch. - Drop `cache: "no-store"` from the medication hook so public responses honour the API's CDN/browser cache headers (matches the sibling registry/differential hooks). Loading / assets: - `Geist_Mono` `preload: false` — the mono face is never in LCP text, so stop preloading it on every route (still loads on-demand via swap). - next.config `images`: enable AVIF then WebP; allow Supabase Storage signed-URL remotePatterns for future next/image use. Motion smoothness (animate the compositor, not layout): - Therapy-compass knob: animate `transform` instead of `left`. - Upload progress bar: `transform: scaleX()` instead of `width`. - Settings header: drop the permanent `will-change: transform`. - Global `:active` press: scope the 1px nudge to buttons/summary, not every `` (it was nudging inline citation/body links). Verified: typecheck, lint, design-system contract, type-scale, icon-scale and the full offline unit suite pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UbhUVWVJRwDibC2YtJ6aRX --- next.config.ts | 15 ++++++++++++ src/app/forms/[slug]/page.tsx | 6 ++++- src/app/globals.css | 3 ++- src/app/layout.tsx | 5 ++++ src/app/medications/[slug]/page.tsx | 16 ++++++++++++- src/app/services/[slug]/page.tsx | 6 ++++- .../DocumentManagerPanel.tsx | 4 ++-- .../medication-record-page.tsx | 24 +++++++++++++++---- .../clinical-dashboard/settings-dialog.tsx | 5 +++- .../use-medication-catalog.ts | 7 +++++- src/components/forms/form-detail-client.tsx | 5 ++-- src/components/registry-record-loader.tsx | 10 ++++++++ .../services/service-detail-client.tsx | 5 ++-- .../therapy-compass/therapy-compass.css | 9 ++++--- 14 files changed, 100 insertions(+), 20 deletions(-) diff --git a/next.config.ts b/next.config.ts index 7ad9f31bd..a1dfcbccb 100644 --- a/next.config.ts +++ b/next.config.ts @@ -47,6 +47,21 @@ const nextConfig: NextConfig = { proxyClientMaxBodySize: "151mb", }, poweredByHeader: false, + images: { + // Prefer AVIF (~20-30% smaller than WebP), falling back to WebP, for any + // next/image output. + formats: ["image/avif", "image/webp"], + // Permit optimizing Supabase Storage signed URLs (private document/image + // previews) through next/image. Signed URLs are served from the project's + // *.supabase.co storage object endpoint; the path scope keeps this narrow. + remotePatterns: [ + { + protocol: "https", + hostname: "*.supabase.co", + pathname: "/storage/v1/object/**", + }, + ], + }, turbopack: { root: projectRoot, }, diff --git a/src/app/forms/[slug]/page.tsx b/src/app/forms/[slug]/page.tsx index 3e8ddd973..8aef1678c 100644 --- a/src/app/forms/[slug]/page.tsx +++ b/src/app/forms/[slug]/page.tsx @@ -19,5 +19,9 @@ export async function generateMetadata({ params }: FormRouteProps): Promise; + // Hand the public fixture record to the client so the detail view paints real + // content on first load instead of a centered spinner; the owner-aware record + // refreshes in place. Absent from the fixtures (owner-only slug) → null. + const fallbackRecord = getFormRecord(slug); + return ; } diff --git a/src/app/globals.css b/src/app/globals.css index 241f1bf60..36c28d12b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -525,8 +525,9 @@ summary { transition-timing-function: var(--ease-out-soft); } +/* Tactile press only on button-like controls. Applying it to every `a` nudged + inline body/citation links down 1px on tap, which reads oddly for text links. */ button:active, -a:active, summary:active { transform: translateY(1px); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7d9a185a0..809791d96 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,6 +17,11 @@ const geistSans = Geist({ const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], + // The mono face is only used deep in the UI (tabular figures, `kbd`, code) and + // never in initial/LCP text, so don't preload it on every route — it competes + // for the critical-path connection. It still loads on-demand via `swap` when + // first painted. The sans face keeps the default preload. + preload: false, }); const baseMetadata: Metadata = { diff --git a/src/app/medications/[slug]/page.tsx b/src/app/medications/[slug]/page.tsx index 4ee491c9a..4670aba99 100644 --- a/src/app/medications/[slug]/page.tsx +++ b/src/app/medications/[slug]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { MedicationRecordPage } from "@/components/clinical-dashboard/medication-record-page"; +import { deriveGovernanceFromSections } from "@/lib/medication-records"; import { getMedicationRecord, loadMedicationSnapshot } from "@/lib/medication-snapshot"; type MedicationPageProps = { @@ -31,5 +32,18 @@ export async function generateMetadata({ params }: MedicationPageProps): Promise export default async function MedicationPage({ params }: MedicationPageProps) { const { slug } = await params; - return ; + // The public snapshot record is already in hand server-side. Pass it (plus the + // governance derived exactly as the public API does) so the page paints real + // clinical content on first load instead of a skeleton, while the client hook + // refreshes in place for authenticated owner records. Slugs absent from the + // snapshot (owner-only medications) pass `undefined` and keep the loading path. + const record = getMedicationRecord(slug); + const fallbackGovernance = record + ? (() => { + const derived = deriveGovernanceFromSections(record); + return { sourceStatus: derived.source_status, validationStatus: derived.validation_status }; + })() + : undefined; + + return ; } diff --git a/src/app/services/[slug]/page.tsx b/src/app/services/[slug]/page.tsx index 4c1986d41..0b17109fe 100644 --- a/src/app/services/[slug]/page.tsx +++ b/src/app/services/[slug]/page.tsx @@ -19,5 +19,9 @@ export async function generateMetadata({ params }: ServiceRouteProps): Promise; + // Hand the public fixture record to the client so the detail view paints real + // content on first load instead of a centered spinner; the owner-aware record + // refreshes in place. Absent from the fixtures (owner-only slug) → null. + const fallbackRecord = getServiceRecord(slug); + return ; } diff --git a/src/components/clinical-dashboard/DocumentManagerPanel.tsx b/src/components/clinical-dashboard/DocumentManagerPanel.tsx index 030e5e9aa..7927cb14d 100644 --- a/src/components/clinical-dashboard/DocumentManagerPanel.tsx +++ b/src/components/clinical-dashboard/DocumentManagerPanel.tsx @@ -429,8 +429,8 @@ export function UploadPanel({ className="h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--surface-inset)]" >
diff --git a/src/components/clinical-dashboard/medication-record-page.tsx b/src/components/clinical-dashboard/medication-record-page.tsx index 1b4f02e85..8db0de950 100644 --- a/src/components/clinical-dashboard/medication-record-page.tsx +++ b/src/components/clinical-dashboard/medication-record-page.tsx @@ -475,8 +475,22 @@ function MedicationRecordDetail({ ); } -export function MedicationRecordPage({ slug }: { slug: string }) { +export function MedicationRecordPage({ + slug, + fallbackRecord, + fallbackGovernance, +}: { + slug: string; + fallbackRecord?: MedicationRecord; + fallbackGovernance?: MedicationGovernance; +}) { const { data, loading, error } = useMedicationDetail(slug); + // Content-first: render the SSR fallback immediately, then swap in the live + // (owner-aware) record once the hook resolves. Only fall back to the skeleton + // when there is no server record to show (owner-only slugs) and the fetch is + // still in flight; the error state applies only when nothing renderable exists. + const record = data?.record ?? fallbackRecord ?? null; + const governance = data?.governance ?? fallbackGovernance; return (
@@ -491,19 +505,19 @@ export function MedicationRecordPage({ slug }: { slug: string }) {
- {loading ? ( + {record ? ( + + ) : loading ? (
- ) : error || !data?.record ? ( + ) : (
- ) : ( - )}