Vitals Tracker Chart
blockhealthcareBiometric vitals monitoring dashboard with multi-day trends, threshold bands, device sync status, 7-day vitals table with risk indicators, clinician alert limits, and quick-entry logging dialog.
Installation
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/vitals-tracker-chart.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/vitals-tracker-chart.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/vitals-tracker-chart.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/vitals-tracker-chart.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/vitals-tracker-chartInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
DailyVitalLoginterface DailyVitalLog {
id: string
date: string
isToday?: boolean
morningBP: string
morningPulse: number
eveningBP: string
eveningPulse: number
avgBP: string
systolicVal: number
diastolicVal: number
fastingGlucose: number
glucoseStatus: 'optimal' | 'normal' | 'elevated'
spo2: number
riskStatus: 'optimal' | 'normal' | 'elevated'
riskLabel: string
notes?: string
}Files installed (1)
app/components/blocks/VitalsTrackerChart.vue40.2 kB
<script setup lang="ts"> import { ref, type HTMLAttributes } from 'vue' import { Activity, Heart, TrendingDown, Minus, CheckCircle2, Plus, Sliders, Droplets, Wind, RefreshCw, Bell, ShieldCheck, Stethoscope, } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Progress } from '@/components/ui/progress' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Switch } from '@/components/ui/switch' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Textarea } from '@/components/ui/textarea' interface DailyVitalLog { id: string date: string isToday?: boolean morningBP: string morningPulse: number eveningBP: string eveningPulse: number avgBP: string systolicVal: number diastolicVal: number fastingGlucose: number glucoseStatus: 'optimal' | 'normal' | 'elevated' spo2: number riskStatus: 'optimal' | 'normal' | 'elevated' riskLabel: string notes?: string } const props = defineProps<{ class?: HTMLAttributes['class'] }>() const timeframe = ref<'7d' | '30d' | '90d'>('7d') const isSyncing = ref(false) const lastSyncText = ref('4m ago') const isLogDialogOpen = ref(false) const showSuccessNotification = ref(false) // Configurable Thresholds State const systolicLimit = ref('140') const diastolicLimit = ref('90') const hrAlertRange = ref('50-100') const glucoseAlertLimit = ref('130') const spo2AlertLimit = ref('95') // Alert Toggles const clinicianSmsAlert = ref(true) const ehrAutoSync = ref(true) const caregiverPush = ref(false) // New Entry Form State const formDate = ref('2026-08-21T08:30') const formPeriod = ref('morning') const formSystolic = ref('120') const formDiastolic = ref('78') const formPulse = ref('68') const formGlucose = ref('94') const formSpo2 = ref('99') const formDevice = ref('withings') const formNotes = ref('') const dailyLogs = ref<DailyVitalLog[]>([ { id: 'vital-1', date: 'Aug 21 (Today)', isToday: true, morningBP: '120/76', morningPulse: 66, eveningBP: '122/78', eveningPulse: 68, avgBP: '121/77', systolicVal: 121, diastolicVal: 77, fastingGlucose: 94, glucoseStatus: 'optimal', spo2: 99, riskStatus: 'optimal', riskLabel: 'Optimal', notes: 'Resting morning reading taken pre-breakfast.', }, { id: 'vital-2', date: 'Aug 20', morningBP: '124/80', morningPulse: 70, eveningBP: '122/79', eveningPulse: 69, avgBP: '123/80', systolicVal: 123, diastolicVal: 80, fastingGlucose: 94, glucoseStatus: 'optimal', spo2: 98, riskStatus: 'normal', riskLabel: 'Normal', }, { id: 'vital-3', date: 'Aug 19', morningBP: '126/82', morningPulse: 71, eveningBP: '125/80', eveningPulse: 70, avgBP: '126/81', systolicVal: 126, diastolicVal: 81, fastingGlucose: 97, glucoseStatus: 'normal', spo2: 99, riskStatus: 'normal', riskLabel: 'Normal', }, { id: 'vital-4', date: 'Aug 18', morningBP: '132/84', morningPulse: 74, eveningBP: '130/82', eveningPulse: 73, avgBP: '131/83', systolicVal: 131, diastolicVal: 83, fastingGlucose: 101, glucoseStatus: 'elevated', spo2: 98, riskStatus: 'elevated', riskLabel: 'Elevated', notes: 'Mild post-work stress reported by patient.', }, { id: 'vital-5', date: 'Aug 17', morningBP: '128/82', morningPulse: 72, eveningBP: '126/80', eveningPulse: 71, avgBP: '127/81', systolicVal: 127, diastolicVal: 81, fastingGlucose: 98, glucoseStatus: 'normal', spo2: 99, riskStatus: 'normal', riskLabel: 'Normal', }, { id: 'vital-6', date: 'Aug 16', morningBP: '122/78', morningPulse: 69, eveningBP: '124/80', eveningPulse: 70, avgBP: '123/79', systolicVal: 123, diastolicVal: 79, fastingGlucose: 95, glucoseStatus: 'optimal', spo2: 99, riskStatus: 'optimal', riskLabel: 'Optimal', }, { id: 'vital-7', date: 'Aug 15', morningBP: '120/78', morningPulse: 68, eveningBP: '121/77', eveningPulse: 67, avgBP: '121/78', systolicVal: 121, diastolicVal: 78, fastingGlucose: 96, glucoseStatus: 'optimal', spo2: 99, riskStatus: 'optimal', riskLabel: 'Optimal', }, ]) function handleSyncRefresh() { isSyncing.value = true setTimeout(() => { isSyncing.value = false lastSyncText.value = 'just now' }, 600) } function handleLogSubmit() { const systolic = parseInt(formSystolic.value, 10) || 120 const diastolic = parseInt(formDiastolic.value, 10) || 80 const pulse = parseInt(formPulse.value, 10) || 68 const glucose = parseInt(formGlucose.value, 10) || 94 const spo2 = parseInt(formSpo2.value, 10) || 99 let riskStatus: 'optimal' | 'normal' | 'elevated' = 'normal' let riskLabel = 'Normal' if (systolic >= 130 || diastolic >= 85 || glucose >= 100) { riskStatus = 'elevated' riskLabel = 'Elevated' } else if (systolic <= 120 && diastolic <= 80 && glucose < 95) { riskStatus = 'optimal' riskLabel = 'Optimal' } const newLog: DailyVitalLog = { id: `vital-${Date.now()}`, date: 'Aug 21 (Manual)', isToday: true, morningBP: `${systolic}/${diastolic}`, morningPulse: pulse, eveningBP: `${systolic}/${diastolic}`, eveningPulse: pulse, avgBP: `${systolic}/${diastolic}`, systolicVal: systolic, diastolicVal: diastolic, fastingGlucose: glucose, glucoseStatus: glucose < 95 ? 'optimal' : glucose <= 99 ? 'normal' : 'elevated', spo2, riskStatus, riskLabel, notes: formNotes.value.trim() || undefined, } dailyLogs.value = [newLog, ...dailyLogs.value.slice(0, 6)] isLogDialogOpen.value = false showSuccessNotification.value = true setTimeout(() => { showSuccessNotification.value = false }, 4000) } function getSparkline(history: number[]) { const min = Math.min(...history) const max = Math.max(...history) const range = max - min || 1 const height = 26 const padding = 3 const points = history.map((val, idx) => { const x = (idx / (history.length - 1)) * 100 const y = height - ((val - min) / range) * (height - padding * 2) - padding return { x, y } }) let path = `M ${points[0].x},${points[0].y}` for (let i = 1; i < points.length; i++) { const prev = points[i - 1] const curr = points[i] const cx = (prev.x + curr.x) / 2 path += ` C ${cx},${prev.y} ${cx},${curr.y} ${curr.x},${curr.y}` } const lastPoint = points[points.length - 1] const area = `${path} L 100,32 L 0,32 Z` return { path, area, lastPoint } } const bpHistory = [121, 123, 126, 131, 127, 123, 121] const hrHistory = [68, 70, 71, 74, 72, 69, 68] const glucoseHistory = [96, 95, 98, 101, 97, 94, 94] </script> <template> <div data-slot="vitals-tracker-chart" :class="cn('mx-auto w-full max-w-6xl space-y-6', props.class)"> <!-- Header Section --> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2.5"> <h1 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl"> Patient Biometrics & Vitals Telemetry </h1> <Badge wrap variant="outline" class="gap-1.5 text-xs font-normal"> <span class="bg-success size-1.5 rounded-full" /> <span class="tabular-nums">MRN-84920</span> </Badge> </div> <p class="text-muted-foreground text-sm"> Continuous physiological telemetry stream · Eleanor Vance (DOB: 1984-06-12) · Care Lead: Dr. M. Vance, MD </p> </div> <div class="flex flex-wrap items-center gap-2.5"> <!-- Device Sync Telemetry Badge --> <div class="bg-muted/50 border-border/80 text-card-foreground flex items-center gap-2 rounded-lg border px-3 py-1.5 text-xs shadow-xs" > <span class="relative flex size-2 shrink-0"> <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75" /> <span class="bg-success relative inline-flex size-2 rounded-full" /> </span> <span class="text-muted-foreground">Apple Health / Withings Connected · Last sync</span> <span class="text-foreground font-medium tabular-nums">{{ lastSyncText }}</span> <Button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-6 rounded-md p-0" aria-label="Sync vitals telemetry" @click="handleSyncRefresh" > <RefreshCw :class="['size-3.5', isSyncing ? 'animate-spin' : '']" /> </Button> </div> <!-- Timeframe Selector --> <div class="bg-muted border-border/60 flex items-center rounded-lg border p-0.5 text-xs"> <button type="button" :class="[ 'rounded-md px-2.5 py-1 font-medium transition-colors', timeframe === '7d' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ]" @click="timeframe = '7d'" > 7 Days </button> <button type="button" :class="[ 'rounded-md px-2.5 py-1 font-medium transition-colors', timeframe === '30d' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ]" @click="timeframe = '30d'" > 30 Days </button> <button type="button" :class="[ 'rounded-md px-2.5 py-1 font-medium transition-colors', timeframe === '90d' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ]" @click="timeframe = '90d'" > 90 Days </button> </div> <!-- Log Vitals Entry Dialog --> <Dialog v-model:open="isLogDialogOpen"> <DialogTrigger as-child> <Button size="sm" class="gap-1.5 shadow-xs"> <Plus class="size-4" /> <span>Log Vitals Entry</span> </Button> </DialogTrigger> <DialogContent class="sm:max-w-xl"> <DialogHeader> <DialogTitle>Log Biometric Vitals Entry</DialogTitle> <DialogDescription> Record manual cuff, glucometer, or pulse oximeter measurements into the clinical telemetry stream. </DialogDescription> </DialogHeader> <div class="grid grid-cols-1 gap-4 py-2 sm:grid-cols-2"> <div class="space-y-1.5 sm:col-span-2"> <Label for="vital-timestamp" class="text-xs">Measurement Timestamp</Label> <Input id="vital-timestamp" v-model="formDate" type="datetime-local" class="h-9 text-xs" /> </div> <div class="space-y-1.5"> <Label for="vital-period" class="text-xs">Context / Reading Period</Label> <Select v-model="formPeriod"> <SelectTrigger id="vital-period" class="h-9 text-xs"> <SelectValue placeholder="Select period" /> </SelectTrigger> <SelectContent> <SelectItem value="morning">Morning (Fasting)</SelectItem> <SelectItem value="midday">Midday (Pre-lunch)</SelectItem> <SelectItem value="evening">Evening (Post-dinner)</SelectItem> <SelectItem value="bedtime">Bedtime (Resting)</SelectItem> </SelectContent> </Select> </div> <div class="space-y-1.5"> <Label for="vital-device" class="text-xs">Telemetry Source Device</Label> <Select v-model="formDevice"> <SelectTrigger id="vital-device" class="h-9 text-xs"> <SelectValue placeholder="Select device" /> </SelectTrigger> <SelectContent> <SelectItem value="withings">Withings BPM Core</SelectItem> <SelectItem value="apple-watch">Apple Watch Series 9</SelectItem> <SelectItem value="omron">Omron Platinum Cuff</SelectItem> <SelectItem value="dexcom">Dexcom G7 CGM</SelectItem> <SelectItem value="manual">Manual Clinical Station</SelectItem> </SelectContent> </Select> </div> <div class="space-y-1.5"> <Label for="systolic-input" class="text-xs">Systolic Pressure (mmHg)</Label> <Input id="systolic-input" v-model="formSystolic" type="number" placeholder="120" class="h-9 text-xs tabular-nums" /> </div> <div class="space-y-1.5"> <Label for="diastolic-input" class="text-xs">Diastolic Pressure (mmHg)</Label> <Input id="diastolic-input" v-model="formDiastolic" type="number" placeholder="80" class="h-9 text-xs tabular-nums" /> </div> <div class="space-y-1.5"> <Label for="pulse-input" class="text-xs">Resting Heart Rate (bpm)</Label> <Input id="pulse-input" v-model="formPulse" type="number" placeholder="68" class="h-9 text-xs tabular-nums" /> </div> <div class="space-y-1.5"> <Label for="glucose-input" class="text-xs">Blood Glucose (mg/dL)</Label> <Input id="glucose-input" v-model="formGlucose" type="number" placeholder="94" class="h-9 text-xs tabular-nums" /> </div> <div class="space-y-1.5 sm:col-span-2"> <Label for="spo2-input" class="text-xs">Blood Oxygen SpO2 (%)</Label> <Input id="spo2-input" v-model="formSpo2" type="number" placeholder="99" class="h-9 text-xs tabular-nums" /> </div> <div class="space-y-1.5 sm:col-span-2"> <Label for="vital-notes" class="text-xs">Clinical Notes & Symptoms (Optional)</Label> <Textarea id="vital-notes" v-model="formNotes" placeholder="e.g. Patient rested seated for 5 minutes prior to measurement. No palpitations or dyspnea." rows="2" class="text-xs" /> </div> </div> <DialogFooter class="gap-2 sm:gap-0"> <Button variant="outline" size="sm" @click="isLogDialogOpen = false">Cancel</Button> <Button size="sm" @click="handleLogSubmit">Save Reading</Button> </DialogFooter> </DialogContent> </Dialog> </div> </div> <!-- Success Toast Notification --> <div v-if="showSuccessNotification" class="border-success/30 bg-success/10 text-foreground dark:text-foreground flex items-center justify-between gap-x-2 rounded-lg border p-3 text-xs shadow-xs" role="status" > <div class="flex items-center gap-2"> <CheckCircle2 class="text-success size-4 shrink-0" /> <span class="font-medium">Vitals telemetry entry successfully appended to EHR clinical chart.</span> </div> <Button aria-label="Dismiss notification" variant="ghost" size="icon-sm" class="text-success size-6" @click="showSuccessNotification = false" > <span class="text-xs">✕</span> </Button> </div> <!-- 4 Primary Vitals Overview Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- 1. Blood Pressure --> <Card class="flex flex-col justify-between"> <CardHeader class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2.5"> <div class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs" aria-hidden="true" > <Activity class="text-success size-4" /> </div> <CardTitle class="truncate text-sm font-medium">Blood Pressure</CardTitle> </div> <Badge wrap variant="success" class="shrink-0 text-xs">Normal</Badge> </div> </CardHeader> <CardContent class="space-y-3 pt-1"> <div> <div class="mb-1 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">122/78</span> <span class="text-muted-foreground text-xs">mmHg</span> </div> <span class="text-muted-foreground text-xs tabular-nums">MAP 92 mmHg</span> </div> <!-- Mini Sparkline for BP Trend --> <div class="h-7 w-full overflow-hidden pt-1" aria-label="7-day systolic blood pressure trend"> <svg class="h-full w-full overflow-visible" viewBox="0 0 100 28" preserveAspectRatio="none"> <defs> <linearGradient id="grad-bp-vue" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="var(--color-emerald-500, #10b981)" stop-opacity="0.25" /> <stop offset="100%" stop-color="var(--color-emerald-500, #10b981)" stop-opacity="0.0" /> </linearGradient> </defs> <path :d="getSparkline(bpHistory).area" fill="url(#grad-bp-vue)" /> <path :d="getSparkline(bpHistory).path" fill="none" stroke="var(--color-emerald-500, #10b981)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <circle :cx="getSparkline(bpHistory).lastPoint.x" :cy="getSparkline(bpHistory).lastPoint.y" r="2.5" class="fill-success" /> </svg> </div> </div> <div class="border-border/60 border-t pt-2.5 text-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">7-Day Mean:</span> <span class="text-foreground font-medium tabular-nums">124/80 mmHg</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">Target < 120/80 mmHg · Controlled</p> </div> </CardContent> </Card> <!-- 2. Resting Heart Rate --> <Card class="flex flex-col justify-between"> <CardHeader class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2.5"> <div class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs" aria-hidden="true" > <Heart class="text-destructive size-4" /> </div> <CardTitle class="truncate text-sm font-medium">Resting Heart Rate</CardTitle> </div> <Badge wrap variant="success" class="shrink-0 text-xs">Healthy</Badge> </div> </CardHeader> <CardContent class="space-y-3 pt-1"> <div> <div class="mb-1 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">68</span> <span class="text-muted-foreground text-xs">bpm</span> </div> <div class="text-success flex items-center gap-1 text-xs font-medium tabular-nums"> <TrendingDown class="size-3" /> <span>-2 bpm</span> </div> </div> <!-- Mini Sparkline for HR Trend --> <div class="h-7 w-full overflow-hidden pt-1" aria-label="7-day resting heart rate trend"> <svg class="h-full w-full overflow-visible" viewBox="0 0 100 28" preserveAspectRatio="none"> <defs> <linearGradient id="grad-hr-vue" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="var(--color-rose-500, #f43f5e)" stop-opacity="0.25" /> <stop offset="100%" stop-color="var(--color-rose-500, #f43f5e)" stop-opacity="0.0" /> </linearGradient> </defs> <path :d="getSparkline(hrHistory).area" fill="url(#grad-hr-vue)" /> <path :d="getSparkline(hrHistory).path" fill="none" stroke="var(--color-rose-500, #f43f5e)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <circle :cx="getSparkline(hrHistory).lastPoint.x" :cy="getSparkline(hrHistory).lastPoint.y" r="2.5" class="fill-destructive" /> </svg> </div> </div> <div class="border-border/60 border-t pt-2.5 text-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">Normal Range:</span> <span class="text-foreground font-medium tabular-nums">60 – 100 bpm</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">Sinus Rhythm · HRV 58 ms</p> </div> </CardContent> </Card> <!-- 3. Blood Glucose / Fasting --> <Card class="flex flex-col justify-between"> <CardHeader class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2.5"> <div class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs" aria-hidden="true" > <Droplets class="text-info size-4" /> </div> <CardTitle class="truncate text-sm font-medium">Blood Glucose</CardTitle> </div> <Badge wrap variant="success" class="shrink-0 text-xs">Fasting < 100</Badge> </div> </CardHeader> <CardContent class="space-y-3 pt-1"> <div> <div class="mb-1 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">94</span> <span class="text-muted-foreground text-xs">mg/dL</span> </div> <div class="text-muted-foreground flex items-center gap-1 text-xs font-medium tabular-nums"> <Minus class="size-3" /> <span>Stable</span> </div> </div> <!-- Mini Sparkline for Glucose Trend --> <div class="h-7 w-full overflow-hidden pt-1" aria-label="7-day blood glucose trend"> <svg class="h-full w-full overflow-visible" viewBox="0 0 100 28" preserveAspectRatio="none"> <defs> <linearGradient id="grad-glu-vue" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="var(--color-sky-500, #0ea5e9)" stop-opacity="0.25" /> <stop offset="100%" stop-color="var(--color-sky-500, #0ea5e9)" stop-opacity="0.0" /> </linearGradient> </defs> <path :d="getSparkline(glucoseHistory).area" fill="url(#grad-glu-vue)" /> <path :d="getSparkline(glucoseHistory).path" fill="none" stroke="var(--color-sky-500, #0ea5e9)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <circle :cx="getSparkline(glucoseHistory).lastPoint.x" :cy="getSparkline(glucoseHistory).lastPoint.y" r="2.5" class="fill-info" /> </svg> </div> </div> <div class="border-border/60 border-t pt-2.5 text-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">Fasting Window:</span> <span class="text-foreground font-medium tabular-nums">12 hrs</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">Estimated HbA1c: 5.4%</p> </div> </CardContent> </Card> <!-- 4. Blood Oxygen SpO2 --> <Card class="flex flex-col justify-between"> <CardHeader class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2.5"> <div class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs" aria-hidden="true" > <Wind class="size-4 text-teal-500" /> </div> <CardTitle class="truncate text-sm font-medium">Blood Oxygen SpO2</CardTitle> </div> <Badge wrap variant="success" class="shrink-0 text-xs">≥ 95% Normal</Badge> </div> </CardHeader> <CardContent class="space-y-3 pt-1"> <div> <div class="mb-1 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">99%</span> <span class="text-muted-foreground text-xs">SpO2</span> </div> <span class="text-success text-xs font-medium">0 drops detected</span> </div> <Progress :model-value="99" class="h-2 [&_[data-slot=progress-indicator]]:bg-teal-500" /> <div class="mt-1.5 flex items-center justify-between gap-x-2 text-xs"> <span class="text-muted-foreground">Pulse Oximeter</span> <span class="text-muted-foreground tabular-nums">Target ≥ 95%</span> </div> </div> <div class="border-border/60 border-t pt-2.5 text-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">Sleep O2 Baseline:</span> <span class="text-foreground font-medium tabular-nums">98.4%</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">Continuous Plethysmography</p> </div> </CardContent> </Card> </div> <!-- 7-Day Vitals Trend Grid & Daily Telemetry Table --> <Card> <CardHeader> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div> <CardTitle>7-Day Vitals Trend & Daily Telemetry Logs</CardTitle> <CardDescription> Comprehensive morning & evening multi-parameter biometrics with systolic range markers and risk stratification. </CardDescription> </div> <div class="flex items-center gap-2"> <Badge wrap variant="outline" class="w-fit text-xs font-normal tabular-nums"> {{ dailyLogs.length }} readings logged </Badge> </div> </div> </CardHeader> <CardContent class="p-0 sm:p-6 sm:pt-0"> <div class="overflow-x-auto"> <Table> <TableHeader> <TableRow> <TableHead class="min-w-[120px]">Date</TableHead> <TableHead class="min-w-[150px]">Morning Reading</TableHead> <TableHead class="min-w-[150px]">Evening Reading</TableHead> <TableHead class="min-w-[110px]">Daily Mean BP</TableHead> <TableHead class="min-w-[120px]">Fasting Glucose</TableHead> <TableHead class="min-w-[80px]">SpO2</TableHead> <TableHead class="min-w-[160px]">Systolic Band Indicator</TableHead> <TableHead class="text-right">Risk Status</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow v-for="log in dailyLogs" :key="log.id" :class="log.isToday ? 'bg-muted/25 font-medium' : ''"> <TableCell class="text-foreground text-xs whitespace-nowrap"> <div class="flex items-center gap-1.5"> <span class="tabular-nums">{{ log.date }}</span> <Badge wrap v-if="log.isToday" variant="secondary" class="h-4.5 px-1.5 text-xs font-normal" >Now</Badge > </div> </TableCell> <TableCell class="text-xs whitespace-nowrap"> <span class="text-foreground font-medium tabular-nums">{{ log.morningBP }}</span> <span class="text-muted-foreground text-xs"> mmHg · </span> <span class="text-muted-foreground tabular-nums">{{ log.morningPulse }} bpm</span> </TableCell> <TableCell class="text-xs whitespace-nowrap"> <span class="text-foreground font-medium tabular-nums">{{ log.eveningBP }}</span> <span class="text-muted-foreground text-xs"> mmHg · </span> <span class="text-muted-foreground tabular-nums">{{ log.eveningPulse }} bpm</span> </TableCell> <TableCell class="text-foreground text-xs font-semibold whitespace-nowrap tabular-nums"> {{ log.avgBP }} <span class="text-muted-foreground text-xs font-normal">mmHg</span> </TableCell> <TableCell class="text-xs whitespace-nowrap"> <span class="text-foreground font-medium tabular-nums">{{ log.fastingGlucose }}</span> <span class="text-muted-foreground text-xs"> mg/dL</span> </TableCell> <TableCell class="text-foreground text-xs font-medium whitespace-nowrap tabular-nums"> {{ log.spo2 }}% </TableCell> <TableCell class="text-xs"> <!-- Visual Horizontal Range Pill --> <div class="w-36 space-y-1"> <div class="bg-muted border-border/70 relative h-3 w-full overflow-hidden rounded-full border"> <!-- Normal Zone (110 - 129) --> <div class="bg-success/20 absolute inset-y-0 right-[50%] left-[16%]" /> <!-- Elevated Zone (130 - 139) --> <div class="bg-warning/25 absolute inset-y-0 right-[33%] left-[50%]" /> <!-- High Zone (>= 140) --> <div class="bg-destructive/25 absolute inset-y-0 right-0 left-[67%]" /> <!-- Reading Dot Marker (Clamped to 100-160 scale) --> <div class="absolute top-1/2 size-2 -translate-y-1/2 rounded-full shadow-xs" :class="[ log.systolicVal < 130 ? 'bg-success ring-success/30 ring-2' : log.systolicVal < 140 ? 'bg-warning ring-warning/30 ring-2' : 'bg-destructive ring-destructive/30 ring-2', ]" :style="{ left: `${Math.min(96, Math.max(4, ((log.systolicVal - 100) / 60) * 100))}%`, }" /> </div> <div class="text-muted-foreground flex justify-between text-xs tabular-nums"> <span>100</span> <span class="text-success font-medium">120</span> <span>140+</span> </div> </div> </TableCell> <TableCell class="text-right"> <Badge :variant=" log.riskStatus === 'optimal' ? 'success' : log.riskStatus === 'normal' ? 'success' : 'warning' " class="text-xs whitespace-normal capitalize" > {{ log.riskLabel }} </Badge> </TableCell> </TableRow> </TableBody> </Table> </div> </CardContent> </Card> <!-- Threshold & Alert Limits Card --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2"> <Card> <CardHeader> <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-2"> <Sliders class="text-primary size-4" /> <CardTitle>Configurable Clinical Thresholds</CardTitle> </div> <Badge wrap variant="outline" class="text-xs font-normal">AHA / ACC Standards</Badge> </div> <CardDescription> Custom warning & critical breach boundaries tailored to this patient's clinical care plan. </CardDescription> </CardHeader> <CardContent class="space-y-4"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="space-y-1.5"> <Label class="text-xs">Systolic Alert Limit</Label> <Select v-model="systolicLimit"> <SelectTrigger class="h-9 text-xs"> <SelectValue placeholder="Select limit" /> </SelectTrigger> <SelectContent> <SelectItem value="130">> 130 mmHg (Elevated)</SelectItem> <SelectItem value="140">> 140 mmHg (Stage 2 Alert)</SelectItem> <SelectItem value="160">> 160 mmHg (Critical Crisis)</SelectItem> </SelectContent> </Select> </div> <div class="space-y-1.5"> <Label class="text-xs">Diastolic Alert Limit</Label> <Select v-model="diastolicLimit"> <SelectTrigger class="h-9 text-xs"> <SelectValue placeholder="Select limit" /> </SelectTrigger> <SelectContent> <SelectItem value="85">> 85 mmHg (Elevated)</SelectItem> <SelectItem value="90">> 90 mmHg (Stage 2 Alert)</SelectItem> <SelectItem value="100">> 100 mmHg (Crisis)</SelectItem> </SelectContent> </Select> </div> <div class="space-y-1.5"> <Label class="text-xs">Heart Rate Alert Range</Label> <Select v-model="hrAlertRange"> <SelectTrigger class="h-9 text-xs"> <SelectValue placeholder="Select range" /> </SelectTrigger> <SelectContent> <SelectItem value="50-100">< 50 or > 100 bpm</SelectItem> <SelectItem value="55-110">< 55 or > 110 bpm</SelectItem> <SelectItem value="45-120">< 45 or > 120 bpm (Wide)</SelectItem> </SelectContent> </Select> </div> <div class="space-y-1.5"> <Label class="text-xs">Fasting Glucose Warning</Label> <Select v-model="glucoseAlertLimit"> <SelectTrigger class="h-9 text-xs"> <SelectValue placeholder="Select limit" /> </SelectTrigger> <SelectContent> <SelectItem value="120">> 120 mg/dL (Pre-diabetic)</SelectItem> <SelectItem value="130">> 130 mg/dL (Clinical Alert)</SelectItem> <SelectItem value="150">> 150 mg/dL (High Trigger)</SelectItem> </SelectContent> </Select> </div> </div> <Separator /> <div class="border-border bg-muted/40 flex items-center justify-between gap-x-2 rounded-lg border p-3"> <div class="space-y-0.5"> <p class="text-foreground text-xs font-medium">Oxygen Desaturation Limit</p> <p class="text-muted-foreground text-xs">Triggers hypoxemia alert if SpO2 drops below bound.</p> </div> <Badge wrap variant="outline" class="text-xs font-medium tabular-nums" >< {{ spo2AlertLimit }}% SpO2</Badge > </div> </CardContent> </Card> <Card> <CardHeader> <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-2"> <Bell class="text-primary size-4" /> <CardTitle>Automated Clinical Alert Protocol</CardTitle> </div> <Badge wrap variant="success" class="gap-1 text-xs"> <ShieldCheck class="size-3" /> <span>Active Sentinel</span> </Badge> </div> <CardDescription> Instant escalation rules and clinician notification triggers for threshold violations. </CardDescription> </CardHeader> <CardContent class="space-y-4"> <div class="flex items-start justify-between gap-4"> <div class="space-y-0.5"> <Label class="text-foreground text-xs font-medium">Clinician Urgent SMS Alert</Label> <p class="text-muted-foreground text-xs leading-relaxed"> Immediately dispatch SMS alert to on-call cardiologist when 2 consecutive readings exceed threshold. </p> </div> <Switch v-model="clinicianSmsAlert" aria-label="Toggle clinician SMS alert" /> </div> <Separator /> <div class="flex items-start justify-between gap-4"> <div class="space-y-0.5"> <Label class="text-foreground text-xs font-medium">EHR Auto-Sync (FHIR R4 Stream)</Label> <p class="text-muted-foreground text-xs leading-relaxed"> Stream verified biometric readings directly to patient's electronic health record chart. </p> </div> <Switch v-model="ehrAutoSync" aria-label="Toggle EHR auto sync" /> </div> <Separator /> <div class="bg-muted/40 border-border space-y-2 rounded-lg border p-3 text-xs"> <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-2"> <Stethoscope class="text-primary size-3.5" /> <span class="text-foreground font-semibold">On-Call Clinical Lead:</span> </div> <span class="text-foreground font-medium">Dr. Sarah Chen, MD (Cardiology)</span> </div> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Pager: #4920 · Priority SLA:</span> <span class="text-success font-medium">< 15 min triage</span> </div> </div> </CardContent> </Card> </div> </div> </template>
Raw manifest:https://uipkge.dev/r/vue/vitals-tracker-chart.json