Blocks
Building blocks for health apps
Whole screens made from PREDICT components: dashboards, reports, onboarding, booking, and sign in. Preview them at any width, then copy the code or add them with the shadcn CLI.
- app
- dashboard
- components
app/dashboard/page.tsx
"use client"
import { CalendarIcon, DownloadIcon } from "lucide-react"
import { Button } from "@workspace/ui/components/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card"
import { Separator } from "@workspace/ui/components/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@workspace/ui/components/sidebar"
import { AgeBar } from "@workspace/ui/predict/age-bar"
import { BioClock } from "@workspace/ui/predict/bio-clock"
import { AppSidebar } from "./components/app-sidebar"
import { BiomarkerGrid } from "./components/biomarker-grid"
export default function Page() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger />
<h1 className="text-sm font-medium">Overview</h1>
<div className="ml-auto flex items-center gap-2">
<Button variant="outline" size="sm" className="hidden sm:inline-flex">
<CalendarIcon data-icon="inline-start" />
12 Oct 2026
</Button>
<Button size="sm">
<DownloadIcon data-icon="inline-start" />
Report
</Button>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 md:pt-0 md:p-6">
<div className="flex flex-col gap-4">
<AgeBar biologicalAge={45} chronologicalAge={38} className="pb-4" />
<div className="grid grid-cols-3 gap-4">
{[
{ label: "Tested", value: "40" },
{ label: "Out of range", value: "9" },
{ label: "Critical", value: "1" },
].map((stat) => (
<Card key={stat.label} size="sm">
<CardContent className="flex flex-col gap-1">
<span className="text-muted-foreground">{stat.label}</span>
<span className="text-2xl font-medium tabular-nums">{stat.value}</span>
</CardContent>
</Card>
))}
</div>
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-5">
<Card className="min-w-0 lg:col-span-3">
<CardHeader>
<CardTitle className="text-sm">Bio clock</CardTitle>
<CardDescription>
Biological age, severity, and the domain that stands out.
</CardDescription>
</CardHeader>
<CardContent>
<BioClock
bioAge={45}
yearsValue="+7 Years"
currentValue={2.6}
className="mx-auto w-full"
size={"default"}
/>
<div className="grid grid-cols-1 gap-4 border-t p-4 sm:grid-cols-3">
<div className="flex gap-2">
<div className="w-2 shrink-0 self-stretch rounded-full bg-foreground" />
<div className="space-y-1">
<p className="text-sm font-semibold">Hour Hand</p>
<p className="text-muted-foreground text-xs">
How sick or unwell you are
</p>
</div>
</div>
<div className="flex gap-2">
<div className="w-0.5 shrink-0 self-stretch rounded-full bg-foreground" />
<div className="space-y-1">
<p className="text-sm font-semibold">Minute Hand</p>
<p className="text-muted-foreground text-xs">
How fast your body is aging
</p>
</div>
</div>
<div className="flex gap-2">
<div className="shrink-0 self-stretch border-l-2 border-dashed border-foreground" /> <div className="space-y-1">
<p className="text-sm font-semibold">Second Hand</p>
<p className="text-muted-foreground text-xs">
What's causing the problem
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="min-w-0 lg:col-span-2">
<BiomarkerGrid />
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
)
}
- app
- report
app/report/page.tsx
"use client"
import { DownloadIcon, ShareIcon } from "lucide-react"
import { Badge } from "@workspace/ui/components/badge"
import { Button } from "@workspace/ui/components/button"
import { Separator } from "@workspace/ui/components/separator"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@workspace/ui/components/tabs"
import {
biomarkerCategoryLabels,
biomarkerRange,
getBiomarker,
type BiomarkerCategory,
} from "@workspace/ui/lib/biomarkers"
import { flagValue, isConcern } from "@workspace/ui/lib/reference-range"
import { ReferenceRange } from "@workspace/ui/tools/reference-range"
// Example results, grouped the way the report reads.
const panel: Partial<Record<BiomarkerCategory, { id: string; value: number }[]>> = {
inflammation: [
{ id: "hs-crp", value: 3.5 },
{ id: "nlr", value: 3.2 },
{ id: "il-6", value: 9.8 },
{ id: "tnf-alpha", value: 4.1 },
],
lipids: [
{ id: "total-cholesterol", value: 225.6 },
{ id: "ldl-c", value: 185.5 },
{ id: "hdl-c", value: 35.7 },
{ id: "triglycerides", value: 120 },
],
metabolic: [
{ id: "fbs", value: 94 },
{ id: "fsi", value: 11 },
{ id: "hba1c", value: 7.6 },
],
kidney: [
{ id: "egfr", value: 82.7 },
{ id: "creatinine", value: 1.8 },
{ id: "bun", value: 21.8 },
{ id: "albumin", value: 4.3 },
],
}
const groups = Object.keys(panel) as BiomarkerCategory[]
const all = groups.flatMap((group) => panel[group]!)
// Only count results on the side the direction marks as bad.
const outOfRange = all.filter(({ id, value }) => {
const range = biomarkerRange(getBiomarker(id)!)
return isConcern(flagValue(value, range), range.direction)
}).length
export default function Page() {
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 p-4 md:p-8">
<header className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<h1 className="text-xl font-medium tracking-tight">Biomarker report</h1>
<Badge variant="outline">October panel</Badge>
</div>
<p className="text-sm text-muted-foreground">
Priya Sharma · Collected 12 Oct 2026 · Fasting 10 hours
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<ShareIcon data-icon="inline-start" />
Share
</Button>
<Button size="sm">
<DownloadIcon data-icon="inline-start" />
Download PDF
</Button>
</div>
</header>
<dl className="grid grid-cols-3 rounded-lg border">
{[
{ label: "Biomarkers", value: all.length },
{ label: "In range", value: all.length - outOfRange },
{ label: "Needs attention", value: outOfRange },
].map((stat) => (
<div key={stat.label} className="flex flex-col gap-1 p-4 not-first:border-l">
<dt className="text-xs text-muted-foreground">{stat.label}</dt>
<dd className="text-2xl font-medium tabular-nums">{stat.value}</dd>
</div>
))}
</dl>
<Separator />
<Tabs defaultValue={groups[0]}>
<TabsList className="max-w-full overflow-x-auto">
{groups.map((group) => (
<TabsTrigger key={group} value={group}>
{biomarkerCategoryLabels[group]}
</TabsTrigger>
))}
</TabsList>
{groups.map((group) => (
<TabsContent key={group} value={group} className="pt-4">
<div className="grid gap-4 sm:grid-cols-2">
{panel[group]!.map(({ id, value }) => {
const biomarker = getBiomarker(id)!
return (
<ReferenceRange
key={id}
name={biomarker.abbreviation}
description={biomarker.name}
value={value}
unit={biomarker.unit}
range={biomarkerRange(biomarker)}
className="max-w-none"
/>
)
})}
</div>
</TabsContent>
))}
</Tabs>
<p className="text-xs text-muted-foreground">
General information, not medical advice. Discuss your results with a clinician.
</p>
</div>
)
}
- app
- index
app/index/page.tsx
"use client"
import { InfoIcon } from "lucide-react"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@workspace/ui/components/accordion"
import { Badge } from "@workspace/ui/components/badge"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@workspace/ui/components/tooltip"
// The CBMI worked example. Each domain's Mean |Z| comes from its biomarker
// Z-scores, Wd is its share of the total, and αd is its priority.
// Score = αd × Mean |Z| × Wd.
const domains = [
{
name: "Cardiometabolic Risk",
abbr: "CMR",
alpha: 6,
biomarkers: [
{ name: "HbA1c", z: 7 },
{ name: "TC", z: 3.338 },
{ name: "LDL-C", z: 9.024 },
{ name: "HDL-C", z: -2.43 },
],
},
{
name: "Inflammatory Risk",
abbr: "IR",
alpha: 2,
biomarkers: [
{ name: "hs-CRP", z: 2.714 },
{ name: "NLR", z: 2.4 },
{ name: "IL-6", z: 3.712 },
],
},
{
name: "Muscle-Cartilage Degeneration Risk",
abbr: "MCDR",
alpha: 2,
biomarkers: [
{ name: "CK-MM", z: 2.667 },
{ name: "Aldolase A", z: 2.627 },
{ name: "IGF-1", z: -2.107 },
{ name: "Myostatin", z: 4.52 },
{ name: "CTX-II", z: 2.736 },
],
},
{
name: "Bone Turnover Risk",
abbr: "BTR",
alpha: 1,
biomarkers: [
{ name: "PTH", z: 2.824 },
{ name: "Ca/P ratio", z: 2.5 },
{ name: "Vitamin D", z: -2.263 },
{ name: "Osteocalcin", z: -2.263 },
{ name: "PINP", z: -2.687 },
],
},
{
name: "Neurovascular Function Risk",
abbr: "NVFR",
alpha: 1,
biomarkers: [
{ name: "BDNF", z: -2.352 },
{ name: "VEGF", z: 3.011 },
{ name: "NOx", z: -2.11 },
],
},
]
const withMeanZ = domains.map((d) => ({
...d,
meanZ: d.biomarkers.reduce((sum, b) => sum + Math.abs(b.z), 0) / d.biomarkers.length,
}))
const totalMeanZ = withMeanZ.reduce((sum, d) => sum + d.meanZ, 0)
const scored = withMeanZ.map((d) => {
const weight = d.meanZ / totalMeanZ
return { ...d, weight, score: d.alpha * d.meanZ * weight }
})
const total = scored.reduce((sum, d) => sum + d.score, 0)
const maxScore = Math.max(...scored.map((d) => d.score))
export default function Page() {
return (
<TooltipProvider>
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 p-4 md:p-8">
<Card>
<CardHeader>
<CardTitle className="text-sm">Composite Biological Musculoskeletal Index</CardTitle>
<CardDescription>Five domains, 21 biomarkers.</CardDescription>
<CardAction>
<Tooltip>
<TooltipTrigger
aria-label="How the score works"
className="text-muted-foreground"
>
<InfoIcon className="size-4" />
</TooltipTrigger>
<TooltipContent className="max-w-60">
Each domain scores αd × Mean |Z| × Wd. The index is their sum.
</TooltipContent>
</Tooltip>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div className="flex items-end gap-3">
<span className="text-5xl font-medium tracking-tight tabular-nums">
{total.toFixed(2)}
</span>
<Badge variant="destructive" className="mb-2">
CBMI
</Badge>
</div>
<ul className="flex flex-col gap-3">
{scored.map((d) => (
<li key={d.abbr} className="grid grid-cols-[3.5rem_1fr_3rem] items-center gap-3">
<span className="text-xs font-medium">{d.abbr}</span>
<div className="h-2 overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary"
style={{ width: `${(d.score / maxScore) * 100}%` }}
/>
</div>
<span className="text-right text-xs tabular-nums">{d.score.toFixed(2)}</span>
</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">Domains</CardTitle>
<CardDescription>Open a domain to see its biomarker Z-scores.</CardDescription>
</CardHeader>
<CardContent>
<Accordion defaultValue={["CMR"]}>
{scored.map((d) => (
<AccordionItem key={d.abbr} value={d.abbr}>
<AccordionTrigger>
<span className="flex flex-1 items-center justify-between gap-4 pr-2">
<span>
{d.name} <span className="text-muted-foreground">({d.abbr})</span>
</span>
<span className="text-muted-foreground tabular-nums">
αd {d.alpha} · Wd {d.weight.toFixed(2)}
</span>
</span>
</AccordionTrigger>
<AccordionContent>
<table className="w-full text-xs">
<thead className="text-muted-foreground">
<tr>
<th className="py-1 text-left font-normal">Biomarker</th>
<th className="py-1 text-right font-normal">Z</th>
<th className="py-1 text-right font-normal">|Z|</th>
</tr>
</thead>
<tbody className="tabular-nums">
{d.biomarkers.map((b) => (
<tr key={b.name} className="border-t">
<td className="py-1.5">{b.name}</td>
<td className="py-1.5 text-right">{b.z.toFixed(2)}</td>
<td className="py-1.5 text-right">{Math.abs(b.z).toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</CardContent>
</Card>
</div>
</TooltipProvider>
)
}
- app
- onboarding
app/onboarding/page.tsx
"use client"
import * as React from "react"
import { ArrowLeftIcon, ArrowRightIcon, CheckIcon } from "lucide-react"
import { Button } from "@workspace/ui/components/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card"
import { Label } from "@workspace/ui/components/label"
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldScrubArea,
} from "@workspace/ui/components/number-field"
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@workspace/ui/components/progress"
import { RadioGroup, RadioGroupItem } from "@workspace/ui/components/radio-group"
import { Switch } from "@workspace/ui/components/switch"
import { PredictMascot } from "@workspace/ui/predict/predict-mascot"
const steps = [
{ title: "Your body", description: "Used for BMI and body surface area." },
{ title: "Your week", description: "How active you are on a normal week." },
{ title: "Your results", description: "Choose who can see your results." },
]
const activity = [
{ value: "low", title: "Mostly sitting", description: "Under 30 minutes of exercise a week." },
{ value: "moderate", title: "Some exercise", description: "Two or three sessions a week." },
{ value: "high", title: "Very active", description: "Four or more sessions a week." },
]
export default function Page() {
const [step, setStep] = React.useState(0)
const [done, setDone] = React.useState(false)
const current = steps[step]!
const isLast = step === steps.length - 1
return (
<div className="flex min-h-svh items-center justify-center p-4">
<Card className="w-full max-w-md">
{done ? (
<CardContent className="flex flex-col items-center gap-4 py-8 text-center">
<PredictMascot className="size-24" aria-hidden />
<div className="flex flex-col gap-1">
<h1 className="text-base font-medium">You're all set</h1>
<p className="text-muted-foreground">
We'll use your profile to read your first panel.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
setDone(false)
setStep(0)
}}
>
Start again
</Button>
</CardContent>
) : (
<>
<CardHeader>
<Progress value={step + 1} max={steps.length}>
<ProgressLabel>Health profile</ProgressLabel>
<ProgressValue>{(_, value) => `Step ${value} of ${steps.length}`}</ProgressValue>
</Progress>
<CardTitle className="pt-4 text-base">{current.title}</CardTitle>
<CardDescription>{current.description}</CardDescription>
</CardHeader>
<CardContent>
{step === 0 && (
<div className="grid gap-4 sm:grid-cols-2">
<NumberField defaultValue={70} min={20} max={300}>
<NumberFieldScrubArea label="Weight (kg)" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
<NumberField defaultValue={170} min={100} max={230}>
<NumberFieldScrubArea label="Height (cm)" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</div>
)}
{step === 1 && (
<RadioGroup defaultValue="moderate" aria-label="Activity level">
{activity.map((option) => (
<label
key={option.value}
className="flex items-start gap-3 rounded-lg border p-3 has-data-checked:border-primary"
>
<RadioGroupItem value={option.value} className="mt-0.5" />
<span className="grid gap-1">
<span className="text-sm font-medium">{option.title}</span>
<span className="text-xs text-muted-foreground">{option.description}</span>
</span>
</label>
))}
</RadioGroup>
)}
{step === 2 && (
<div className="flex flex-col gap-4">
<Label className="justify-between">
Share results with my clinician
<Switch defaultChecked />
</Label>
<Label className="justify-between">
Email me when results are ready
<Switch defaultChecked />
</Label>
<Label className="justify-between">
Use my data for research
<Switch />
</Label>
</div>
)}
</CardContent>
<CardFooter className="justify-between">
<Button
variant="ghost"
size="sm"
disabled={step === 0}
onClick={() => setStep((s) => s - 1)}
>
<ArrowLeftIcon data-icon="inline-start" />
Back
</Button>
<Button
size="sm"
onClick={() => (isLast ? setDone(true) : setStep((s) => s + 1))}
>
{isLast ? "Finish" : "Continue"}
{isLast ? (
<CheckIcon data-icon="inline-end" />
) : (
<ArrowRightIcon data-icon="inline-end" />
)}
</Button>
</CardFooter>
</>
)}
</Card>
</div>
)
}
- app
- book
app/book/page.tsx
"use client"
import * as React from "react"
import { CheckIcon, MapPinIcon } from "lucide-react"
import { Button } from "@workspace/ui/components/button"
import { Calendar } from "@workspace/ui/components/calendar"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog"
import { RadioGroup, RadioGroupItem } from "@workspace/ui/components/radio-group"
import { Separator } from "@workspace/ui/components/separator"
// Blood draws need a fast, so only morning slots.
const slots = ["07:00", "07:30", "08:00", "08:30", "09:00", "09:30", "10:00", "10:30"]
const clinics = [
{ value: "andheri", name: "Andheri West", address: "Link Road, Mumbai" },
{ value: "bandra", name: "Bandra", address: "Hill Road, Mumbai" },
{ value: "home", name: "Home visit", address: "A phlebotomist comes to you" },
]
function tomorrow() {
const date = new Date()
date.setDate(date.getDate() + 1)
return date
}
export default function Page() {
const [date, setDate] = React.useState<Date | undefined>(tomorrow)
const [slot, setSlot] = React.useState("08:00")
const [clinic, setClinic] = React.useState("andheri")
const clinicName = clinics.find((c) => c.value === clinic)?.name
const dateLabel = date?.toLocaleDateString(undefined, {
weekday: "long",
day: "numeric",
month: "long",
})
return (
<div className="flex min-h-svh items-center justify-center p-4">
<Card className="w-full max-w-3xl">
<CardHeader>
<CardTitle className="text-base">Book your blood test</CardTitle>
<CardDescription>Fast for 10 to 12 hours before your slot. Water is fine.</CardDescription>
</CardHeader>
<CardContent className="grid gap-6 md:grid-cols-[auto_1fr]">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
disabled={{ before: tomorrow() }}
className="mx-auto rounded-lg border"
/>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h2 id="slot-label" className="text-xs font-medium">Time</h2>
<RadioGroup
value={slot}
onValueChange={(value) => setSlot(value as string)}
aria-labelledby="slot-label"
className="grid grid-cols-4 gap-2"
>
{slots.map((time) => (
<label
key={time}
className="flex cursor-pointer items-center justify-center rounded-full border py-2 text-xs tabular-nums has-data-checked:border-primary has-data-checked:bg-primary has-data-checked:text-primary-foreground"
>
<RadioGroupItem value={time} className="sr-only" />
{time}
</label>
))}
</RadioGroup>
</div>
<div className="flex flex-col gap-2">
<h2 id="clinic-label" className="text-xs font-medium">Where</h2>
<RadioGroup
value={clinic}
onValueChange={(value) => setClinic(value as string)}
aria-labelledby="clinic-label"
>
{clinics.map((option) => (
<label
key={option.value}
className="flex items-start gap-3 rounded-lg border p-3 has-data-checked:border-primary"
>
<RadioGroupItem value={option.value} className="mt-0.5" />
<span className="grid gap-0.5">
<span className="text-sm font-medium">{option.name}</span>
<span className="text-xs text-muted-foreground">{option.address}</span>
</span>
</label>
))}
</RadioGroup>
</div>
</div>
</CardContent>
<Separator />
<CardFooter className="flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="flex items-center gap-2 text-xs text-muted-foreground">
<MapPinIcon className="size-3.5 shrink-0" />
{date ? `${dateLabel} at ${slot}, ${clinicName}` : "Pick a date"}
</p>
<Dialog>
<DialogTrigger render={<Button disabled={!date} />}>Confirm booking</DialogTrigger>
<DialogContent showCloseButton={false}>
<DialogHeader>
<div className="mb-2 flex size-10 items-center justify-center rounded-full bg-status-normal/15 text-status-normal">
<CheckIcon className="size-5" />
</div>
<DialogTitle>You're booked</DialogTitle>
<DialogDescription>
{dateLabel} at {slot}, {clinicName}. We'll send a reminder the evening before.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button />}>Done</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</CardFooter>
</Card>
</div>
)
}
- app
- login
app/login/page.tsx
"use client"
import { Button } from "@workspace/ui/components/button"
import { Input } from "@workspace/ui/components/input"
import { Label } from "@workspace/ui/components/label"
import { Separator } from "@workspace/ui/components/separator"
import { Logo } from "@workspace/ui/predict/logo"
import { PredictMascot } from "@workspace/ui/predict/predict-mascot"
export default function Page() {
return (
<div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10">
<Logo role="img" aria-label="PREDICT" className="h-10 size-30 self-start" />
<div className="flex flex-1 items-center justify-center">
<form
className="flex w-full max-w-xs flex-col gap-6"
onSubmit={(event) => event.preventDefault()}
>
<div className="flex flex-col gap-1 text-center">
<h1 className="text-xl font-medium tracking-tight">Welcome back</h1>
<p className="text-sm text-muted-foreground">
Sign in to see your latest results.
</p>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="login-email">Email</Label>
<Input id="login-email" type="email" placeholder="you@example.com" required />
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="login-password">Password</Label>
<a href="#" className="text-xs text-muted-foreground underline-offset-4 hover:underline">
Forgot password?
</a>
</div>
<Input id="login-password" type="password" required />
</div>
<Button type="submit">Sign in</Button>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<Separator className="flex-1" />
or
<Separator className="flex-1" />
</div>
<Button type="button" variant="outline">
Email me a sign-in link
</Button>
<p className="text-center text-xs text-muted-foreground">
New to PREDICT?{" "}
<a href="#" className="text-foreground underline underline-offset-4">
Book your first test
</a>
</p>
</form>
</div>
</div>
<div className="relative hidden flex-col items-center justify-center gap-8 overflow-hidden bg-secondary p-10 lg:flex">
<PredictMascot className="size-40" aria-hidden />
<div className="flex max-w-sm flex-col gap-2 text-center">
<p className="text-lg font-medium tracking-tight">Understand your biology</p>
<p className="text-sm text-muted-foreground">
40 biomarkers, read against healthy ranges and explained in plain words.
</p>
</div>
</div>
</div>
)
}
- app
- header
app/header/page.tsx
"use client"
import { MenuIcon } from "lucide-react"
import { Button } from "@workspace/ui/components/button"
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@workspace/ui/components/navigation-menu"
import { Separator } from "@workspace/ui/components/separator"
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet"
import { Logo } from "@workspace/ui/predict/logo"
const tests = [
{ title: "Full panel", href: "#", description: "40 biomarkers across every body system." },
{ title: "Heart health", href: "#", description: "Lipids, inflammation and blood pressure." },
{ title: "Hormones", href: "#", description: "Thyroid, cortisol and sex hormones." },
{ title: "Metabolic", href: "#", description: "Glucose, insulin and HbA1c." },
]
const links = [
{ title: "How it works", href: "#" },
{ title: "Pricing", href: "#" },
{ title: "Clinics", href: "#" },
]
export default function Page() {
return (
<div className="min-h-svh">
<header className="sticky top-2 z-40 max-w-6xl mx-auto rounded-full border bg-background/40 backdrop-blur-md">
<div className="mx-auto flex h-12 max-w-6xl items-center gap-6 px-2">
<a href="#" className="shrink-0 pl-4">
<Logo role="img" aria-label="PREDICT" className="h-5 size-24" />
</a>
<NavigationMenu className="hidden md:flex">
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Tests</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[480px] grid-cols-2 gap-1">
{tests.map((test) => (
<li key={test.title}>
<NavigationMenuLink
href={test.href}
className="flex-col items-start gap-1"
>
<span className="font-medium">{test.title}</span>
<span className="line-clamp-2 text-muted-foreground">
{test.description}
</span>
</NavigationMenuLink>
</li>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
{links.map((link) => (
<NavigationMenuItem key={link.title}>
<NavigationMenuLink
href={link.href}
className={navigationMenuTriggerStyle()}
>
{link.title}
</NavigationMenuLink>
</NavigationMenuItem>
))}
</NavigationMenuList>
</NavigationMenu>
<div className="ms-auto hidden items-center gap-1 md:flex">
<Button variant="ghost" size="sm" render={<a href="#" />}>
Sign in
</Button>
<Button size="sm" render={<a href="#" />}>
Book a test
</Button>
</div>
<Sheet>
<SheetTrigger
render={<Button variant="ghost" size="icon-sm" className="ms-auto md:hidden" />}
>
<MenuIcon />
<span className="sr-only">Open menu</span>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>
<Logo role="img" aria-label="PREDICT" className="h-5 size-24" />
</SheetTitle>
</SheetHeader>
<nav className="flex flex-col gap-1 px-6">
<p className="pb-1 text-muted-foreground">Tests</p>
{tests.map((test) => (
<a key={test.title} href={test.href} className="py-1.5 text-sm">
{test.title}
</a>
))}
<Separator className="my-3" />
{links.map((link) => (
<a key={link.title} href={link.href} className="py-1.5 text-sm">
{link.title}
</a>
))}
</nav>
<SheetFooter>
<Button render={<a href="#" />}>Book a test</Button>
<Button variant="outline" render={<a href="#" />}>
Sign in
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
</header>
<main className="mx-auto flex h-screen max-w-6xl flex-col items-center gap-4 px-6 py-24 text-center">
<h1 className="text-3xl font-medium tracking-tight">Understand your biology</h1>
<p className="max-w-md text-sm text-muted-foreground">
40 biomarkers, read against healthy ranges and explained in plain words.
</p>
</main>
<main className="mx-auto flex h-screen max-w-6xl flex-col items-center gap-4 px-6 py-24 text-center">
<h1 className="text-3xl font-medium tracking-tight">Understand your biology</h1>
<p className="max-w-md text-sm text-muted-foreground">
40 biomarkers, read against healthy ranges and explained in plain words.
</p>
</main>
</div>
)
}