Refractor. Update styling
CI / backend (push) Successful in 10m14s
CI / frontend (push) Successful in 1m55s

This commit is contained in:
2026-08-11 06:48:07 +08:00
parent e3f3a3ced8
commit 87e551c0d7
57 changed files with 450 additions and 393 deletions
@@ -12,7 +12,7 @@ public sealed class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbConte
{ {
var connectionString = var connectionString =
Environment.GetEnvironmentVariable("ConnectionStrings__DefaultConnection") Environment.GetEnvironmentVariable("ConnectionStrings__DefaultConnection")
?? "Host=127.0.0.1;Database=vigilcare;Username=postgres;Password=postgres"; ?? "Host=localhost;Port=5436;Database=vigilcare;Username=postgres;Password=password";
var options = new DbContextOptionsBuilder<AppDbContext>() var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(connectionString) .UseNpgsql(connectionString)
+6
View File
@@ -4,6 +4,12 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap"
rel="stylesheet"
/>
<title>VigilCare Clinical</title> <title>VigilCare Clinical</title>
<script> <script>
;(function () { ;(function () {
@@ -9,8 +9,9 @@ describe('Badge', () => {
slots: { default: 'High' }, slots: { default: 'High' },
}) })
const classes = wrapper.classes().join(' ') const classes = wrapper.classes().join(' ')
expect(classes).toContain('bg-severity-critical/10') expect(classes).toContain('bg-severity-critical')
expect(classes).toContain('text-severity-critical') expect(classes).toContain('text-white')
expect(classes).toContain('rounded-sm')
}) })
it('rendersWarningVariant', () => { it('rendersWarningVariant', () => {
@@ -19,8 +20,8 @@ describe('Badge', () => {
slots: { default: 'Med' }, slots: { default: 'Med' },
}) })
const classes = wrapper.classes().join(' ') const classes = wrapper.classes().join(' ')
expect(classes).toContain('bg-severity-warning/10') expect(classes).toContain('bg-severity-warning')
expect(classes).toContain('text-severity-warning') expect(classes).toContain('text-white')
}) })
it('rendersSlotContent', () => { it('rendersSlotContent', () => {
@@ -47,8 +47,9 @@ describe('DepartmentOverviewView', () => {
const wrapper = mount(DepartmentOverviewView) const wrapper = mount(DepartmentOverviewView)
await vi.waitFor(() => expect(wrapper.text()).toContain('1 active patient')) await vi.waitFor(() => expect(wrapper.text()).toContain('1 active patient'))
expect(wrapper.text()).toContain('Department Overview') expect(wrapper.text()).toContain('patients')
expect(wrapper.text()).toContain('Critical (NEWS2 ≥ 7)') expect(wrapper.text()).toContain('critical')
expect(wrapper.text()).toContain('Alert volume') expect(wrapper.text()).toContain('Alert volume')
expect(wrapper.text()).toContain('Open in Virtual Ward')
}) })
}) })
@@ -37,7 +37,7 @@ describe('PatientBanner', () => {
const wrapper = mount(PatientBanner, { props: { encounter } }) const wrapper = mount(PatientBanner, { props: { encounter } })
expect(wrapper.text()).toContain('Penicillin, Latex') expect(wrapper.text()).toContain('Penicillin, Latex')
const strip = wrapper.find('[role="status"]') const strip = wrapper.find('[role="status"]')
expect(strip.classes().join(' ')).toMatch(/bg-red-600/) expect(strip.classes().join(' ')).toMatch(/bg-severity-critical/)
}) })
it('showsSimChipWhenPatientIsSimulated', () => { it('showsSimChipWhenPatientIsSimulated', () => {
@@ -43,7 +43,7 @@ describe('SepsisBoardView', () => {
setActivePinia(createPinia()) setActivePinia(createPinia())
const wrapper = mount(SepsisBoardView) const wrapper = mount(SepsisBoardView)
await vi.waitFor(() => expect(wrapper.text()).toContain('Alice A')) await vi.waitFor(() => expect(wrapper.text()).toContain('Alice A'))
expect(wrapper.text()).toContain('Sepsis Bundle Board') expect(wrapper.text()).toContain('1 active bundle')
expect(wrapper.text()).toContain('Serum lactate') expect(wrapper.text()).toContain('Serum lactate')
}) })
}) })
@@ -91,7 +91,7 @@ describe('WardTable', () => {
it('showsCriticalBadgeForHighNews2', () => { it('showsCriticalBadgeForHighNews2', () => {
const wrapper = mount(WardTable, { props: { patients, ...sortProps } }) const wrapper = mount(WardTable, { props: { patients, ...sortProps } })
const highRiskRow = wrapper.findAll('tbody tr')[2] const highRiskRow = wrapper.findAll('tbody tr')[2]
expect(highRiskRow.html()).toContain('text-severity-critical') expect(highRiskRow.html()).toContain('bg-severity-critical')
}) })
it('showsExtendedClinicalColumns', () => { it('showsExtendedClinicalColumns', () => {
@@ -67,7 +67,7 @@ function submit() {
:type="field.type" :type="field.type"
:step="field.step" :step="field.step"
:readonly="readOnlyCode && field.key === 'observationCode'" :readonly="readOnlyCode && field.key === 'observationCode'"
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
:class="[ :class="[
showError(field.key) ? 'border-red-500' : 'border-gray-300', showError(field.key) ? 'border-red-500' : 'border-gray-300',
readOnlyCode && field.key === 'observationCode' ? 'bg-gray-100 dark:bg-gray-700' : '', readOnlyCode && field.key === 'observationCode' ? 'bg-gray-100 dark:bg-gray-700' : '',
@@ -62,7 +62,7 @@ function submit() {
v-model="values.username" v-model="values.username"
type="text" type="text"
autocomplete="off" autocomplete="off"
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
:class="showError('username') ? 'border-red-500' : 'border-gray-300'" :class="showError('username') ? 'border-red-500' : 'border-gray-300'"
@blur="onBlur('username')" @blur="onBlur('username')"
> >
@@ -79,7 +79,7 @@ function submit() {
v-model="values.password" v-model="values.password"
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
:class="showError('password') ? 'border-red-500' : 'border-gray-300'" :class="showError('password') ? 'border-red-500' : 'border-gray-300'"
@blur="onBlur('password')" @blur="onBlur('password')"
> >
@@ -95,7 +95,7 @@ function submit() {
<input <input
v-model="values.displayName" v-model="values.displayName"
type="text" type="text"
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
:class="showError('displayName') ? 'border-red-500' : 'border-gray-300'" :class="showError('displayName') ? 'border-red-500' : 'border-gray-300'"
@blur="onBlur('displayName')" @blur="onBlur('displayName')"
> >
@@ -110,7 +110,7 @@ function submit() {
</label> </label>
<select <select
v-model="values.role" v-model="values.role"
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
:class="showError('role') ? 'border-red-500' : 'border-gray-300'" :class="showError('role') ? 'border-red-500' : 'border-gray-300'"
@blur="onBlur('role')" @blur="onBlur('role')"
> >
@@ -128,7 +128,7 @@ function submit() {
id="user-active" id="user-active"
v-model="values.isActive" v-model="values.isActive"
type="checkbox" type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" class="h-4 w-4 rounded border-gray-300 text-brand focus:ring-brand"
> >
<label for="user-active" class="text-sm text-gray-700 dark:text-gray-300"> <label for="user-active" class="text-sm text-gray-700 dark:text-gray-300">
Account active Account active
@@ -73,7 +73,7 @@ function onConfirm() {
<textarea <textarea
v-model="note" v-model="note"
rows="3" rows="3"
class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100" class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
placeholder="Add clinical context (optional)" placeholder="Add clinical context (optional)"
/> />
</label> </label>
@@ -1,7 +1,6 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import Card from '@/components/ui/Card.vue' import Card from '@/components/ui/Card.vue'
import Badge from '@/components/ui/Badge.vue'
import SeverityBadge from '@/components/ui/SeverityBadge.vue' import SeverityBadge from '@/components/ui/SeverityBadge.vue'
import Button from '@/components/ui/Button.vue' import Button from '@/components/ui/Button.vue'
import FeedbackButtons from '@/components/feedback/FeedbackButtons.vue' import FeedbackButtons from '@/components/feedback/FeedbackButtons.vue'
@@ -41,29 +40,33 @@ function formatTime(iso) {
if (!iso) return '' if (!iso) return ''
return new Date(iso).toLocaleString() return new Date(iso).toLocaleString()
} }
const statusLabel = computed(() => props.alert.status?.replaceAll('_', ' ') ?? '')
</script> </script>
<template> <template>
<Card> <Card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"> <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<SeverityBadge :severity="alert.severity" /> <SeverityBadge :severity="alert.severity" />
<Badge variant="info" size="xs">{{ alert.status }}</Badge> <span class="text-xs font-medium text-ink-muted">{{ statusLabel }}</span>
<Badge v-if="actionHint" variant="info" size="xs">{{ actionHint }}</Badge>
</div> </div>
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white"> <h3 class="mt-1.5 text-sm font-semibold text-ink">
{{ alertTypeLabel(alert.alertType) }} {{ alertTypeLabel(alert.alertType) }}
</h3> </h3>
<p v-if="alertSummaryText(alert)" class="mt-2 text-sm text-gray-600 dark:text-gray-400"> <p v-if="actionHint" class="mt-1 text-xs font-medium text-brand">
{{ actionHint }}
</p>
<p v-if="alertSummaryText(alert)" class="mt-1.5 text-sm text-ink-secondary">
{{ alertSummaryText(alert) }} {{ alertSummaryText(alert) }}
</p> </p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-500"> <p class="mt-1.5 text-xs text-ink-muted">
{{ formatTime(alert.triggeredAt) }} {{ formatTime(alert.triggeredAt) }}
</p> </p>
<p <p
v-if="alert.acknowledgedBy" v-if="alert.acknowledgedBy"
class="mt-2 text-xs text-gray-500 dark:text-gray-400" class="mt-1 text-xs text-ink-muted"
> >
Acknowledged by {{ formatAcknowledgedByDisplay(alert.acknowledgedBy) }} Acknowledged by {{ formatAcknowledgedByDisplay(alert.acknowledgedBy) }}
<span v-if="alert.acknowledgedAt">at {{ formatTime(alert.acknowledgedAt) }}</span> <span v-if="alert.acknowledgedAt">at {{ formatTime(alert.acknowledgedAt) }}</span>
@@ -94,7 +97,7 @@ function formatTime(iso) {
<div <div
v-if="alert.status === 'ACKNOWLEDGED' || alert.status === 'RESOLVED'" v-if="alert.status === 'ACKNOWLEDGED' || alert.status === 'RESOLVED'"
class="mt-4 border-t border-gray-100 pt-4 dark:border-gray-700" class="mt-3 border-t border-border pt-3"
> >
<FeedbackButtons <FeedbackButtons
:alert-id="alert.id" :alert-id="alert.id"
@@ -10,16 +10,16 @@ const tabs = [
</script> </script>
<template> <template>
<div class="-mx-4 overflow-x-auto px-4 sm:mx-0 sm:px-0"> <div class="-mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0">
<div class="flex min-w-max gap-2 border-b border-gray-200 dark:border-gray-700 sm:min-w-0 sm:flex-wrap"> <div class="flex min-w-max gap-1 border-b border-border sm:min-w-0 sm:flex-wrap">
<button <button
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.value" :key="tab.value"
type="button" type="button"
class="shrink-0 border-b-2 px-4 py-2 text-sm font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" class="shrink-0 border-b-2 px-3 py-2 text-sm font-medium transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:class="activeFilter === tab.value :class="activeFilter === tab.value
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400' ? 'border-brand text-brand'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" : 'border-transparent text-ink-muted hover:text-ink-secondary'"
@click="activeFilter = tab.value" @click="activeFilter = tab.value"
> >
{{ tab.label }} {{ tab.label }}
@@ -200,7 +200,7 @@ function formatMed(med) {
<p <p
v-if="actionHint" v-if="actionHint"
class="rounded bg-blue-50 px-4 py-2 text-sm text-blue-800 dark:bg-blue-950/40 dark:text-blue-200" class="rounded bg-brand-soft px-4 py-2 text-sm text-brand dark:bg-brand-soft dark:text-brand-muted"
> >
{{ actionHint }} {{ actionHint }}
</p> </p>
@@ -21,18 +21,22 @@ function onSelect() {
<template> <template>
<button <button
type="button" type="button"
class="w-full text-left transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" class="w-full text-left transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:class="hasPatients ? 'cursor-pointer hover:-translate-y-0.5' : 'cursor-default opacity-80'" :class="hasPatients ? 'cursor-pointer' : 'cursor-default opacity-80'"
:disabled="!hasPatients" :disabled="!hasPatients"
@click="onSelect" @click="onSelect"
> >
<Card padding="lg" class="h-full"> <Card
<div class="mb-4 flex items-start justify-between gap-4"> padding="lg"
class="h-full border-l-2 border-l-transparent transition duration-150"
:class="hasPatients ? 'hover:border-l-brand' : ''"
>
<div class="mb-3 flex items-start justify-between gap-3">
<div> <div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white"> <h2 class="text-base font-semibold text-ink">
{{ department.label }} {{ department.label }}
</h2> </h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p class="mt-0.5 text-sm text-ink-muted">
{{ department.patientCount }} active patient{{ department.patientCount === 1 ? '' : 's' }} {{ department.patientCount }} active patient{{ department.patientCount === 1 ? '' : 's' }}
</p> </p>
</div> </div>
@@ -41,44 +45,44 @@ function onSelect() {
</Badge> </Badge>
</div> </div>
<div class="space-y-4"> <div class="space-y-3">
<div> <div>
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> <p class="mb-1.5 text-xs font-medium text-ink-muted">
NEWS2 acuity NEWS2 acuity
</p> </p>
<AcuityBar :acuity="department.acuity" /> <AcuityBar :acuity="department.acuity" />
</div> </div>
<dl class="grid grid-cols-2 gap-4 text-sm"> <dl class="grid grid-cols-2 gap-3 text-sm">
<div> <div>
<dt class="text-gray-500 dark:text-gray-400">Avg NEWS2</dt> <dt class="text-ink-muted">Avg NEWS2</dt>
<dd class="mt-1 font-semibold text-gray-900 dark:text-white"> <dd class="mt-0.5 font-semibold text-ink">
{{ department.averageNews2 ?? '—' }} {{ department.averageNews2 ?? '—' }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-gray-500 dark:text-gray-400">Open alerts</dt> <dt class="text-ink-muted">Open alerts</dt>
<dd class="mt-1 font-semibold text-gray-900 dark:text-white"> <dd class="mt-0.5 font-semibold text-ink">
{{ department.openAlertCount }} {{ department.openAlertCount }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-gray-500 dark:text-gray-400">Sepsis bundles</dt> <dt class="text-ink-muted">Sepsis bundles</dt>
<dd class="mt-1 font-semibold text-gray-900 dark:text-white"> <dd class="mt-0.5 font-semibold text-ink">
{{ department.activeBundleCount }} {{ department.activeBundleCount }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-gray-500 dark:text-gray-400">Alert volume</dt> <dt class="text-ink-muted">Alert volume</dt>
<dd class="mt-1 font-semibold text-gray-900 dark:text-white"> <dd class="mt-0.5 font-semibold text-ink">
{{ department.alertVolume }} {{ department.alertVolume }}
</dd> </dd>
</div> </div>
</dl> </dl>
</div> </div>
<p v-if="hasPatients" class="mt-4 text-xs font-medium text-blue-600 dark:text-blue-400"> <p v-if="hasPatients" class="mt-3 text-xs font-medium text-brand">
View patients in Virtual Ward Open in Virtual Ward
</p> </p>
</Card> </Card>
</button> </button>
@@ -24,7 +24,7 @@ const ratings = [
{ value: 'too-early', label: 'Too early', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300' }, { value: 'too-early', label: 'Too early', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300' },
{ value: 'too-late', label: 'Too late', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300' }, { value: 'too-late', label: 'Too late', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300' },
{ value: 'false-positive', label: 'False positive', color: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' }, { value: 'false-positive', label: 'False positive', color: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' },
{ value: 'missing-context', label: 'Missing context', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' }, { value: 'missing-context', label: 'Missing context', color: 'bg-brand-soft text-brand dark:bg-brand-soft dark:text-brand-hover' },
{ value: 'would-act', label: 'Would act', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' }, { value: 'would-act', label: 'Would act', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' },
] ]
@@ -69,7 +69,7 @@ async function submitNotes() {
role="radio" role="radio"
:aria-checked="selectedRating === r.value" :aria-checked="selectedRating === r.value"
:disabled="submitting || !!selectedRating" :disabled="submitting || !!selectedRating"
class="rounded-full px-4 py-2 text-xs font-medium transition duration-150 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:opacity-50" class="rounded-sm px-3 py-1.5 text-xs font-medium transition duration-150 focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 disabled:opacity-50"
:class="[ :class="[
selectedRating === r.value ? r.color : 'bg-gray-100 text-gray-500 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700', selectedRating === r.value ? r.color : 'bg-gray-100 text-gray-500 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700',
selectedRating === r.value ? 'ring-2 ring-current' : '', selectedRating === r.value ? 'ring-2 ring-current' : '',
@@ -81,7 +81,7 @@ async function submitNotes() {
<button <button
v-if="selectedRating && !showNotes" v-if="selectedRating && !showNotes"
class="rounded-full px-4 py-2 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" class="rounded-sm px-3 py-1.5 text-xs text-ink-muted hover:text-ink-secondary"
@click.stop="showNotes = true" @click.stop="showNotes = true"
> >
+ Note + Note
@@ -100,7 +100,7 @@ async function submitNotes() {
@keydown.enter="submitNotes" @keydown.enter="submitNotes"
/> />
<button <button
class="rounded bg-blue-500 px-4 py-2 text-xs text-white hover:bg-blue-600 disabled:opacity-50" class="rounded bg-brand px-4 py-2 text-xs text-white hover:bg-brand-hover disabled:opacity-50"
:disabled="submitting" :disabled="submitting"
@click.stop="submitNotes" @click.stop="submitNotes"
> >
@@ -31,23 +31,20 @@ function onDepartmentChange(event) {
</script> </script>
<template> <template>
<header class="sticky top-0 z-30 border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"> <header class="sticky top-0 z-30 border-b border-border bg-surface-raised">
<div class="flex h-16 items-center gap-4 px-4 lg:px-8"> <div class="flex h-14 items-center gap-3 px-3 lg:px-5">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="truncate text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> <h1 class="truncate text-base font-semibold text-ink">
VigilCare Clinical
</p>
<h1 class="truncate text-lg font-semibold text-gray-900 dark:text-gray-100">
{{ pageTitle }} {{ pageTitle }}
</h1> </h1>
</div> </div>
<div class="flex shrink-0 items-center gap-2 sm:gap-4"> <div class="flex shrink-0 items-center gap-2 sm:gap-3">
<label v-if="route.name === 'WardDashboard'" class="hidden sm:block"> <label v-if="route.name === 'WardDashboard'" class="hidden sm:block">
<span class="sr-only">Department filter</span> <span class="sr-only">Department filter</span>
<select <select
:value="department ?? ''" :value="department ?? ''"
class="rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200" class="rounded-md border border-border bg-surface-raised px-3 py-1.5 text-sm text-ink-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
@change="onDepartmentChange" @change="onDepartmentChange"
> >
<option v-for="dept in departments" :key="dept.value" :value="dept.value"> <option v-for="dept in departments" :key="dept.value" :value="dept.value">
@@ -58,13 +55,13 @@ function onDepartmentChange(event) {
<button <button
type="button" type="button"
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-600 transition duration-200 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:text-gray-300 dark:hover:bg-gray-800" class="flex h-8 w-8 items-center justify-center rounded-md text-ink-secondary transition duration-150 hover:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:aria-label="alertSoundMuted ? 'Unmute critical alert sound' : 'Mute critical alert sound'" :aria-label="alertSoundMuted ? 'Unmute critical alert sound' : 'Mute critical alert sound'"
@click="settingsStore.toggleAlertSoundMute()" @click="settingsStore.toggleAlertSoundMute()"
> >
<svg <svg
v-if="alertSoundMuted" v-if="alertSoundMuted"
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -85,7 +82,7 @@ function onDepartmentChange(event) {
</svg> </svg>
<svg <svg
v-else v-else
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -102,13 +99,13 @@ function onDepartmentChange(event) {
<button <button
type="button" type="button"
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-600 transition duration-200 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:text-gray-300 dark:hover:bg-gray-800" class="flex h-8 w-8 items-center justify-center rounded-md text-ink-secondary transition duration-150 hover:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:aria-label="darkMode ? 'Switch to light mode' : 'Switch to dark mode'" :aria-label="darkMode ? 'Switch to light mode' : 'Switch to dark mode'"
@click="toggle" @click="toggle"
> >
<svg <svg
v-if="darkMode" v-if="darkMode"
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -123,7 +120,7 @@ function onDepartmentChange(event) {
</svg> </svg>
<svg <svg
v-else v-else
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -138,13 +135,13 @@ function onDepartmentChange(event) {
</svg> </svg>
</button> </button>
<div class="hidden items-center gap-3 border-l border-gray-200 pl-4 dark:border-gray-700 sm:flex"> <div class="hidden items-center gap-3 border-l border-border pl-3 sm:flex">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"> <span class="text-sm font-medium text-ink-secondary">
{{ authStore.displayName }} {{ authStore.displayName }}
</span> </span>
<button <button
type="button" type="button"
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-600 transition duration-200 hover:bg-red-50 hover:text-red-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 dark:text-gray-300 dark:hover:bg-red-950/50 dark:hover:text-red-400" class="flex h-8 w-8 items-center justify-center rounded-md text-ink-secondary transition duration-150 hover:bg-severity-critical-light hover:text-severity-critical focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-severity-critical"
aria-label="Sign out" aria-label="Sign out"
@click="authStore.logout()" @click="authStore.logout()"
> >
@@ -31,17 +31,17 @@ watch(() => authStore.isAuthenticated, (ok) => {
<template> <template>
<a <a
href="#main-content" href="#main-content"
class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-lg focus:bg-blue-600 focus:px-4 focus:py-3 focus:text-sm focus:font-medium focus:text-white focus:outline-none focus:ring-2 focus:ring-blue-400" class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-brand focus:px-4 focus:py-3 focus:text-sm focus:font-medium focus:text-brand-foreground focus:outline-none focus:ring-2 focus:ring-brand-hover"
> >
Skip to main content Skip to main content
</a> </a>
<div class="flex min-h-screen bg-gray-50 dark:bg-gray-950"> <div class="flex min-h-screen bg-surface">
<AppSidebar /> <AppSidebar />
<div class="flex min-w-0 flex-1 flex-col"> <div class="flex min-w-0 flex-1 flex-col">
<AppHeader /> <AppHeader />
<SimulationModeBanner /> <SimulationModeBanner />
<CriticalAlertBanner /> <CriticalAlertBanner />
<main id="main-content" class="flex-1 overflow-y-auto p-4 pb-24 lg:p-8 lg:pb-8" tabindex="-1"> <main id="main-content" class="flex-1 overflow-y-auto p-3 pb-24 lg:p-5 lg:pb-5" tabindex="-1">
<div class="mx-auto w-full max-w-7xl"> <div class="mx-auto w-full max-w-7xl">
<slot /> <slot />
</div> </div>
@@ -10,27 +10,27 @@ const { mainNavLinks, adminNavLinks, showAdminSection } = useRoleAccess()
function linkClasses(path) { function linkClasses(path) {
const active = route.path.startsWith(path) const active = route.path.startsWith(path)
return active return active
? 'bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300' ? 'bg-brand-soft text-brand-soft-text'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800' : 'text-ink-secondary hover:bg-surface-muted'
} }
</script> </script>
<template> <template>
<aside class="hidden w-64 shrink-0 border-r border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900 lg:flex lg:flex-col"> <aside class="hidden w-60 shrink-0 border-r border-border bg-surface-raised lg:flex lg:flex-col">
<div class="flex h-16 items-center px-8"> <div class="flex h-14 items-center px-4">
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">VigilCare</span> <span class="text-base font-bold text-brand">VigilCare</span>
</div> </div>
<nav class="flex-1 px-4 py-4" aria-label="Main navigation"> <nav class="flex-1 px-2 py-3" aria-label="Main navigation">
<ul class="space-y-2"> <ul class="space-y-0.5">
<li v-for="link in mainNavLinks" :key="link.to"> <li v-for="link in mainNavLinks" :key="link.to">
<RouterLink <RouterLink
:to="link.to" :to="link.to"
class="flex items-center gap-4 rounded-lg px-4 py-2 text-sm font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:class="linkClasses(link.to)" :class="linkClasses(link.to)"
> >
<svg <svg
v-if="link.icon === 'ward'" v-if="link.icon === 'ward'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -45,7 +45,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'sepsis'" v-else-if="link.icon === 'sepsis'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -60,7 +60,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'alerts'" v-else-if="link.icon === 'alerts'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -75,7 +75,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'departments'" v-else-if="link.icon === 'departments'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -90,7 +90,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'reconciliation'" v-else-if="link.icon === 'reconciliation'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -105,7 +105,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'ops'" v-else-if="link.icon === 'ops'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -120,7 +120,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'analytics'" v-else-if="link.icon === 'analytics'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -135,7 +135,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'simulation'" v-else-if="link.icon === 'simulation'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -156,7 +156,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else v-else
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -174,20 +174,20 @@ function linkClasses(path) {
</li> </li>
</ul> </ul>
<div v-if="showAdminSection" class="mt-8"> <div v-if="showAdminSection" class="mt-6">
<p class="mb-2 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"> <p class="mb-1.5 px-3 text-xs font-medium text-ink-muted">
Admin Admin
</p> </p>
<ul class="space-y-2"> <ul class="space-y-0.5">
<li v-for="link in adminNavLinks" :key="link.to"> <li v-for="link in adminNavLinks" :key="link.to">
<RouterLink <RouterLink
:to="link.to" :to="link.to"
class="flex items-center gap-4 rounded-lg px-4 py-2 text-sm font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:class="linkClasses(link.to)" :class="linkClasses(link.to)"
> >
<svg <svg
v-if="link.icon === 'users'" v-if="link.icon === 'users'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -202,7 +202,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'audit'" v-else-if="link.icon === 'audit'"
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -217,7 +217,7 @@ function linkClasses(path) {
</svg> </svg>
<svg <svg
v-else v-else
class="h-6 w-6 shrink-0" class="h-5 w-5 shrink-0"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -242,21 +242,21 @@ function linkClasses(path) {
</ul> </ul>
</div> </div>
</nav> </nav>
<div class="border-t border-gray-200 px-4 py-4 dark:border-gray-800"> <div class="border-t border-border px-2 py-3">
<div class="mb-2 px-4"> <div class="mb-1.5 px-3">
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100"> <p class="truncate text-sm font-medium text-ink">
{{ authStore.displayName }} {{ authStore.displayName }}
</p> </p>
<p class="truncate text-xs text-gray-500 dark:text-gray-400"> <p class="truncate text-xs text-ink-muted">
{{ authStore.role }} {{ authStore.role }}
</p> </p>
</div> </div>
<button <button
type="button" type="button"
class="flex w-full items-center gap-4 rounded-lg px-4 py-2 text-sm font-medium text-gray-700 transition duration-200 hover:bg-red-50 hover:text-red-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 dark:text-gray-300 dark:hover:bg-red-950/50 dark:hover:text-red-400" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-ink-secondary transition duration-150 hover:bg-severity-critical-light hover:text-severity-critical focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-severity-critical"
@click="authStore.logout()" @click="authStore.logout()"
> >
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <svg class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg> </svg>
Sign out Sign out
@@ -27,18 +27,18 @@ function closeOverflow() {
<template> <template>
<nav <nav
class="fixed inset-x-0 bottom-0 z-40 border-t border-gray-200 bg-white lg:hidden dark:border-gray-800 dark:bg-gray-900" class="fixed inset-x-0 bottom-0 z-40 border-t border-border bg-surface-raised lg:hidden"
aria-label="Mobile navigation" aria-label="Mobile navigation"
> >
<div v-if="overflowOpen && mobileOverflowLinks.length" class="border-b border-gray-200 dark:border-gray-800"> <div v-if="overflowOpen && mobileOverflowLinks.length" class="border-b border-border">
<ul class="flex flex-col py-2"> <ul class="flex flex-col py-1.5">
<li v-for="link in mobileOverflowLinks" :key="link.to"> <li v-for="link in mobileOverflowLinks" :key="link.to">
<RouterLink <RouterLink
:to="link.to" :to="link.to"
class="flex items-center gap-3 px-4 py-3 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500" class="flex items-center gap-3 px-4 py-2.5 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
:class="linkActive(link.to) :class="linkActive(link.to)
? 'text-blue-600 dark:text-blue-400' ? 'text-brand'
: 'text-gray-700 dark:text-gray-300'" : 'text-ink-secondary'"
@click="closeOverflow" @click="closeOverflow"
> >
{{ link.label }} {{ link.label }}
@@ -47,19 +47,19 @@ function closeOverflow() {
</ul> </ul>
</div> </div>
<ul class="flex h-16 items-stretch"> <ul class="flex h-14 items-stretch">
<li v-for="link in mobileNavLinks" :key="link.to" class="min-w-0 flex-1"> <li v-for="link in mobileNavLinks" :key="link.to" class="min-w-0 flex-1">
<RouterLink <RouterLink
:to="link.to" :to="link.to"
class="flex h-full flex-col items-center justify-center gap-1 px-1 py-2 text-[11px] font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 sm:gap-2 sm:px-4 sm:text-xs" class="flex h-full flex-col items-center justify-center gap-0.5 px-1 py-1.5 text-[11px] font-medium transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand sm:gap-1 sm:px-3 sm:text-xs"
:class="linkActive(link.to) :class="linkActive(link.to)
? 'text-blue-600 dark:text-blue-400' ? 'text-brand'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" : 'text-ink-muted hover:text-ink-secondary'"
@click="closeOverflow" @click="closeOverflow"
> >
<svg <svg
v-if="link.icon === 'ward'" v-if="link.icon === 'ward'"
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -74,7 +74,7 @@ function closeOverflow() {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'sepsis'" v-else-if="link.icon === 'sepsis'"
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -89,7 +89,7 @@ function closeOverflow() {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'reconciliation'" v-else-if="link.icon === 'reconciliation'"
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -104,7 +104,7 @@ function closeOverflow() {
</svg> </svg>
<svg <svg
v-else-if="link.icon === 'simulation'" v-else-if="link.icon === 'simulation'"
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -125,7 +125,7 @@ function closeOverflow() {
</svg> </svg>
<svg <svg
v-else v-else
class="h-6 w-6" class="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -145,15 +145,15 @@ function closeOverflow() {
<li v-if="mobileOverflowLinks.length" class="min-w-0 flex-1"> <li v-if="mobileOverflowLinks.length" class="min-w-0 flex-1">
<button <button
type="button" type="button"
class="flex h-full w-full flex-col items-center justify-center gap-1 px-1 py-2 text-[11px] font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 sm:gap-2 sm:px-4 sm:text-xs" class="flex h-full w-full flex-col items-center justify-center gap-0.5 px-1 py-1.5 text-[11px] font-medium transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand sm:gap-1 sm:px-3 sm:text-xs"
:class="overflowOpen || mobileOverflowLinks.some(l => linkActive(l.to)) :class="overflowOpen || mobileOverflowLinks.some(l => linkActive(l.to))
? 'text-blue-600 dark:text-blue-400' ? 'text-brand'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" : 'text-ink-muted hover:text-ink-secondary'"
:aria-expanded="overflowOpen" :aria-expanded="overflowOpen"
aria-label="More navigation" aria-label="More navigation"
@click="toggleOverflow" @click="toggleOverflow"
> >
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" />
</svg> </svg>
<span class="truncate">More</span> <span class="truncate">More</span>
@@ -163,10 +163,10 @@ function closeOverflow() {
<li class="min-w-0 flex-1"> <li class="min-w-0 flex-1">
<button <button
type="button" type="button"
class="flex h-full w-full flex-col items-center justify-center gap-1 px-1 py-2 text-[11px] font-medium text-gray-500 transition duration-200 hover:text-red-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-red-500 dark:text-gray-400 dark:hover:text-red-400 sm:gap-2 sm:px-4 sm:text-xs" class="flex h-full w-full flex-col items-center justify-center gap-0.5 px-1 py-1.5 text-[11px] font-medium text-ink-muted transition duration-150 hover:text-severity-critical focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-severity-critical sm:gap-1 sm:px-3 sm:text-xs"
@click="authStore.logout()" @click="authStore.logout()"
> >
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg> </svg>
<span class="truncate">Sign out</span> <span class="truncate">Sign out</span>
@@ -65,8 +65,8 @@ async function resolve(alertId) {
> >
<button <button
type="button" type="button"
class="flex w-full min-h-11 cursor-pointer flex-col gap-4 rounded-lg px-2 py-2 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 sm:flex-row sm:items-start sm:justify-between" class="flex w-full min-h-11 cursor-pointer flex-col gap-4 rounded-lg px-2 py-2 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand sm:flex-row sm:items-start sm:justify-between"
:class="selectedId === alert.id ? 'bg-blue-50/50 dark:bg-blue-950/20' : 'hover:bg-gray-50 dark:hover:bg-gray-800/40'" :class="selectedId === alert.id ? 'bg-brand-soft dark:bg-brand-soft' : 'hover:bg-gray-50 dark:hover:bg-gray-800/40'"
:aria-pressed="selectedId === alert.id" :aria-pressed="selectedId === alert.id"
:aria-label="`Select ${alertTypeLabel(alert.alertType)} alert, ${alert.severity} severity`" :aria-label="`Select ${alertTypeLabel(alert.alertType)} alert, ${alert.severity} severity`"
@click="emit('select', alert)" @click="emit('select', alert)"
@@ -55,7 +55,7 @@ function submit() {
<label class="mb-2 block text-xs text-gray-500 dark:text-gray-400">Eye (E)</label> <label class="mb-2 block text-xs text-gray-500 dark:text-gray-400">Eye (E)</label>
<select <select
v-model.number="eye" v-model.number="eye"
class="w-full rounded border border-gray-300 px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border border-gray-300 px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
> >
<option v-for="opt in eyeOptions" :key="opt.value" :value="opt.value"> <option v-for="opt in eyeOptions" :key="opt.value" :value="opt.value">
{{ opt.label }} {{ opt.label }}
@@ -67,7 +67,7 @@ function submit() {
<label class="mb-2 block text-xs text-gray-500 dark:text-gray-400">Verbal (V)</label> <label class="mb-2 block text-xs text-gray-500 dark:text-gray-400">Verbal (V)</label>
<select <select
v-model.number="verbal" v-model.number="verbal"
class="w-full rounded border border-gray-300 px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border border-gray-300 px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
> >
<option v-for="opt in verbalOptions" :key="opt.value" :value="opt.value"> <option v-for="opt in verbalOptions" :key="opt.value" :value="opt.value">
{{ opt.label }} {{ opt.label }}
@@ -79,7 +79,7 @@ function submit() {
<label class="mb-2 block text-xs text-gray-500 dark:text-gray-400">Motor (M)</label> <label class="mb-2 block text-xs text-gray-500 dark:text-gray-400">Motor (M)</label>
<select <select
v-model.number="motor" v-model.number="motor"
class="w-full rounded border border-gray-300 px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border border-gray-300 px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
> >
<option v-for="opt in motorOptions" :key="opt.value" :value="opt.value"> <option v-for="opt in motorOptions" :key="opt.value" :value="opt.value">
{{ opt.label }} {{ opt.label }}
@@ -96,7 +96,7 @@ function submit() {
</div> </div>
<button <button
type="button" type="button"
class="w-full rounded bg-blue-500 px-6 py-2 text-sm font-medium text-white hover:bg-blue-600 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:opacity-50 sm:w-auto" class="w-full rounded bg-brand px-6 py-2 text-sm font-medium text-white hover:bg-brand-hover focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 disabled:opacity-50 sm:w-auto"
:disabled="submitting" :disabled="submitting"
@click="submit" @click="submit"
> >
@@ -34,12 +34,12 @@ const emergencyContact = computed(() => {
</script> </script>
<template> <template>
<header class="w-full min-w-0 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700"> <header class="w-full min-w-0 overflow-hidden rounded-md border border-border">
<div <div
class="px-4 py-2 text-sm font-medium" class="px-3 py-1.5 text-sm font-medium"
:class="allergiesKnown :class="allergiesKnown
? 'bg-red-600 text-white' ? 'bg-severity-critical text-white'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'" : 'bg-surface-muted text-ink-secondary'"
role="status" role="status"
:aria-label="allergiesKnown ? `Known allergies: ${allergyText}` : 'No known drug allergies'" :aria-label="allergiesKnown ? `Known allergies: ${allergyText}` : 'No known drug allergies'"
> >
@@ -47,69 +47,69 @@ const emergencyContact = computed(() => {
{{ allergyText }} {{ allergyText }}
</div> </div>
<div class="space-y-3 bg-white p-4 dark:bg-gray-900"> <div class="space-y-2.5 bg-surface-raised p-3">
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1"> <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h1 class="text-xl font-bold text-gray-900 dark:text-white"> <h1 class="text-lg font-semibold text-ink">
{{ fullName }} {{ fullName }}
</h1> </h1>
<span <span
v-if="simulated" v-if="simulated"
class="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-200" class="inline-flex items-center rounded-sm px-1.5 py-0.5 text-[10px] font-bold uppercase bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-200"
title="Simulated patient" title="Simulated patient"
> >
SIM SIM
</span> </span>
<span class="text-sm text-gray-600 dark:text-gray-400"> <span class="text-sm text-ink-muted">
MRN {{ patient.mrn ?? '—' }} MRN {{ patient.mrn ?? '—' }}
</span> </span>
</div> </div>
<dl class="grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3"> <dl class="grid gap-2.5 text-sm sm:grid-cols-2 lg:grid-cols-3">
<div> <div>
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">DOB / Age</dt> <dt class="text-xs font-medium text-ink-muted">DOB / Age</dt>
<dd class="mt-0.5 text-gray-900 dark:text-gray-100"> <dd class="mt-0.5 text-ink">
{{ formatDobWithAge(patient.dateOfBirth) }} {{ formatDobWithAge(patient.dateOfBirth) }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Gender</dt> <dt class="text-xs font-medium text-ink-muted">Gender</dt>
<dd class="mt-0.5 text-gray-900 dark:text-gray-100"> <dd class="mt-0.5 text-ink">
{{ formatGender(patient.gender) }} {{ formatGender(patient.gender) }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Blood type</dt> <dt class="text-xs font-medium text-ink-muted">Blood type</dt>
<dd class="mt-0.5 text-gray-900 dark:text-gray-100"> <dd class="mt-0.5 text-ink">
{{ formatBloodType(patient.bloodType) }} {{ formatBloodType(patient.bloodType) }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Room / Bed</dt> <dt class="text-xs font-medium text-ink-muted">Room / Bed</dt>
<dd class="mt-0.5 text-gray-900 dark:text-gray-100"> <dd class="mt-0.5 text-ink">
{{ encounter.roomBed ?? '—' }} {{ encounter.roomBed ?? '—' }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Department</dt> <dt class="text-xs font-medium text-ink-muted">Department</dt>
<dd class="mt-0.5 text-gray-900 dark:text-gray-100"> <dd class="mt-0.5 text-ink">
{{ formatDepartment(encounter.department) }} {{ formatDepartment(encounter.department) }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Attending</dt> <dt class="text-xs font-medium text-ink-muted">Attending</dt>
<dd class="mt-0.5 text-gray-900 dark:text-gray-100"> <dd class="mt-0.5 text-ink">
{{ encounter.attendingPhysician ?? '—' }} {{ encounter.attendingPhysician ?? '—' }}
</dd> </dd>
</div> </div>
<div class="sm:col-span-2 lg:col-span-3"> <div class="sm:col-span-2 lg:col-span-3">
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Admission reason</dt> <dt class="text-xs font-medium text-ink-muted">Admission reason</dt>
<dd class="mt-0.5 text-gray-900 dark:text-gray-100"> <dd class="mt-0.5 text-ink">
{{ encounter.admissionReason ?? '—' }} {{ encounter.admissionReason ?? '—' }}
</dd> </dd>
</div> </div>
<div v-if="emergencyContact" class="sm:col-span-2 lg:col-span-3"> <div v-if="emergencyContact" class="sm:col-span-2 lg:col-span-3">
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Emergency contact</dt> <dt class="text-xs font-medium text-ink-muted">Emergency contact</dt>
<dd class="mt-0.5 text-gray-900 dark:text-gray-100"> <dd class="mt-0.5 text-ink">
{{ emergencyContact }} {{ emergencyContact }}
</dd> </dd>
</div> </div>
@@ -61,7 +61,7 @@ async function onGcsSubmit({ eye, verbal, motor }) {
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">GCS</div> <div class="text-xs font-medium text-gray-500 dark:text-gray-400">GCS</div>
<button <button
type="button" type="button"
class="text-xs text-blue-600 hover:underline dark:text-blue-400" class="text-xs text-brand hover:underline dark:text-brand-hover"
@click="showGcsForm = !showGcsForm" @click="showGcsForm = !showGcsForm"
> >
{{ showGcsForm ? 'Cancel' : 'Record GCS' }} {{ showGcsForm ? 'Cancel' : 'Record GCS' }}
@@ -68,7 +68,7 @@ function organVariant(score) {
<button <button
type="button" type="button"
class="text-xs text-blue-600 hover:underline dark:text-blue-400" class="text-xs text-brand hover:underline dark:text-brand-hover"
@click="fetch" @click="fetch"
> >
Refresh SOFA Refresh SOFA
@@ -58,7 +58,7 @@ function submit() {
v-model="values[field.key]" v-model="values[field.key]"
type="number" type="number"
:step="field.step" :step="field.step"
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
:class="showError(field.key) ? 'border-red-500' : 'border-gray-300'" :class="showError(field.key) ? 'border-red-500' : 'border-gray-300'"
@blur="onBlur(field.key)" @blur="onBlur(field.key)"
> >
@@ -71,7 +71,7 @@ function submit() {
<label class="mb-2 block text-xs text-gray-500 dark:text-gray-400">AVPU (score)</label> <label class="mb-2 block text-xs text-gray-500 dark:text-gray-400">AVPU (score)</label>
<select <select
v-model="values.avpu" v-model="values.avpu"
class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" class="w-full rounded border px-4 py-2 text-sm focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-600 dark:bg-gray-800 dark:text-white"
:class="showError('avpu') ? 'border-red-500' : 'border-gray-300'" :class="showError('avpu') ? 'border-red-500' : 'border-gray-300'"
@blur="onBlur('avpu')" @blur="onBlur('avpu')"
> >
@@ -94,7 +94,7 @@ function submit() {
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
type="button" type="button"
class="w-full rounded bg-blue-500 px-6 py-2 text-sm font-medium text-white hover:bg-blue-600 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:opacity-50 sm:w-auto" class="w-full rounded bg-brand px-6 py-2 text-sm font-medium text-white hover:bg-brand-hover focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 disabled:opacity-50 sm:w-auto"
:disabled="submitting" :disabled="submitting"
@click="submit" @click="submit"
> >
@@ -53,7 +53,7 @@ async function onSubmit(observations) {
</h2> </h2>
<button <button
type="button" type="button"
class="text-xs text-blue-600 hover:underline dark:text-blue-400" class="text-xs text-brand hover:underline dark:text-brand-hover"
@click="showVitalsForm = !showVitalsForm" @click="showVitalsForm = !showVitalsForm"
> >
{{ showVitalsForm ? 'Cancel' : 'Record Vitals' }} {{ showVitalsForm ? 'Cancel' : 'Record Vitals' }}
@@ -34,7 +34,7 @@ const speedPresets = [
<div class="min-w-0 flex-1" role="progressbar" :aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100" :aria-label="`Replay progress ${progress}%`"> <div class="min-w-0 flex-1" role="progressbar" :aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100" :aria-label="`Replay progress ${progress}%`">
<div class="h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700"> <div class="h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div <div
class="h-full rounded-full bg-blue-500 transition-all duration-300" class="h-full rounded-full bg-brand transition-all duration-300"
:style="{ width: `${progress}%` }" :style="{ width: `${progress}%` }"
/> />
</div> </div>
@@ -49,9 +49,9 @@ const speedPresets = [
v-for="preset in speedPresets" v-for="preset in speedPresets"
:key="preset.value" :key="preset.value"
type="button" type="button"
class="min-h-11 min-w-11 rounded px-3 py-2 text-xs transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" class="min-h-11 min-w-11 rounded px-3 py-2 text-xs transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:class="speed === preset.value :class="speed === preset.value
? 'bg-blue-500 text-white' ? 'bg-brand text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400'" : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400'"
:aria-pressed="speed === preset.value" :aria-pressed="speed === preset.value"
:aria-label="`Set replay speed to ${preset.label}`" :aria-label="`Set replay speed to ${preset.label}`"
@@ -22,15 +22,15 @@ const outstanding = computed(() => outstandingElements(props.bundle))
<template> <template>
<article <article
class="cursor-pointer rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition duration-200 hover:border-gray-300 hover:shadow-md active:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:hover:border-gray-600 dark:active:bg-gray-800" class="cursor-pointer rounded-md border border-border bg-surface-raised p-3 transition duration-200 hover:border-brand/40 active:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
> >
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<h3 class="truncate text-base font-semibold text-gray-900 dark:text-white"> <h3 class="truncate text-base font-semibold text-ink">
{{ bundle.firstName }} {{ bundle.lastName }} {{ bundle.firstName }} {{ bundle.lastName }}
</h3> </h3>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ bundle.mrn }}</p> <p class="text-xs text-ink-muted">{{ bundle.mrn }}</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1 text-xs text-ink-muted">
{{ formatDepartment(bundle.department) }} {{ formatDepartment(bundle.department) }}
<span v-if="bundle.roomBed"> · {{ bundle.roomBed }}</span> <span v-if="bundle.roomBed"> · {{ bundle.roomBed }}</span>
</p> </p>
@@ -38,19 +38,19 @@ const outstanding = computed(() => outstandingElements(props.bundle))
<Badge :variant="urgencyVariant(urgency)">{{ urgencyLabel(urgency) }}</Badge> <Badge :variant="urgencyVariant(urgency)">{{ urgencyLabel(urgency) }}</Badge>
</div> </div>
<dl class="mt-4 grid grid-cols-2 gap-4 border-t border-gray-100 pt-4 dark:border-gray-800"> <dl class="mt-3 grid grid-cols-2 gap-3 border-t border-border pt-3">
<div> <div>
<dt class="text-xs text-gray-500 dark:text-gray-400">Time remaining</dt> <dt class="text-xs text-ink-muted">Time remaining</dt>
<dd <dd
class="mt-2 font-mono text-sm font-semibold" class="mt-1.5 font-mono text-sm font-semibold"
:class="urgency === 'overdue' ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white'" :class="urgency === 'overdue' ? 'text-severity-critical' : 'text-ink'"
> >
{{ formatCountdown(bundle.deadlineAt, now) }} {{ formatCountdown(bundle.deadlineAt, now) }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-gray-500 dark:text-gray-400">Outstanding</dt> <dt class="text-xs text-ink-muted">Outstanding</dt>
<dd class="mt-2 text-sm text-gray-900 dark:text-white"> <dd class="mt-1.5 text-sm text-ink">
<span v-if="outstanding.length === 0">None</span> <span v-if="outstanding.length === 0">None</span>
<ul v-else class="space-y-1"> <ul v-else class="space-y-1">
<li v-for="element in outstanding" :key="element.id" class="text-xs"> <li v-for="element in outstanding" :key="element.id" class="text-xs">
@@ -16,7 +16,7 @@ function goToPatient(encounterId) {
</script> </script>
<template> <template>
<div class="space-y-4 md:hidden"> <div class="space-y-3 md:hidden">
<SepsisBundleCard <SepsisBundleCard
v-for="bundle in bundles" v-for="bundle in bundles"
:key="bundle.id" :key="bundle.id"
@@ -26,27 +26,27 @@ function goToPatient(encounterId) {
/> />
</div> </div>
<div class="hidden overflow-x-auto rounded-lg border border-gray-200 md:block dark:border-gray-700"> <div class="hidden overflow-x-auto rounded-md border border-border md:block">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table class="min-w-full divide-y divide-border">
<thead class="sticky top-0 bg-gray-50 dark:bg-gray-800"> <thead class="sticky top-0 bg-surface-muted">
<tr> <tr>
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Patient</th> <th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Patient</th>
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Department</th> <th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Department</th>
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Room</th> <th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Room</th>
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Started</th> <th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Started</th>
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Deadline</th> <th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Deadline</th>
<th class="px-4 py-4 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Remaining</th> <th class="px-3 py-2.5 text-right text-xs font-medium text-ink-muted">Remaining</th>
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Status</th> <th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Status</th>
<th class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Elements</th> <th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Elements</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900"> <tbody class="divide-y divide-border bg-surface-raised">
<SepsisBundleRow <SepsisBundleRow
v-for="bundle in bundles" v-for="bundle in bundles"
:key="bundle.id" :key="bundle.id"
:bundle="bundle" :bundle="bundle"
:now="now" :now="now"
class="cursor-pointer transition duration-200 hover:bg-gray-50 dark:hover:bg-gray-800" class="cursor-pointer transition duration-200 hover:bg-surface-muted"
@click="goToPatient(bundle.encounterId)" @click="goToPatient(bundle.encounterId)"
/> />
</tbody> </tbody>
@@ -149,7 +149,7 @@ defineExpose({ closeConfirm })
v-model="confirmText" v-model="confirmText"
type="text" type="text"
autocomplete="off" autocomplete="off"
class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100" class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
placeholder="RESET" placeholder="RESET"
> >
</label> </label>
@@ -72,7 +72,7 @@ function onSelect() {
padding="md" padding="md"
class="flex h-full flex-col transition" class="flex h-full flex-col transition"
:class="selected :class="selected
? 'ring-2 ring-blue-500 dark:ring-blue-400' ? 'ring-2 ring-brand dark:ring-brand-hover'
: 'hover:border-gray-300 dark:hover:border-gray-600'" : 'hover:border-gray-300 dark:hover:border-gray-600'"
> >
<div <div
@@ -100,7 +100,7 @@ function onSelect() {
<button <button
v-if="canExpand" v-if="canExpand"
type="button" type="button"
class="mt-1 self-start text-xs font-medium text-blue-600 hover:underline dark:text-blue-400" class="mt-1 self-start text-xs font-medium text-brand hover:underline dark:text-brand-hover"
@click="expanded = !expanded" @click="expanded = !expanded"
> >
{{ expanded ? 'Show less' : 'Show more' }} {{ expanded ? 'Show less' : 'Show more' }}
@@ -67,7 +67,7 @@ function progressWidth(run) {
{{ runStatusLabel(run.status) }} {{ runStatusLabel(run.status) }}
</Badge> </Badge>
<span <span
class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900/30 dark:text-blue-300" class="inline-flex items-center rounded-sm bg-brand-soft px-2 py-0.5 text-xs font-medium text-brand dark:bg-brand-soft dark:text-brand-hover"
> >
{{ formatSpeedBadge(run.speed) }} {{ formatSpeedBadge(run.speed) }}
</span> </span>
@@ -77,7 +77,7 @@ function progressWidth(run) {
<template v-if="patientLink(run)"> <template v-if="patientLink(run)">
<RouterLink <RouterLink
:to="patientLink(run)" :to="patientLink(run)"
class="font-medium text-blue-600 hover:underline dark:text-blue-400" class="font-medium text-brand hover:underline dark:text-brand-hover"
> >
{{ run.patientDisplayName || 'Open patient' }} {{ run.patientDisplayName || 'Open patient' }}
</RouterLink> </RouterLink>
@@ -134,7 +134,7 @@ function progressWidth(run) {
aria-valuemax="100" aria-valuemax="100"
> >
<div <div
class="h-full rounded-full bg-blue-600 transition-all dark:bg-blue-500" class="h-full rounded-full bg-brand transition-all dark:bg-brand"
:style="{ width: progressWidth(run) }" :style="{ width: progressWidth(run) }"
/> />
</div> </div>
@@ -223,7 +223,7 @@ function progressWidth(run) {
<RouterLink <RouterLink
v-if="patientLink(run)" v-if="patientLink(run)"
:to="patientLink(run)" :to="patientLink(run)"
class="font-medium text-blue-600 hover:underline dark:text-blue-400" class="font-medium text-brand hover:underline dark:text-brand-hover"
> >
{{ run.patientDisplayName || 'Open' }} {{ run.patientDisplayName || 'Open' }}
</RouterLink> </RouterLink>
@@ -8,13 +8,13 @@ const props = defineProps({
}) })
const classes = computed(() => { const classes = computed(() => {
const base = 'inline-flex items-center font-medium rounded-full' const base = 'inline-flex items-center font-semibold rounded-sm tracking-normal'
const sizes = { xs: 'px-2 py-2 text-xs', sm: 'px-2 py-2 text-xs', md: 'px-4 py-2 text-sm' } const sizes = { xs: 'px-1.5 py-0.5 text-[11px]', sm: 'px-2 py-0.5 text-xs', md: 'px-2.5 py-1 text-sm' }
const variants = { const variants = {
critical: 'bg-severity-critical/10 text-severity-critical dark:bg-severity-critical-dark/20 dark:text-red-300', critical: 'bg-severity-critical text-white dark:bg-severity-critical dark:text-white',
warning: 'bg-severity-warning/10 text-severity-warning dark:bg-severity-warning-dark/20 dark:text-amber-300', warning: 'bg-severity-warning text-white dark:bg-severity-warning dark:text-white',
info: 'bg-severity-info/10 text-severity-info dark:bg-severity-info-dark/20 dark:text-blue-300', info: 'bg-severity-info text-white dark:bg-severity-info dark:text-white',
success: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300', success: 'bg-risk-low text-white dark:bg-risk-low dark:text-white',
} }
return twMerge(base, sizes[props.size], variants[props.variant]) return twMerge(base, sizes[props.size], variants[props.variant])
}) })
@@ -23,21 +23,21 @@ const attrs = useAttrs()
const classes = computed(() => { const classes = computed(() => {
const base = const base =
'inline-flex items-center justify-center font-medium rounded-lg transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none' 'inline-flex items-center justify-center font-medium rounded-md transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none'
const sizes = { const sizes = {
sm: 'min-h-11 px-4 py-2 text-xs', sm: 'min-h-10 px-3 py-1.5 text-xs',
md: 'min-h-11 px-4 py-2 text-sm', md: 'min-h-10 px-3.5 py-2 text-sm',
lg: 'min-h-12 px-6 py-3 text-base', lg: 'min-h-11 px-5 py-2.5 text-base',
} }
const variants = { const variants = {
primary: primary:
'bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600', 'bg-brand text-brand-foreground hover:bg-brand-hover focus-visible:ring-brand',
secondary: secondary:
'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-400 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600', 'bg-surface-muted text-ink hover:bg-border focus-visible:ring-brand dark:hover:bg-surface-muted/80',
danger: danger:
'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500 dark:bg-red-500 dark:hover:bg-red-600', 'bg-severity-critical text-white hover:bg-severity-critical-dark focus-visible:ring-severity-critical',
ghost: ghost:
'bg-transparent text-gray-700 hover:bg-gray-100 focus-visible:ring-gray-400 dark:text-gray-300 dark:hover:bg-gray-800', 'bg-transparent text-ink-secondary hover:bg-surface-muted focus-visible:ring-brand',
} }
return twMerge(base, sizes[props.size], variants[props.variant], attrs.class) return twMerge(base, sizes[props.size], variants[props.variant], attrs.class)
}) })
@@ -12,12 +12,12 @@ const props = defineProps({
const classes = computed(() => { const classes = computed(() => {
const base = const base =
'rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900' 'rounded-md border border-border bg-surface-raised'
const paddings = { const paddings = {
none: '', none: '',
sm: 'p-4', sm: 'p-3',
md: 'p-4', md: 'p-3',
lg: 'p-8', lg: 'p-4',
} }
return twMerge(base, paddings[props.padding]) return twMerge(base, paddings[props.padding])
}) })
@@ -21,7 +21,7 @@ function toggle() {
<button <button
:id="headerId" :id="headerId"
type="button" type="button"
class="flex min-h-11 w-full items-center justify-between gap-4 rounded-lg border border-gray-200 bg-white px-4 py-3 text-left text-sm font-semibold text-gray-900 transition hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-900 dark:text-white dark:hover:bg-gray-800" class="flex min-h-11 w-full items-center justify-between gap-4 rounded-lg border border-gray-200 bg-white px-4 py-3 text-left text-sm font-semibold text-gray-900 transition hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-700 dark:bg-gray-900 dark:text-white dark:hover:bg-gray-800"
:aria-expanded="open" :aria-expanded="open"
:aria-controls="panelId" :aria-controls="panelId"
@click="toggle" @click="toggle"
@@ -5,11 +5,11 @@ defineProps({
</script> </script>
<template> <template>
<div class="flex flex-col items-center justify-center py-12 text-center"> <div class="flex flex-col items-center justify-center py-10 text-center">
<div class="mb-4 text-gray-400 dark:text-gray-500"> <div class="mb-3 text-ink-muted">
<slot name="icon"> <slot name="icon">
<svg <svg
class="mx-auto h-12 w-12" class="mx-auto h-10 w-10"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -24,7 +24,7 @@ defineProps({
</svg> </svg>
</slot> </slot>
</div> </div>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ message }}</p> <p class="text-sm text-ink-muted">{{ message }}</p>
<div v-if="$slots.action" class="mt-4"> <div v-if="$slots.action" class="mt-4">
<slot name="action" /> <slot name="action" />
</div> </div>
@@ -44,9 +44,9 @@ onBeforeUnmount(() => {
aria-modal="true" aria-modal="true"
:aria-labelledby="title ? titleId : undefined" :aria-labelledby="title ? titleId : undefined"
tabindex="-1" tabindex="-1"
class="relative z-10 max-h-[calc(100svh-2rem)] w-full max-w-md overflow-y-auto rounded-lg bg-white p-8 shadow-lg dark:bg-gray-800" class="relative z-10 max-h-[calc(100svh-2rem)] w-full max-w-md overflow-y-auto rounded-md border border-border bg-surface-raised p-4 sm:p-5"
> >
<h2 v-if="title" :id="titleId" class="mb-4 text-lg font-semibold dark:text-white">{{ title }}</h2> <h2 v-if="title" :id="titleId" class="mb-3 text-lg font-semibold text-ink">{{ title }}</h2>
<slot /> <slot />
</div> </div>
</div> </div>
@@ -9,7 +9,7 @@ defineProps({
<div <div
v-for="n in rows" v-for="n in rows"
:key="n" :key="n"
class="h-12 rounded-lg bg-gray-200 dark:bg-gray-700" class="h-10 rounded-md bg-surface-muted"
/> />
</div> </div>
</template> </template>
@@ -45,80 +45,80 @@ function onKeydown(event) {
role="button" role="button"
tabindex="0" tabindex="0"
:aria-label="cardLabel" :aria-label="cardLabel"
class="cursor-pointer rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition duration-200 hover:border-gray-300 hover:shadow-md active:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-900 dark:hover:border-gray-600 dark:active:bg-gray-800" class="cursor-pointer rounded-md border border-border bg-surface-raised p-3 transition duration-200 hover:border-brand/40 active:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
@click="emit('activate')" @click="emit('activate')"
@keydown="onKeydown" @keydown="onKeydown"
> >
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> <span class="text-xs font-medium text-ink-muted">
Room {{ patientRoom(patient) }} Room {{ patientRoom(patient) }}
</span> </span>
<Badge v-if="patient.sepsisActive" variant="critical" size="xs">Sepsis</Badge> <Badge v-if="patient.sepsisActive" variant="critical" size="xs">Sepsis</Badge>
<span <span
v-if="simulated" v-if="simulated"
class="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-200" class="inline-flex items-center rounded-sm px-1.5 py-0.5 text-[10px] font-bold uppercase bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-200"
title="Simulated patient" title="Simulated patient"
> >
SIM SIM
</span> </span>
</div> </div>
<h3 class="mt-2 truncate text-base font-semibold text-gray-900 dark:text-white"> <h3 class="mt-2 truncate text-base font-semibold text-ink">
{{ patient.firstName }} {{ patient.lastName }} {{ patient.firstName }} {{ patient.lastName }}
</h3> </h3>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ patient.mrn }}</p> <p class="text-xs text-ink-muted">{{ patient.mrn }}</p>
<p v-if="patient.attendingPhysician" class="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p v-if="patient.attendingPhysician" class="mt-1 text-xs text-ink-muted">
{{ patient.attendingPhysician }} {{ patient.attendingPhysician }}
</p> </p>
</div> </div>
<Badge :variant="riskVariant">NEWS2 {{ patient.news2Score ?? '—' }}</Badge> <Badge :variant="riskVariant">NEWS2 {{ patient.news2Score ?? '—' }}</Badge>
</div> </div>
<dl class="mt-4 grid grid-cols-2 gap-4 border-t border-gray-100 pt-4 dark:border-gray-800"> <dl class="mt-3 grid grid-cols-2 gap-3 border-t border-border pt-3">
<div> <div>
<dt class="text-xs text-gray-500 dark:text-gray-400">SOFA</dt> <dt class="text-xs text-ink-muted">SOFA</dt>
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white"> <dd class="mt-1.5 text-sm font-medium text-ink">
{{ patient.sofaScore ?? '—' }} {{ patient.sofaScore ?? '—' }}
<span v-if="patient.sofaDelta != null && patient.sofaDelta > 0" class="text-xs text-amber-600 dark:text-amber-400"> <span v-if="patient.sofaDelta != null && patient.sofaDelta > 0" class="text-xs text-severity-warning">
Δ+{{ patient.sofaDelta }} Δ+{{ patient.sofaDelta }}
</span> </span>
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-gray-500 dark:text-gray-400">GCS</dt> <dt class="text-xs text-ink-muted">GCS</dt>
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white"> <dd class="mt-1.5 text-sm font-medium text-ink">
{{ patient.gcsScore ?? '—' }} {{ patient.gcsScore ?? '—' }}
<span v-if="patient.gcsClassification" class="text-xs text-gray-500 dark:text-gray-400"> <span v-if="patient.gcsClassification" class="text-xs text-ink-muted">
{{ formatGcsClassification(patient.gcsClassification) }} {{ formatGcsClassification(patient.gcsClassification) }}
</span> </span>
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-gray-500 dark:text-gray-400">qSOFA</dt> <dt class="text-xs text-ink-muted">qSOFA</dt>
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white"> <dd class="mt-1.5 text-sm font-medium text-ink">
{{ patient.qsofaScore ?? '—' }} {{ patient.qsofaScore ?? '—' }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-gray-500 dark:text-gray-400">LOS</dt> <dt class="text-xs text-ink-muted">LOS</dt>
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white"> <dd class="mt-1.5 text-sm font-medium text-ink">
{{ formatLengthOfStay(patient.admittedAt) }} {{ formatLengthOfStay(patient.admittedAt) }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-gray-500 dark:text-gray-400">Last vitals</dt> <dt class="text-xs text-ink-muted">Last vitals</dt>
<dd class="mt-2 text-sm font-medium" :class="stalenessClass(vitalsStaleness)"> <dd class="mt-1.5 text-sm font-medium" :class="stalenessClass(vitalsStaleness)">
{{ formatLastObservation(patient.lastObservationAt) }} {{ formatLastObservation(patient.lastObservationAt) }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-gray-500 dark:text-gray-400">Open alerts</dt> <dt class="text-xs text-ink-muted">Open alerts</dt>
<dd class="mt-2"> <dd class="mt-1.5">
<Badge v-if="patient.openAlertCount > 0" :variant="patient.openAlertCount > 2 ? 'critical' : 'warning'"> <Badge v-if="patient.openAlertCount > 0" :variant="patient.openAlertCount > 2 ? 'critical' : 'warning'">
{{ patient.openAlertCount }} {{ patient.openAlertCount }}
</Badge> </Badge>
<span v-else class="text-sm text-gray-400">0</span> <span v-else class="text-sm text-ink-muted">0</span>
</dd> </dd>
</div> </div>
</dl> </dl>
@@ -67,67 +67,67 @@ function onKeydown(event) {
@keydown="onKeydown" @keydown="onKeydown"
@click="emit('activate')" @click="emit('activate')"
> >
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-700 dark:text-gray-300"> <td class="whitespace-nowrap px-3 py-2.5 text-sm text-ink-secondary">
{{ patientRoom(patient) }} {{ patientRoom(patient) }}
</td> </td>
<td class="px-4 py-4"> <td class="px-3 py-2.5">
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<div class="text-sm font-medium text-gray-900 dark:text-white"> <div class="text-sm font-medium text-ink">
{{ patient.firstName }} {{ patient.lastName }} {{ patient.firstName }} {{ patient.lastName }}
</div> </div>
<span <span
v-if="simulated" v-if="simulated"
class="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-200" class="inline-flex items-center rounded-sm px-1.5 py-0.5 text-[10px] font-bold uppercase bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-200"
title="Simulated patient" title="Simulated patient"
> >
SIM SIM
</span> </span>
</div> </div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ patient.mrn }}</div> <div class="text-xs text-ink-muted">{{ patient.mrn }}</div>
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-700 dark:text-gray-300"> <td class="whitespace-nowrap px-3 py-2.5 text-sm text-ink-secondary">
{{ formatDepartment(patient.department) }} {{ formatDepartment(patient.department) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-right"> <td class="whitespace-nowrap px-3 py-2.5 text-right">
<Badge :variant="riskVariant">{{ patient.news2Score ?? '—' }}</Badge> <Badge :variant="riskVariant">{{ patient.news2Score ?? '—' }}</Badge>
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-right text-sm"> <td class="whitespace-nowrap px-3 py-2.5 text-right text-sm">
<div class="flex flex-col items-end gap-1"> <div class="flex flex-col items-end gap-1">
<span class="font-medium text-gray-900 dark:text-white">{{ patient.sofaScore ?? '—' }}</span> <span class="font-medium text-ink">{{ patient.sofaScore ?? '—' }}</span>
<Badge v-if="patient.sofaDelta != null && patient.sofaDelta > 0" :variant="sofaVariant" size="xs"> <Badge v-if="patient.sofaDelta != null && patient.sofaDelta > 0" :variant="sofaVariant" size="xs">
Δ+{{ patient.sofaDelta }} Δ+{{ patient.sofaDelta }}
</Badge> </Badge>
</div> </div>
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-right text-sm"> <td class="whitespace-nowrap px-3 py-2.5 text-right text-sm">
<div class="flex flex-col items-end gap-1"> <div class="flex flex-col items-end gap-1">
<Badge :variant="gcsVariant">{{ patient.gcsScore ?? '—' }}</Badge> <Badge :variant="gcsVariant">{{ patient.gcsScore ?? '—' }}</Badge>
<span v-if="patient.gcsClassification" class="text-xs text-gray-500 dark:text-gray-400"> <span v-if="patient.gcsClassification" class="text-xs text-ink-muted">
{{ formatGcsClassification(patient.gcsClassification) }} {{ formatGcsClassification(patient.gcsClassification) }}
</span> </span>
</div> </div>
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-right text-sm text-gray-700 dark:text-gray-300"> <td class="whitespace-nowrap px-3 py-2.5 text-right text-sm text-ink-secondary">
{{ patient.qsofaScore ?? '—' }} {{ patient.qsofaScore ?? '—' }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-700 dark:text-gray-300"> <td class="whitespace-nowrap px-3 py-2.5 text-sm text-ink-secondary">
{{ patient.attendingPhysician ?? '—' }} {{ patient.attendingPhysician ?? '—' }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-700 dark:text-gray-300"> <td class="whitespace-nowrap px-3 py-2.5 text-sm text-ink-secondary">
{{ formatLengthOfStay(patient.admittedAt) }} {{ formatLengthOfStay(patient.admittedAt) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm" :class="stalenessClass(vitalsStaleness)"> <td class="whitespace-nowrap px-3 py-2.5 text-sm" :class="stalenessClass(vitalsStaleness)">
{{ formatLastObservation(patient.lastObservationAt) }} {{ formatLastObservation(patient.lastObservationAt) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4"> <td class="whitespace-nowrap px-3 py-2.5">
<Badge v-if="patient.sepsisActive" variant="critical">Active</Badge> <Badge v-if="patient.sepsisActive" variant="critical">Active</Badge>
<span v-else class="text-sm text-gray-400">No</span> <span v-else class="text-sm text-ink-muted">No</span>
</td> </td>
<td class="whitespace-nowrap px-4 py-4"> <td class="whitespace-nowrap px-3 py-2.5">
<Badge v-if="patient.openAlertCount > 0" :variant="patient.openAlertCount > 2 ? 'critical' : 'warning'"> <Badge v-if="patient.openAlertCount > 0" :variant="patient.openAlertCount > 2 ? 'critical' : 'warning'">
{{ patient.openAlertCount }} {{ patient.openAlertCount }}
</Badge> </Badge>
<span v-else class="text-sm text-gray-400">0</span> <span v-else class="text-sm text-ink-muted">0</span>
</td> </td>
</tr> </tr>
</template> </template>
@@ -16,15 +16,15 @@ const emit = defineEmits(['sort'])
<template> <template>
<th <th
class="px-4 py-4 text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400" class="px-3 py-2.5 text-xs font-medium text-ink-muted"
:class="align === 'right' ? 'text-right' : 'text-left'" :class="align === 'right' ? 'text-right' : 'text-left'"
> >
<button <button
type="button" type="button"
class="inline-flex items-center gap-1 transition duration-200 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:hover:text-gray-200" class="inline-flex items-center gap-1 transition duration-200 hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:class="[ :class="[
align === 'right' ? 'ml-auto' : '', align === 'right' ? 'ml-auto' : '',
activeField === field ? 'text-gray-900 dark:text-white' : '', activeField === field ? 'text-ink' : '',
]" ]"
@click="emit('sort', field)" @click="emit('sort', field)"
> >
@@ -45,7 +45,7 @@ function onRowFocus(index) {
<template> <template>
<!-- Mobile: card list --> <!-- Mobile: card list -->
<div class="space-y-4 md:hidden" role="list" aria-label="Ward patients"> <div class="space-y-3 md:hidden" role="list" aria-label="Ward patients">
<PatientCard <PatientCard
v-for="patient in patients" v-for="patient in patients"
:key="patient.encounterId" :key="patient.encounterId"
@@ -55,9 +55,9 @@ function onRowFocus(index) {
</div> </div>
<!-- Desktop: scrollable table --> <!-- Desktop: scrollable table -->
<div class="hidden overflow-x-auto rounded-lg border border-gray-200 md:block dark:border-gray-700"> <div class="hidden overflow-x-auto rounded-md border border-border md:block">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700" aria-label="Ward patients"> <table class="min-w-full divide-y divide-border" aria-label="Ward patients">
<thead class="sticky top-0 bg-gray-50 dark:bg-gray-800"> <thead class="sticky top-0 bg-surface-muted">
<tr> <tr>
<SortableHeader <SortableHeader
label="Room" label="Room"
@@ -88,10 +88,10 @@ function onRowFocus(index) {
align="right" align="right"
@sort="emit('sort', $event)" @sort="emit('sort', $event)"
/> />
<th scope="col" class="px-4 py-4 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"> <th scope="col" class="px-3 py-2.5 text-right text-xs font-medium text-ink-muted">
SOFA SOFA
</th> </th>
<th scope="col" class="px-4 py-4 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"> <th scope="col" class="px-3 py-2.5 text-right text-xs font-medium text-ink-muted">
GCS GCS
</th> </th>
<SortableHeader <SortableHeader
@@ -102,13 +102,13 @@ function onRowFocus(index) {
align="right" align="right"
@sort="emit('sort', $event)" @sort="emit('sort', $event)"
/> />
<th scope="col" class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"> <th scope="col" class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">
Attending Attending
</th> </th>
<th scope="col" class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"> <th scope="col" class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">
LOS LOS
</th> </th>
<th scope="col" class="px-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"> <th scope="col" class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">
Last vitals Last vitals
</th> </th>
<SortableHeader <SortableHeader
@@ -128,7 +128,7 @@ function onRowFocus(index) {
</tr> </tr>
</thead> </thead>
<tbody <tbody
class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900" class="divide-y divide-border bg-surface-raised"
@keydown="onTableKeydown" @keydown="onTableKeydown"
> >
<PatientRow <PatientRow
@@ -136,7 +136,7 @@ function onRowFocus(index) {
:key="patient.encounterId" :key="patient.encounterId"
:patient="patient" :patient="patient"
:tabindex="focusedRowIndex === index || (focusedRowIndex < 0 && index === 0) ? 0 : -1" :tabindex="focusedRowIndex === index || (focusedRowIndex < 0 && index === 0) ? 0 : -1"
class="cursor-pointer transition duration-200 hover:bg-gray-50 focus-visible:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 dark:hover:bg-gray-800 dark:focus-visible:bg-gray-800" class="cursor-pointer transition duration-200 hover:bg-surface-muted focus-visible:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
@focus="onRowFocus(index)" @focus="onRowFocus(index)"
@activate="goToPatient(patient.encounterId)" @activate="goToPatient(patient.encounterId)"
/> />
@@ -16,14 +16,14 @@ const filterOptions = [
</script> </script>
<template> <template>
<div class="mb-4 space-y-4 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"> <div class="mb-4 space-y-3 rounded-md border border-border bg-surface-raised p-3">
<label class="block"> <label class="block">
<span class="sr-only">Search patients</span> <span class="sr-only">Search patients</span>
<input <input
:value="searchInput" :value="searchInput"
type="search" type="search"
placeholder="Search by name or MRN…" placeholder="Search by name or MRN…"
class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" class="w-full rounded-md border border-border bg-surface-raised px-3 py-2 text-sm text-ink placeholder:text-ink-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
@input="wardStore.setSearchInput($event.target.value)" @input="wardStore.setSearchInput($event.target.value)"
> >
</label> </label>
@@ -11,8 +11,8 @@ export const TIMELINE_EVENT_TYPES = [
export function timelineEventStyles(type) { export function timelineEventStyles(type) {
const styles = { const styles = {
observation: { observation: {
dot: 'bg-blue-500', dot: 'bg-brand',
border: 'border-blue-500', border: 'border-brand',
badge: 'info', badge: 'info',
}, },
alert: { alert: {
@@ -2,16 +2,16 @@ import { computed, ref, onMounted, onUnmounted } from 'vue'
const THEMES = { const THEMES = {
light: { light: {
text: '#6b7280', text: '#64748b',
grid: 'rgba(0, 0, 0, 0.06)', grid: 'rgba(15, 23, 42, 0.08)',
title: '#374151', title: '#334155',
accentLine: '#111827', accentLine: '#0f172a',
}, },
dark: { dark: {
text: '#9ca3af', text: '#94a3b8',
grid: 'rgba(255, 255, 255, 0.08)', grid: 'rgba(241, 245, 249, 0.08)',
title: '#d1d5db', title: '#cbd5e1',
accentLine: '#e5e7eb', accentLine: '#e2e8f0',
}, },
} }
+77 -1
View File
@@ -1,6 +1,79 @@
@import "tailwindcss"; @import "tailwindcss";
/* 8px spacing rhythm — use Tailwind scale steps 2, 4, 6, 8, 12, 16 (8, 16, 24, 32, 48, 64 px) */ @custom-variant dark (&:where(.dark, .dark *));
/* Clinical surface tokens — swap under .dark */
:root {
--surface: #f1f5f9;
--surface-raised: #ffffff;
--surface-muted: #e2e8f0;
--border-default: #cbd5e1;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #64748b;
--brand-muted-fill: #ccfbf1;
--brand-muted-text: #0f766e;
}
.dark {
--surface: #0f172a;
--surface-raised: #1e293b;
--surface-muted: #334155;
--border-default: #334155;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--brand-muted-fill: rgba(15, 118, 110, 0.25);
--brand-muted-text: #5eead4;
}
@theme {
--font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol";
/* Brand accent (teal) */
--color-brand: #0f766e;
--color-brand-hover: #0d9488;
--color-brand-muted: #ccfbf1;
--color-brand-foreground: #ffffff;
--color-brand-soft: var(--brand-muted-fill);
--color-brand-soft-text: var(--brand-muted-text);
/* Surfaces */
--color-surface: var(--surface);
--color-surface-raised: var(--surface-raised);
--color-surface-muted: var(--surface-muted);
--color-border: var(--border-default);
/* Semantic text helpers */
--color-ink: var(--text-primary);
--color-ink-secondary: var(--text-secondary);
--color-ink-muted: var(--text-muted);
/* Severity (clinical meaning preserved) */
--color-severity-critical: #dc2626;
--color-severity-critical-light: #fef2f2;
--color-severity-critical-dark: #991b1b;
--color-severity-warning: #d97706;
--color-severity-warning-light: #fffbeb;
--color-severity-warning-dark: #92400e;
--color-severity-info: #0369a1;
--color-severity-info-light: #e0f2fe;
--color-severity-info-dark: #075985;
/* Status */
--color-status-open: #ef4444;
--color-status-acknowledged: #d97706;
--color-status-resolved: #16a34a;
--color-status-escalated: #b45309;
/* Risk */
--color-risk-low: #16a34a;
--color-risk-medium: #d97706;
--color-risk-high: #ef4444;
}
/* 8px spacing rhythm — use Tailwind scale steps 2, 4, 6, 8, 12, 16 */
@layer base { @layer base {
html { html {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -10,6 +83,9 @@
body { body {
margin: 0; margin: 0;
min-height: 100svh; min-height: 100svh;
font-family: var(--font-sans);
background-color: var(--surface);
color: var(--text-primary);
} }
#app { #app {
@@ -102,7 +102,7 @@ function pct(rate) {
Scenario Scenario
</span> </span>
<select <select
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
:value="selectedScenarioId ?? ''" :value="selectedScenarioId ?? ''"
@change="changeScenario" @change="changeScenario"
> >
@@ -148,7 +148,7 @@ function pct(rate) {
</Card> </Card>
<Card> <Card>
<div class="text-center"> <div class="text-center">
<div class="text-3xl font-bold text-blue-600">{{ summaryStats.acknowledgementRate }}%</div> <div class="text-3xl font-bold text-brand">{{ summaryStats.acknowledgementRate }}%</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Acknowledgement Rate</div> <div class="text-sm text-gray-500 dark:text-gray-400">Acknowledgement Rate</div>
</div> </div>
</Card> </Card>
@@ -118,7 +118,7 @@ onMounted(load)
type="button" type="button"
class="border-b-2 px-4 py-2 text-sm font-medium transition" class="border-b-2 px-4 py-2 text-sm font-medium transition"
:class="activeTab === 'audit' :class="activeTab === 'audit'
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400' ? 'border-brand text-brand dark:border-brand-hover dark:text-brand-hover'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
@click="switchTab('audit')" @click="switchTab('audit')"
> >
@@ -128,7 +128,7 @@ onMounted(load)
type="button" type="button"
class="border-b-2 px-4 py-2 text-sm font-medium transition" class="border-b-2 px-4 py-2 text-sm font-medium transition"
:class="activeTab === 'phi' :class="activeTab === 'phi'
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400' ? 'border-brand text-brand dark:border-brand-hover dark:text-brand-hover'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
@click="switchTab('phi')" @click="switchTab('phi')"
> >
@@ -4,7 +4,6 @@ import { useRouter } from 'vue-router'
import { useDepartmentsStore } from '@/stores/departments' import { useDepartmentsStore } from '@/stores/departments'
import { usePolling } from '@/composables/usePolling' import { usePolling } from '@/composables/usePolling'
import DepartmentCard from '@/components/departments/DepartmentCard.vue' import DepartmentCard from '@/components/departments/DepartmentCard.vue'
import Card from '@/components/ui/Card.vue'
import Badge from '@/components/ui/Badge.vue' import Badge from '@/components/ui/Badge.vue'
import Skeleton from '@/components/ui/Skeleton.vue' import Skeleton from '@/components/ui/Skeleton.vue'
@@ -20,13 +19,32 @@ function onSelectDepartment(filterValue) {
</script> </script>
<template> <template>
<div class="space-y-8"> <div class="space-y-4">
<div class="flex flex-wrap items-center justify-between gap-4"> <div class="flex flex-wrap items-center justify-between gap-3">
<div> <div
<h1 class="text-xl font-bold dark:text-white">Department Overview</h1> class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-ink-secondary"
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400"> role="group"
Unit-level snapshot across active patients, acuity, alerts, and sepsis bundles. aria-label="Department summary"
</p> >
<div class="flex items-baseline gap-1.5">
<span class="text-base font-semibold text-ink">{{ totals.patientCount }}</span>
<span class="text-ink-muted">patients</span>
</div>
<span class="hidden text-border sm:inline" aria-hidden="true">|</span>
<div class="flex items-baseline gap-1.5">
<span class="text-base font-semibold text-severity-critical">{{ totals.criticalCount }}</span>
<span class="text-ink-muted">critical</span>
</div>
<span class="hidden text-border sm:inline" aria-hidden="true">|</span>
<div class="flex items-baseline gap-1.5">
<span class="text-base font-semibold text-severity-warning">{{ totals.openAlertCount }}</span>
<span class="text-ink-muted">open alerts</span>
</div>
<span class="hidden text-border sm:inline" aria-hidden="true">|</span>
<div class="flex items-baseline gap-1.5">
<span class="text-base font-semibold text-brand">{{ totals.activeBundleCount }}</span>
<span class="text-ink-muted">sepsis bundles</span>
</div>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<Badge v-if="totals.criticalCount > 0" variant="critical"> <Badge v-if="totals.criticalCount > 0" variant="critical">
@@ -38,37 +56,10 @@ function onSelectDepartment(filterValue) {
</div> </div>
</div> </div>
<p v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ error }}</p> <p v-if="error" class="text-sm text-severity-critical">{{ error }}</p>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<div class="text-center">
<div class="text-3xl font-bold dark:text-white">{{ totals.patientCount }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Active patients</div>
</div>
</Card>
<Card>
<div class="text-center">
<div class="text-3xl font-bold text-red-600">{{ totals.criticalCount }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Critical (NEWS2 7)</div>
</div>
</Card>
<Card>
<div class="text-center">
<div class="text-3xl font-bold text-amber-600">{{ totals.openAlertCount }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Open alerts</div>
</div>
</Card>
<Card>
<div class="text-center">
<div class="text-3xl font-bold text-blue-600">{{ totals.activeBundleCount }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">Active sepsis bundles</div>
</div>
</Card>
</div>
<Skeleton v-if="loading && departments.length === 0" :rows="3" /> <Skeleton v-if="loading && departments.length === 0" :rows="3" />
<div v-else class="grid grid-cols-1 gap-4 xl:grid-cols-3"> <div v-else class="grid grid-cols-1 gap-3 xl:grid-cols-3">
<DepartmentCard <DepartmentCard
v-for="department in departments" v-for="department in departments"
:key="department.key" :key="department.key"
@@ -42,7 +42,7 @@ async function openDetail(gateway) {
type="button" type="button"
class="rounded-lg px-4 py-2 text-sm font-medium transition duration-150" class="rounded-lg px-4 py-2 text-sm font-medium transition duration-150"
:class="store.statusFilter === tab.value :class="store.statusFilter === tab.value
? 'bg-blue-600 text-white' ? 'bg-brand text-white'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'" : 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'"
@click="store.setStatusFilter(tab.value)" @click="store.setStatusFilter(tab.value)"
> >
@@ -95,7 +95,7 @@ async function openDetail(gateway) {
v-if="drawerOpen && store.selectedGateway" v-if="drawerOpen && store.selectedGateway"
class="fixed inset-y-0 right-0 z-50 w-full max-w-md border-l border-gray-200 bg-white p-4 shadow-lg dark:border-gray-800 dark:bg-gray-900" class="fixed inset-y-0 right-0 z-50 w-full max-w-md border-l border-gray-200 bg-white p-4 shadow-lg dark:border-gray-800 dark:bg-gray-900"
> >
<button type="button" class="mb-4 text-sm text-blue-600" @click="drawerOpen = false">Close</button> <button type="button" class="mb-4 text-sm text-brand" @click="drawerOpen = false">Close</button>
<h2 class="mb-4 text-lg font-semibold">{{ store.selectedGateway.gateway.gatewayCode }}</h2> <h2 class="mb-4 text-lg font-semibold">{{ store.selectedGateway.gateway.gatewayCode }}</h2>
<h3 class="mb-2 text-sm font-medium text-gray-500">Recent sync batches</h3> <h3 class="mb-2 text-sm font-medium text-gray-500">Recent sync batches</h3>
<ul class="space-y-2"> <ul class="space-y-2">
+9 -9
View File
@@ -35,18 +35,18 @@ async function submit() {
</script> </script>
<template> <template>
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 dark:bg-gray-950"> <div class="flex min-h-screen items-center justify-center bg-surface px-4">
<form <form
class="w-full max-w-sm space-y-4 rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900" class="w-full max-w-sm space-y-4 rounded-md border border-border bg-surface-raised p-5"
@submit.prevent="submit" @submit.prevent="submit"
> >
<div> <div>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">VigilCare</h1> <h1 class="text-2xl font-semibold tracking-tight text-ink">VigilCare</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Sign in to continue</p> <p class="mt-1 text-sm text-ink-muted">Sign in to continue</p>
</div> </div>
<label class="block"> <label class="block">
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> <span class="mb-1 block text-xs font-medium text-ink-muted">
Username Username
</span> </span>
<input <input
@@ -54,12 +54,12 @@ async function submit() {
type="text" type="text"
autocomplete="username" autocomplete="username"
required required
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-950 dark:text-white" class="w-full rounded-md border border-border bg-surface-raised px-3 py-2 text-sm text-ink focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20"
/> />
</label> </label>
<label class="block"> <label class="block">
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> <span class="mb-1 block text-xs font-medium text-ink-muted">
Password Password
</span> </span>
<input <input
@@ -67,11 +67,11 @@ async function submit() {
type="password" type="password"
autocomplete="current-password" autocomplete="current-password"
required required
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-950 dark:text-white" class="w-full rounded-md border border-border bg-surface-raised px-3 py-2 text-sm text-ink focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20"
/> />
</label> </label>
<p v-if="error" class="text-sm text-red-600 dark:text-red-400" role="alert"> <p v-if="error" class="text-sm text-severity-critical" role="alert">
{{ error }} {{ error }}
</p> </p>
@@ -62,7 +62,7 @@ onMounted(load)
<input <input
v-model="includeResolved" v-model="includeResolved"
type="checkbox" type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" class="h-4 w-4 rounded border-gray-300 text-brand focus:ring-brand"
@change="onToggleResolved" @change="onToggleResolved"
> >
Include resolved Include resolved
@@ -119,7 +119,7 @@ onMounted(load)
<RouterLink <RouterLink
v-if="item.encounterId" v-if="item.encounterId"
:to="`/patients/${item.encounterId}`" :to="`/patients/${item.encounterId}`"
class="text-sm font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" class="text-sm font-medium text-brand hover:text-brand dark:text-brand-hover dark:hover:text-brand-hover"
> >
View patient View patient
</RouterLink> </RouterLink>
@@ -32,18 +32,15 @@ const summaryLine = computed(() => {
<template> <template>
<div> <div>
<div class="mb-4 flex flex-wrap items-center justify-between gap-4"> <div class="mb-4 flex flex-wrap items-center justify-between gap-3">
<div> <p class="text-sm text-ink-secondary">{{ summaryLine }}</p>
<h1 class="text-xl font-bold dark:text-white">Sepsis Bundle Board</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ summaryLine }}</p>
</div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<Badge v-if="summary.overdue > 0" variant="critical">{{ summary.overdue }} overdue</Badge> <Badge v-if="summary.overdue > 0" variant="critical">{{ summary.overdue }} overdue</Badge>
<Badge v-if="summary.at_risk > 0" variant="warning">{{ summary.at_risk }} at risk</Badge> <Badge v-if="summary.at_risk > 0" variant="warning">{{ summary.at_risk }} at risk</Badge>
</div> </div>
</div> </div>
<p v-if="error" class="mb-4 text-sm text-red-600 dark:text-red-400">{{ error }}</p> <p v-if="error" class="mb-4 text-sm text-severity-critical">{{ error }}</p>
<Skeleton v-if="loading && sortedBundles.length === 0" :rows="4" /> <Skeleton v-if="loading && sortedBundles.length === 0" :rows="4" />
<EmptyState v-else-if="sortedBundles.length === 0" message="No active sepsis bundles" /> <EmptyState v-else-if="sortedBundles.length === 0" message="No active sepsis bundles" />
@@ -259,9 +259,9 @@ onBeforeUnmount(() => {
v-for="opt in availableSpeeds" v-for="opt in availableSpeeds"
:key="opt.label" :key="opt.label"
type="button" type="button"
class="min-h-11 rounded-md px-4 py-2 text-left text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" class="min-h-11 rounded-md px-4 py-2 text-left text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
:class="speed === opt.multiplier :class="speed === opt.multiplier
? 'bg-blue-600 text-white dark:bg-blue-500' ? 'bg-brand text-white dark:bg-brand'
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800'" : 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800'"
:aria-pressed="speed === opt.multiplier" :aria-pressed="speed === opt.multiplier"
@click="speed = opt.multiplier" @click="speed = opt.multiplier"
@@ -269,7 +269,7 @@ onBeforeUnmount(() => {
<span class="font-medium">{{ opt.label }}</span> <span class="font-medium">{{ opt.label }}</span>
<span <span
class="mt-0.5 block text-xs" class="mt-0.5 block text-xs"
:class="speed === opt.multiplier ? 'text-blue-100' : 'text-gray-500 dark:text-gray-400'" :class="speed === opt.multiplier ? 'text-brand-muted' : 'text-gray-500 dark:text-gray-400'"
> >
<template v-if="activeTab === 'scenarios' && wallClockFor(opt.multiplier)"> <template v-if="activeTab === 'scenarios' && wallClockFor(opt.multiplier)">
{{ wallClockFor(opt.multiplier) }} for selected scenario {{ wallClockFor(opt.multiplier) }} for selected scenario
@@ -298,7 +298,7 @@ onBeforeUnmount(() => {
type="button" type="button"
class="border-b-2 px-4 py-2 text-sm font-medium transition" class="border-b-2 px-4 py-2 text-sm font-medium transition"
:class="activeTab === 'sessions' :class="activeTab === 'sessions'
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400' ? 'border-brand text-brand dark:border-brand-hover dark:text-brand-hover'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
:aria-selected="activeTab === 'sessions'" :aria-selected="activeTab === 'sessions'"
@click="switchTab('sessions')" @click="switchTab('sessions')"
@@ -309,7 +309,7 @@ onBeforeUnmount(() => {
type="button" type="button"
class="border-b-2 px-4 py-2 text-sm font-medium transition" class="border-b-2 px-4 py-2 text-sm font-medium transition"
:class="activeTab === 'scenarios' :class="activeTab === 'scenarios'
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400' ? 'border-brand text-brand dark:border-brand-hover dark:text-brand-hover'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'" : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
:aria-selected="activeTab === 'scenarios'" :aria-selected="activeTab === 'scenarios'"
@click="switchTab('scenarios')" @click="switchTab('scenarios')"
@@ -371,7 +371,7 @@ onBeforeUnmount(() => {
v-model="searchQuery" v-model="searchQuery"
type="search" type="search"
placeholder="Search scenarios by name, tag, or department…" placeholder="Search scenarios by name, tag, or department…"
class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
> >
</label> </label>
@@ -53,20 +53,19 @@ function onMobileSortChange(event) {
<template> <template>
<div> <div>
<div class="mb-4 flex flex-wrap items-center justify-between gap-4"> <div class="mb-3 flex flex-wrap items-center justify-end gap-3">
<h1 class="text-xl font-bold dark:text-white">Virtual Ward</h1>
<Badge v-if="criticalCount > 0" variant="critical"> <Badge v-if="criticalCount > 0" variant="critical">
{{ criticalCount }} critical {{ criticalCount }} critical
</Badge> </Badge>
</div> </div>
<label class="mb-4 block sm:hidden"> <label class="mb-3 block sm:hidden">
<span class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> <span class="mb-1.5 block text-xs font-medium text-ink-muted">
Department Department
</span> </span>
<select <select
:value="department ?? ''" :value="department ?? ''"
class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200" class="w-full rounded-md border border-border bg-surface-raised px-3 py-2 text-sm text-ink-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
@change="onDepartmentChange" @change="onDepartmentChange"
> >
<option v-for="dept in departments" :key="dept.value" :value="dept.value"> <option v-for="dept in departments" :key="dept.value" :value="dept.value">
@@ -77,13 +76,13 @@ function onMobileSortChange(event) {
<WardToolbar @export-handoff="showHandoff = true" /> <WardToolbar @export-handoff="showHandoff = true" />
<label class="mb-4 block md:hidden"> <label class="mb-3 block md:hidden">
<span class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"> <span class="mb-1.5 block text-xs font-medium text-ink-muted">
Sort by Sort by
</span> </span>
<select <select
:value="wardSortField" :value="wardSortField"
class="w-full rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200" class="w-full rounded-md border border-border bg-surface-raised px-3 py-2 text-sm text-ink-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
@change="onMobileSortChange" @change="onMobileSortChange"
> >
<option v-for="option in WARD_SORT_FIELDS" :key="option.key" :value="option.key"> <option v-for="option in WARD_SORT_FIELDS" :key="option.key" :value="option.key">
+4 -22
View File
@@ -1,26 +1,8 @@
/**
* Legacy theme file kept for tooling that still reads it.
* Source of truth for colors/fonts is @theme in src/style.css (Tailwind v4).
*/
export default { export default {
content: ['./index.html', './src/**/*.{vue,js,ts}'], content: ['./index.html', './src/**/*.{vue,js,ts}'],
darkMode: 'class', darkMode: 'class',
theme: {
extend: {
colors: {
severity: {
critical: { DEFAULT: '#dc2626', light: '#fef2f2', dark: '#991b1b' },
warning: { DEFAULT: '#f59e0b', light: '#fffbeb', dark: '#92400e' },
info: { DEFAULT: '#3b82f6', light: '#eff6ff', dark: '#1e40af' },
},
status: {
open: '#ef4444',
acknowledged: '#f59e0b',
resolved: '#22c55e',
escalated: '#7c3aed',
},
risk: {
low: '#22c55e',
medium: '#f59e0b',
high: '#ef4444',
},
},
},
},
} }