
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Biometric 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.
Also available for React ->$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.jsonnpx shadcn-vue@latest add @uipkge/vitals-tracker-chartInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
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
}<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