Refractor. Update styling
This commit is contained in:
@@ -12,7 +12,7 @@ public sealed class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbConte
|
||||
{
|
||||
var connectionString =
|
||||
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>()
|
||||
.UseNpgsql(connectionString)
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<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>
|
||||
<script>
|
||||
;(function () {
|
||||
|
||||
@@ -9,8 +9,9 @@ describe('Badge', () => {
|
||||
slots: { default: 'High' },
|
||||
})
|
||||
const classes = wrapper.classes().join(' ')
|
||||
expect(classes).toContain('bg-severity-critical/10')
|
||||
expect(classes).toContain('text-severity-critical')
|
||||
expect(classes).toContain('bg-severity-critical')
|
||||
expect(classes).toContain('text-white')
|
||||
expect(classes).toContain('rounded-sm')
|
||||
})
|
||||
|
||||
it('rendersWarningVariant', () => {
|
||||
@@ -19,8 +20,8 @@ describe('Badge', () => {
|
||||
slots: { default: 'Med' },
|
||||
})
|
||||
const classes = wrapper.classes().join(' ')
|
||||
expect(classes).toContain('bg-severity-warning/10')
|
||||
expect(classes).toContain('text-severity-warning')
|
||||
expect(classes).toContain('bg-severity-warning')
|
||||
expect(classes).toContain('text-white')
|
||||
})
|
||||
|
||||
it('rendersSlotContent', () => {
|
||||
|
||||
@@ -47,8 +47,9 @@ describe('DepartmentOverviewView', () => {
|
||||
const wrapper = mount(DepartmentOverviewView)
|
||||
await vi.waitFor(() => expect(wrapper.text()).toContain('1 active patient'))
|
||||
|
||||
expect(wrapper.text()).toContain('Department Overview')
|
||||
expect(wrapper.text()).toContain('Critical (NEWS2 ≥ 7)')
|
||||
expect(wrapper.text()).toContain('patients')
|
||||
expect(wrapper.text()).toContain('critical')
|
||||
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 } })
|
||||
expect(wrapper.text()).toContain('Penicillin, Latex')
|
||||
const strip = wrapper.find('[role="status"]')
|
||||
expect(strip.classes().join(' ')).toMatch(/bg-red-600/)
|
||||
expect(strip.classes().join(' ')).toMatch(/bg-severity-critical/)
|
||||
})
|
||||
|
||||
it('showsSimChipWhenPatientIsSimulated', () => {
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('SepsisBoardView', () => {
|
||||
setActivePinia(createPinia())
|
||||
const wrapper = mount(SepsisBoardView)
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('WardTable', () => {
|
||||
it('showsCriticalBadgeForHighNews2', () => {
|
||||
const wrapper = mount(WardTable, { props: { patients, ...sortProps } })
|
||||
const highRiskRow = wrapper.findAll('tbody tr')[2]
|
||||
expect(highRiskRow.html()).toContain('text-severity-critical')
|
||||
expect(highRiskRow.html()).toContain('bg-severity-critical')
|
||||
})
|
||||
|
||||
it('showsExtendedClinicalColumns', () => {
|
||||
|
||||
@@ -67,7 +67,7 @@ function submit() {
|
||||
:type="field.type"
|
||||
:step="field.step"
|
||||
: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="[
|
||||
showError(field.key) ? 'border-red-500' : 'border-gray-300',
|
||||
readOnlyCode && field.key === 'observationCode' ? 'bg-gray-100 dark:bg-gray-700' : '',
|
||||
|
||||
@@ -62,7 +62,7 @@ function submit() {
|
||||
v-model="values.username"
|
||||
type="text"
|
||||
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'"
|
||||
@blur="onBlur('username')"
|
||||
>
|
||||
@@ -79,7 +79,7 @@ function submit() {
|
||||
v-model="values.password"
|
||||
type="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'"
|
||||
@blur="onBlur('password')"
|
||||
>
|
||||
@@ -95,7 +95,7 @@ function submit() {
|
||||
<input
|
||||
v-model="values.displayName"
|
||||
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'"
|
||||
@blur="onBlur('displayName')"
|
||||
>
|
||||
@@ -110,7 +110,7 @@ function submit() {
|
||||
</label>
|
||||
<select
|
||||
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'"
|
||||
@blur="onBlur('role')"
|
||||
>
|
||||
@@ -128,7 +128,7 @@ function submit() {
|
||||
id="user-active"
|
||||
v-model="values.isActive"
|
||||
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">
|
||||
Account active
|
||||
|
||||
@@ -73,7 +73,7 @@ function onConfirm() {
|
||||
<textarea
|
||||
v-model="note"
|
||||
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)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Card from '@/components/ui/Card.vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
import SeverityBadge from '@/components/ui/SeverityBadge.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import FeedbackButtons from '@/components/feedback/FeedbackButtons.vue'
|
||||
@@ -41,29 +40,33 @@ function formatTime(iso) {
|
||||
if (!iso) return ''
|
||||
return new Date(iso).toLocaleString()
|
||||
}
|
||||
|
||||
const statusLabel = computed(() => props.alert.status?.replaceAll('_', ' ') ?? '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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="flex flex-wrap items-center gap-2">
|
||||
<SeverityBadge :severity="alert.severity" />
|
||||
<Badge variant="info" size="xs">{{ alert.status }}</Badge>
|
||||
<Badge v-if="actionHint" variant="info" size="xs">{{ actionHint }}</Badge>
|
||||
<span class="text-xs font-medium text-ink-muted">{{ statusLabel }}</span>
|
||||
</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) }}
|
||||
</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) }}
|
||||
</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) }}
|
||||
</p>
|
||||
<p
|
||||
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) }}
|
||||
<span v-if="alert.acknowledgedAt">at {{ formatTime(alert.acknowledgedAt) }}</span>
|
||||
@@ -94,7 +97,7 @@ function formatTime(iso) {
|
||||
|
||||
<div
|
||||
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
|
||||
:alert-id="alert.id"
|
||||
@@ -104,4 +107,4 @@ function formatTime(iso) {
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -10,16 +10,16 @@ const tabs = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="-mx-4 overflow-x-auto px-4 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="-mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0">
|
||||
<div class="flex min-w-max gap-1 border-b border-border sm:min-w-0 sm:flex-wrap">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
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
|
||||
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
? 'border-brand text-brand'
|
||||
: 'border-transparent text-ink-muted hover:text-ink-secondary'"
|
||||
@click="activeFilter = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
|
||||
@@ -200,7 +200,7 @@ function formatMed(med) {
|
||||
|
||||
<p
|
||||
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 }}
|
||||
</p>
|
||||
|
||||
@@ -21,18 +21,22 @@ function onSelect() {
|
||||
<template>
|
||||
<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="hasPatients ? 'cursor-pointer hover:-translate-y-0.5' : 'cursor-default opacity-80'"
|
||||
class="w-full text-left transition duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||
:class="hasPatients ? 'cursor-pointer' : 'cursor-default opacity-80'"
|
||||
:disabled="!hasPatients"
|
||||
@click="onSelect"
|
||||
>
|
||||
<Card padding="lg" class="h-full">
|
||||
<div class="mb-4 flex items-start justify-between gap-4">
|
||||
<Card
|
||||
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>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<h2 class="text-base font-semibold text-ink">
|
||||
{{ department.label }}
|
||||
</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' }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -41,44 +45,44 @@ function onSelect() {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<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
|
||||
</p>
|
||||
<AcuityBar :acuity="department.acuity" />
|
||||
</div>
|
||||
|
||||
<dl class="grid grid-cols-2 gap-4 text-sm">
|
||||
<dl class="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Avg NEWS2</dt>
|
||||
<dd class="mt-1 font-semibold text-gray-900 dark:text-white">
|
||||
<dt class="text-ink-muted">Avg NEWS2</dt>
|
||||
<dd class="mt-0.5 font-semibold text-ink">
|
||||
{{ department.averageNews2 ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Open alerts</dt>
|
||||
<dd class="mt-1 font-semibold text-gray-900 dark:text-white">
|
||||
<dt class="text-ink-muted">Open alerts</dt>
|
||||
<dd class="mt-0.5 font-semibold text-ink">
|
||||
{{ department.openAlertCount }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Sepsis bundles</dt>
|
||||
<dd class="mt-1 font-semibold text-gray-900 dark:text-white">
|
||||
<dt class="text-ink-muted">Sepsis bundles</dt>
|
||||
<dd class="mt-0.5 font-semibold text-ink">
|
||||
{{ department.activeBundleCount }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Alert volume</dt>
|
||||
<dd class="mt-1 font-semibold text-gray-900 dark:text-white">
|
||||
<dt class="text-ink-muted">Alert volume</dt>
|
||||
<dd class="mt-0.5 font-semibold text-ink">
|
||||
{{ department.alertVolume }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<p v-if="hasPatients" class="mt-4 text-xs font-medium text-blue-600 dark:text-blue-400">
|
||||
View patients in Virtual Ward
|
||||
<p v-if="hasPatients" class="mt-3 text-xs font-medium text-brand">
|
||||
Open in Virtual Ward
|
||||
</p>
|
||||
</Card>
|
||||
</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-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: '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' },
|
||||
]
|
||||
|
||||
@@ -69,7 +69,7 @@ async function submitNotes() {
|
||||
role="radio"
|
||||
:aria-checked="selectedRating === r.value"
|
||||
: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="[
|
||||
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' : '',
|
||||
@@ -81,7 +81,7 @@ async function submitNotes() {
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
+ Note
|
||||
@@ -100,7 +100,7 @@ async function submitNotes() {
|
||||
@keydown.enter="submitNotes"
|
||||
/>
|
||||
<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"
|
||||
@click.stop="submitNotes"
|
||||
>
|
||||
|
||||
@@ -31,23 +31,20 @@ function onDepartmentChange(event) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="sticky top-0 z-30 border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="flex h-16 items-center gap-4 px-4 lg:px-8">
|
||||
<header class="sticky top-0 z-30 border-b border-border bg-surface-raised">
|
||||
<div class="flex h-14 items-center gap-3 px-3 lg:px-5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
VigilCare Clinical
|
||||
</p>
|
||||
<h1 class="truncate text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h1 class="truncate text-base font-semibold text-ink">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
</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">
|
||||
<span class="sr-only">Department filter</span>
|
||||
<select
|
||||
: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"
|
||||
>
|
||||
<option v-for="dept in departments" :key="dept.value" :value="dept.value">
|
||||
@@ -58,13 +55,13 @@ function onDepartmentChange(event) {
|
||||
|
||||
<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'"
|
||||
@click="settingsStore.toggleAlertSoundMute()"
|
||||
>
|
||||
<svg
|
||||
v-if="alertSoundMuted"
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -85,7 +82,7 @@ function onDepartmentChange(event) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -102,13 +99,13 @@ function onDepartmentChange(event) {
|
||||
|
||||
<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'"
|
||||
@click="toggle"
|
||||
>
|
||||
<svg
|
||||
v-if="darkMode"
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -123,7 +120,7 @@ function onDepartmentChange(event) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -138,13 +135,13 @@ function onDepartmentChange(event) {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="hidden items-center gap-3 border-l border-gray-200 pl-4 dark:border-gray-700 sm:flex">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<div class="hidden items-center gap-3 border-l border-border pl-3 sm:flex">
|
||||
<span class="text-sm font-medium text-ink-secondary">
|
||||
{{ authStore.displayName }}
|
||||
</span>
|
||||
<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"
|
||||
@click="authStore.logout()"
|
||||
>
|
||||
|
||||
@@ -31,17 +31,17 @@ watch(() => authStore.isAuthenticated, (ok) => {
|
||||
<template>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
<div class="flex min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||
<div class="flex min-h-screen bg-surface">
|
||||
<AppSidebar />
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<AppHeader />
|
||||
<SimulationModeBanner />
|
||||
<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">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -10,27 +10,27 @@ const { mainNavLinks, adminNavLinks, showAdminSection } = useRoleAccess()
|
||||
function linkClasses(path) {
|
||||
const active = route.path.startsWith(path)
|
||||
return active
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
? 'bg-brand-soft text-brand-soft-text'
|
||||
: 'text-ink-secondary hover:bg-surface-muted'
|
||||
}
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<div class="flex h-16 items-center px-8">
|
||||
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">VigilCare</span>
|
||||
<aside class="hidden w-60 shrink-0 border-r border-border bg-surface-raised lg:flex lg:flex-col">
|
||||
<div class="flex h-14 items-center px-4">
|
||||
<span class="text-base font-bold text-brand">VigilCare</span>
|
||||
</div>
|
||||
<nav class="flex-1 px-4 py-4" aria-label="Main navigation">
|
||||
<ul class="space-y-2">
|
||||
<nav class="flex-1 px-2 py-3" aria-label="Main navigation">
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="link in mainNavLinks" :key="link.to">
|
||||
<RouterLink
|
||||
: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)"
|
||||
>
|
||||
<svg
|
||||
v-if="link.icon === 'ward'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -45,7 +45,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'sepsis'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -60,7 +60,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'alerts'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -75,7 +75,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'departments'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -90,7 +90,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'reconciliation'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -105,7 +105,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'ops'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -120,7 +120,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'analytics'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -135,7 +135,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'simulation'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -156,7 +156,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -174,20 +174,20 @@ function linkClasses(path) {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="showAdminSection" class="mt-8">
|
||||
<p class="mb-2 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<div v-if="showAdminSection" class="mt-6">
|
||||
<p class="mb-1.5 px-3 text-xs font-medium text-ink-muted">
|
||||
Admin
|
||||
</p>
|
||||
<ul class="space-y-2">
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="link in adminNavLinks" :key="link.to">
|
||||
<RouterLink
|
||||
: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)"
|
||||
>
|
||||
<svg
|
||||
v-if="link.icon === 'users'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -202,7 +202,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'audit'"
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -217,7 +217,7 @@ function linkClasses(path) {
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -242,21 +242,21 @@ function linkClasses(path) {
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="border-t border-gray-200 px-4 py-4 dark:border-gray-800">
|
||||
<div class="mb-2 px-4">
|
||||
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<div class="border-t border-border px-2 py-3">
|
||||
<div class="mb-1.5 px-3">
|
||||
<p class="truncate text-sm font-medium text-ink">
|
||||
{{ authStore.displayName }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
<p class="truncate text-xs text-ink-muted">
|
||||
{{ authStore.role }}
|
||||
</p>
|
||||
</div>
|
||||
<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()"
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
Sign out
|
||||
|
||||
@@ -27,18 +27,18 @@ function closeOverflow() {
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<div v-if="overflowOpen && mobileOverflowLinks.length" class="border-b border-gray-200 dark:border-gray-800">
|
||||
<ul class="flex flex-col py-2">
|
||||
<div v-if="overflowOpen && mobileOverflowLinks.length" class="border-b border-border">
|
||||
<ul class="flex flex-col py-1.5">
|
||||
<li v-for="link in mobileOverflowLinks" :key="link.to">
|
||||
<RouterLink
|
||||
: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)
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-700 dark:text-gray-300'"
|
||||
? 'text-brand'
|
||||
: 'text-ink-secondary'"
|
||||
@click="closeOverflow"
|
||||
>
|
||||
{{ link.label }}
|
||||
@@ -47,19 +47,19 @@ function closeOverflow() {
|
||||
</ul>
|
||||
</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">
|
||||
<RouterLink
|
||||
: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)
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
? 'text-brand'
|
||||
: 'text-ink-muted hover:text-ink-secondary'"
|
||||
@click="closeOverflow"
|
||||
>
|
||||
<svg
|
||||
v-if="link.icon === 'ward'"
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -74,7 +74,7 @@ function closeOverflow() {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'sepsis'"
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -89,7 +89,7 @@ function closeOverflow() {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'reconciliation'"
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -104,7 +104,7 @@ function closeOverflow() {
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="link.icon === 'simulation'"
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -125,7 +125,7 @@ function closeOverflow() {
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -145,15 +145,15 @@ function closeOverflow() {
|
||||
<li v-if="mobileOverflowLinks.length" class="min-w-0 flex-1">
|
||||
<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))
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'"
|
||||
? 'text-brand'
|
||||
: 'text-ink-muted hover:text-ink-secondary'"
|
||||
:aria-expanded="overflowOpen"
|
||||
aria-label="More navigation"
|
||||
@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" />
|
||||
</svg>
|
||||
<span class="truncate">More</span>
|
||||
@@ -163,10 +163,10 @@ function closeOverflow() {
|
||||
<li class="min-w-0 flex-1">
|
||||
<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()"
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
<span class="truncate">Sign out</span>
|
||||
|
||||
@@ -65,8 +65,8 @@ async function resolve(alertId) {
|
||||
>
|
||||
<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="selectedId === alert.id ? 'bg-blue-50/50 dark:bg-blue-950/20' : 'hover:bg-gray-50 dark:hover:bg-gray-800/40'"
|
||||
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-brand-soft dark:bg-brand-soft' : 'hover:bg-gray-50 dark:hover:bg-gray-800/40'"
|
||||
:aria-pressed="selectedId === alert.id"
|
||||
:aria-label="`Select ${alertTypeLabel(alert.alertType)} alert, ${alert.severity} severity`"
|
||||
@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>
|
||||
<select
|
||||
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">
|
||||
{{ 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>
|
||||
<select
|
||||
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">
|
||||
{{ 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>
|
||||
<select
|
||||
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">
|
||||
{{ opt.label }}
|
||||
@@ -96,7 +96,7 @@ function submit() {
|
||||
</div>
|
||||
<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"
|
||||
@click="submit"
|
||||
>
|
||||
|
||||
@@ -34,12 +34,12 @@ const emergencyContact = computed(() => {
|
||||
</script>
|
||||
|
||||
<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
|
||||
class="px-4 py-2 text-sm font-medium"
|
||||
class="px-3 py-1.5 text-sm font-medium"
|
||||
:class="allergiesKnown
|
||||
? 'bg-red-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'"
|
||||
? 'bg-severity-critical text-white'
|
||||
: 'bg-surface-muted text-ink-secondary'"
|
||||
role="status"
|
||||
:aria-label="allergiesKnown ? `Known allergies: ${allergyText}` : 'No known drug allergies'"
|
||||
>
|
||||
@@ -47,69 +47,69 @@ const emergencyContact = computed(() => {
|
||||
{{ allergyText }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 bg-white p-4 dark:bg-gray-900">
|
||||
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">
|
||||
<div class="space-y-2.5 bg-surface-raised p-3">
|
||||
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<h1 class="text-lg font-semibold text-ink">
|
||||
{{ fullName }}
|
||||
</h1>
|
||||
<span
|
||||
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"
|
||||
>
|
||||
SIM
|
||||
</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span class="text-sm text-ink-muted">
|
||||
MRN {{ patient.mrn ?? '—' }}
|
||||
</span>
|
||||
</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>
|
||||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">DOB / Age</dt>
|
||||
<dd class="mt-0.5 text-gray-900 dark:text-gray-100">
|
||||
<dt class="text-xs font-medium text-ink-muted">DOB / Age</dt>
|
||||
<dd class="mt-0.5 text-ink">
|
||||
{{ formatDobWithAge(patient.dateOfBirth) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Gender</dt>
|
||||
<dd class="mt-0.5 text-gray-900 dark:text-gray-100">
|
||||
<dt class="text-xs font-medium text-ink-muted">Gender</dt>
|
||||
<dd class="mt-0.5 text-ink">
|
||||
{{ formatGender(patient.gender) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Blood type</dt>
|
||||
<dd class="mt-0.5 text-gray-900 dark:text-gray-100">
|
||||
<dt class="text-xs font-medium text-ink-muted">Blood type</dt>
|
||||
<dd class="mt-0.5 text-ink">
|
||||
{{ formatBloodType(patient.bloodType) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Room / Bed</dt>
|
||||
<dd class="mt-0.5 text-gray-900 dark:text-gray-100">
|
||||
<dt class="text-xs font-medium text-ink-muted">Room / Bed</dt>
|
||||
<dd class="mt-0.5 text-ink">
|
||||
{{ encounter.roomBed ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Department</dt>
|
||||
<dd class="mt-0.5 text-gray-900 dark:text-gray-100">
|
||||
<dt class="text-xs font-medium text-ink-muted">Department</dt>
|
||||
<dd class="mt-0.5 text-ink">
|
||||
{{ formatDepartment(encounter.department) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Attending</dt>
|
||||
<dd class="mt-0.5 text-gray-900 dark:text-gray-100">
|
||||
<dt class="text-xs font-medium text-ink-muted">Attending</dt>
|
||||
<dd class="mt-0.5 text-ink">
|
||||
{{ encounter.attendingPhysician ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<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>
|
||||
<dd class="mt-0.5 text-gray-900 dark:text-gray-100">
|
||||
<dt class="text-xs font-medium text-ink-muted">Admission reason</dt>
|
||||
<dd class="mt-0.5 text-ink">
|
||||
{{ encounter.admissionReason ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<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>
|
||||
<dd class="mt-0.5 text-gray-900 dark:text-gray-100">
|
||||
<dt class="text-xs font-medium text-ink-muted">Emergency contact</dt>
|
||||
<dd class="mt-0.5 text-ink">
|
||||
{{ emergencyContact }}
|
||||
</dd>
|
||||
</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>
|
||||
<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"
|
||||
>
|
||||
{{ showGcsForm ? 'Cancel' : 'Record GCS' }}
|
||||
|
||||
@@ -68,7 +68,7 @@ function organVariant(score) {
|
||||
|
||||
<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"
|
||||
>
|
||||
Refresh SOFA
|
||||
|
||||
@@ -58,7 +58,7 @@ function submit() {
|
||||
v-model="values[field.key]"
|
||||
type="number"
|
||||
: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'"
|
||||
@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>
|
||||
<select
|
||||
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'"
|
||||
@blur="onBlur('avpu')"
|
||||
>
|
||||
@@ -94,7 +94,7 @@ function submit() {
|
||||
<div class="flex justify-end">
|
||||
<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"
|
||||
@click="submit"
|
||||
>
|
||||
|
||||
@@ -53,7 +53,7 @@ async function onSubmit(observations) {
|
||||
</h2>
|
||||
<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"
|
||||
>
|
||||
{{ 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="h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<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}%` }"
|
||||
/>
|
||||
</div>
|
||||
@@ -49,9 +49,9 @@ const speedPresets = [
|
||||
v-for="preset in speedPresets"
|
||||
:key="preset.value"
|
||||
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
|
||||
? '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'"
|
||||
:aria-pressed="speed === preset.value"
|
||||
:aria-label="`Set replay speed to ${preset.label}`"
|
||||
|
||||
@@ -22,15 +22,15 @@ const outstanding = computed(() => outstandingElements(props.bundle))
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<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 }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ bundle.mrn }}</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p class="text-xs text-ink-muted">{{ bundle.mrn }}</p>
|
||||
<p class="mt-1 text-xs text-ink-muted">
|
||||
{{ formatDepartment(bundle.department) }}
|
||||
<span v-if="bundle.roomBed"> · {{ bundle.roomBed }}</span>
|
||||
</p>
|
||||
@@ -38,19 +38,19 @@ const outstanding = computed(() => outstandingElements(props.bundle))
|
||||
<Badge :variant="urgencyVariant(urgency)">{{ urgencyLabel(urgency) }}</Badge>
|
||||
</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>
|
||||
<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
|
||||
class="mt-2 font-mono text-sm font-semibold"
|
||||
:class="urgency === 'overdue' ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white'"
|
||||
class="mt-1.5 font-mono text-sm font-semibold"
|
||||
:class="urgency === 'overdue' ? 'text-severity-critical' : 'text-ink'"
|
||||
>
|
||||
{{ formatCountdown(bundle.deadlineAt, now) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">Outstanding</dt>
|
||||
<dd class="mt-2 text-sm text-gray-900 dark:text-white">
|
||||
<dt class="text-xs text-ink-muted">Outstanding</dt>
|
||||
<dd class="mt-1.5 text-sm text-ink">
|
||||
<span v-if="outstanding.length === 0">None</span>
|
||||
<ul v-else class="space-y-1">
|
||||
<li v-for="element in outstanding" :key="element.id" class="text-xs">
|
||||
|
||||
@@ -16,7 +16,7 @@ function goToPatient(encounterId) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4 md:hidden">
|
||||
<div class="space-y-3 md:hidden">
|
||||
<SepsisBundleCard
|
||||
v-for="bundle in bundles"
|
||||
:key="bundle.id"
|
||||
@@ -26,27 +26,27 @@ function goToPatient(encounterId) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="hidden overflow-x-auto rounded-lg border border-gray-200 md:block dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="sticky top-0 bg-gray-50 dark:bg-gray-800">
|
||||
<div class="hidden overflow-x-auto rounded-md border border-border md:block">
|
||||
<table class="min-w-full divide-y divide-border">
|
||||
<thead class="sticky top-0 bg-surface-muted">
|
||||
<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-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">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-4 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">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-4 py-4 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">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-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">Patient</th>
|
||||
<th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Department</th>
|
||||
<th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Room</th>
|
||||
<th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Started</th>
|
||||
<th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Deadline</th>
|
||||
<th class="px-3 py-2.5 text-right text-xs font-medium text-ink-muted">Remaining</th>
|
||||
<th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Status</th>
|
||||
<th class="px-3 py-2.5 text-left text-xs font-medium text-ink-muted">Elements</th>
|
||||
</tr>
|
||||
</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
|
||||
v-for="bundle in bundles"
|
||||
:key="bundle.id"
|
||||
:bundle="bundle"
|
||||
: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)"
|
||||
/>
|
||||
</tbody>
|
||||
|
||||
@@ -149,7 +149,7 @@ defineExpose({ closeConfirm })
|
||||
v-model="confirmText"
|
||||
type="text"
|
||||
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"
|
||||
>
|
||||
</label>
|
||||
|
||||
@@ -72,7 +72,7 @@ function onSelect() {
|
||||
padding="md"
|
||||
class="flex h-full flex-col transition"
|
||||
: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'"
|
||||
>
|
||||
<div
|
||||
@@ -100,7 +100,7 @@ function onSelect() {
|
||||
<button
|
||||
v-if="canExpand"
|
||||
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"
|
||||
>
|
||||
{{ expanded ? 'Show less' : 'Show more' }}
|
||||
|
||||
@@ -67,7 +67,7 @@ function progressWidth(run) {
|
||||
{{ runStatusLabel(run.status) }}
|
||||
</Badge>
|
||||
<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) }}
|
||||
</span>
|
||||
@@ -77,7 +77,7 @@ function progressWidth(run) {
|
||||
<template v-if="patientLink(run)">
|
||||
<RouterLink
|
||||
: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' }}
|
||||
</RouterLink>
|
||||
@@ -134,7 +134,7 @@ function progressWidth(run) {
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<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) }"
|
||||
/>
|
||||
</div>
|
||||
@@ -223,7 +223,7 @@ function progressWidth(run) {
|
||||
<RouterLink
|
||||
v-if="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' }}
|
||||
</RouterLink>
|
||||
|
||||
@@ -8,13 +8,13 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
const base = 'inline-flex items-center font-medium rounded-full'
|
||||
const sizes = { xs: 'px-2 py-2 text-xs', sm: 'px-2 py-2 text-xs', md: 'px-4 py-2 text-sm' }
|
||||
const base = 'inline-flex items-center font-semibold rounded-sm tracking-normal'
|
||||
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 = {
|
||||
critical: 'bg-severity-critical/10 text-severity-critical dark:bg-severity-critical-dark/20 dark:text-red-300',
|
||||
warning: 'bg-severity-warning/10 text-severity-warning dark:bg-severity-warning-dark/20 dark:text-amber-300',
|
||||
info: 'bg-severity-info/10 text-severity-info dark:bg-severity-info-dark/20 dark:text-blue-300',
|
||||
success: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300',
|
||||
critical: 'bg-severity-critical text-white dark:bg-severity-critical dark:text-white',
|
||||
warning: 'bg-severity-warning text-white dark:bg-severity-warning dark:text-white',
|
||||
info: 'bg-severity-info text-white dark:bg-severity-info dark:text-white',
|
||||
success: 'bg-risk-low text-white dark:bg-risk-low dark:text-white',
|
||||
}
|
||||
return twMerge(base, sizes[props.size], variants[props.variant])
|
||||
})
|
||||
@@ -22,4 +22,4 @@ const classes = computed(() => {
|
||||
|
||||
<template>
|
||||
<span :class="classes"><slot /></span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -23,21 +23,21 @@ const attrs = useAttrs()
|
||||
|
||||
const classes = computed(() => {
|
||||
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 = {
|
||||
sm: 'min-h-11 px-4 py-2 text-xs',
|
||||
md: 'min-h-11 px-4 py-2 text-sm',
|
||||
lg: 'min-h-12 px-6 py-3 text-base',
|
||||
sm: 'min-h-10 px-3 py-1.5 text-xs',
|
||||
md: 'min-h-10 px-3.5 py-2 text-sm',
|
||||
lg: 'min-h-11 px-5 py-2.5 text-base',
|
||||
}
|
||||
const variants = {
|
||||
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:
|
||||
'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:
|
||||
'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:
|
||||
'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)
|
||||
})
|
||||
|
||||
@@ -12,12 +12,12 @@ const props = defineProps({
|
||||
|
||||
const classes = computed(() => {
|
||||
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 = {
|
||||
none: '',
|
||||
sm: 'p-4',
|
||||
md: 'p-4',
|
||||
lg: 'p-8',
|
||||
sm: 'p-3',
|
||||
md: 'p-3',
|
||||
lg: 'p-4',
|
||||
}
|
||||
return twMerge(base, paddings[props.padding])
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ function toggle() {
|
||||
<button
|
||||
:id="headerId"
|
||||
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-controls="panelId"
|
||||
@click="toggle"
|
||||
|
||||
@@ -5,11 +5,11 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="mb-4 text-gray-400 dark:text-gray-500">
|
||||
<div class="flex flex-col items-center justify-center py-10 text-center">
|
||||
<div class="mb-3 text-ink-muted">
|
||||
<slot name="icon">
|
||||
<svg
|
||||
class="mx-auto h-12 w-12"
|
||||
class="mx-auto h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -24,7 +24,7 @@ defineProps({
|
||||
</svg>
|
||||
</slot>
|
||||
</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">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
|
||||
@@ -44,9 +44,9 @@ onBeforeUnmount(() => {
|
||||
aria-modal="true"
|
||||
:aria-labelledby="title ? titleId : undefined"
|
||||
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 />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ defineProps({
|
||||
<div
|
||||
v-for="n in rows"
|
||||
:key="n"
|
||||
class="h-12 rounded-lg bg-gray-200 dark:bg-gray-700"
|
||||
class="h-10 rounded-md bg-surface-muted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -45,80 +45,80 @@ function onKeydown(event) {
|
||||
role="button"
|
||||
tabindex="0"
|
||||
: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')"
|
||||
@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="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) }}
|
||||
</span>
|
||||
<Badge v-if="patient.sepsisActive" variant="critical" size="xs">Sepsis</Badge>
|
||||
<span
|
||||
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"
|
||||
>
|
||||
SIM
|
||||
</span>
|
||||
</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 }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ patient.mrn }}</p>
|
||||
<p v-if="patient.attendingPhysician" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p class="text-xs text-ink-muted">{{ patient.mrn }}</p>
|
||||
<p v-if="patient.attendingPhysician" class="mt-1 text-xs text-ink-muted">
|
||||
{{ patient.attendingPhysician }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge :variant="riskVariant">NEWS2 {{ patient.news2Score ?? '—' }}</Badge>
|
||||
</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>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">SOFA</dt>
|
||||
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
<dt class="text-xs text-ink-muted">SOFA</dt>
|
||||
<dd class="mt-1.5 text-sm font-medium text-ink">
|
||||
{{ 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 }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">GCS</dt>
|
||||
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
<dt class="text-xs text-ink-muted">GCS</dt>
|
||||
<dd class="mt-1.5 text-sm font-medium text-ink">
|
||||
{{ 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) }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">qSOFA</dt>
|
||||
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
<dt class="text-xs text-ink-muted">qSOFA</dt>
|
||||
<dd class="mt-1.5 text-sm font-medium text-ink">
|
||||
{{ patient.qsofaScore ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">LOS</dt>
|
||||
<dd class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
<dt class="text-xs text-ink-muted">LOS</dt>
|
||||
<dd class="mt-1.5 text-sm font-medium text-ink">
|
||||
{{ formatLengthOfStay(patient.admittedAt) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">Last vitals</dt>
|
||||
<dd class="mt-2 text-sm font-medium" :class="stalenessClass(vitalsStaleness)">
|
||||
<dt class="text-xs text-ink-muted">Last vitals</dt>
|
||||
<dd class="mt-1.5 text-sm font-medium" :class="stalenessClass(vitalsStaleness)">
|
||||
{{ formatLastObservation(patient.lastObservationAt) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-gray-500 dark:text-gray-400">Open alerts</dt>
|
||||
<dd class="mt-2">
|
||||
<dt class="text-xs text-ink-muted">Open alerts</dt>
|
||||
<dd class="mt-1.5">
|
||||
<Badge v-if="patient.openAlertCount > 0" :variant="patient.openAlertCount > 2 ? 'critical' : 'warning'">
|
||||
{{ patient.openAlertCount }}
|
||||
</Badge>
|
||||
<span v-else class="text-sm text-gray-400">0</span>
|
||||
<span v-else class="text-sm text-ink-muted">0</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@@ -67,67 +67,67 @@ function onKeydown(event) {
|
||||
@keydown="onKeydown"
|
||||
@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) }}
|
||||
</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="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<div class="text-sm font-medium text-ink">
|
||||
{{ patient.firstName }} {{ patient.lastName }}
|
||||
</div>
|
||||
<span
|
||||
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"
|
||||
>
|
||||
SIM
|
||||
</span>
|
||||
</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 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) }}
|
||||
</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>
|
||||
</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">
|
||||
<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">
|
||||
Δ+{{ patient.sofaDelta }}
|
||||
</Badge>
|
||||
</div>
|
||||
</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">
|
||||
<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) }}
|
||||
</span>
|
||||
</div>
|
||||
</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 ?? '—' }}
|
||||
</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 ?? '—' }}
|
||||
</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) }}
|
||||
</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) }}
|
||||
</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>
|
||||
<span v-else class="text-sm text-gray-400">No</span>
|
||||
<span v-else class="text-sm text-ink-muted">No</span>
|
||||
</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'">
|
||||
{{ patient.openAlertCount }}
|
||||
</Badge>
|
||||
<span v-else class="text-sm text-gray-400">0</span>
|
||||
<span v-else class="text-sm text-ink-muted">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@@ -16,15 +16,15 @@ const emit = defineEmits(['sort'])
|
||||
|
||||
<template>
|
||||
<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'"
|
||||
>
|
||||
<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="[
|
||||
align === 'right' ? 'ml-auto' : '',
|
||||
activeField === field ? 'text-gray-900 dark:text-white' : '',
|
||||
activeField === field ? 'text-ink' : '',
|
||||
]"
|
||||
@click="emit('sort', field)"
|
||||
>
|
||||
|
||||
@@ -45,7 +45,7 @@ function onRowFocus(index) {
|
||||
|
||||
<template>
|
||||
<!-- 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
|
||||
v-for="patient in patients"
|
||||
:key="patient.encounterId"
|
||||
@@ -55,9 +55,9 @@ function onRowFocus(index) {
|
||||
</div>
|
||||
|
||||
<!-- Desktop: scrollable table -->
|
||||
<div class="hidden overflow-x-auto rounded-lg border border-gray-200 md:block dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700" aria-label="Ward patients">
|
||||
<thead class="sticky top-0 bg-gray-50 dark:bg-gray-800">
|
||||
<div class="hidden overflow-x-auto rounded-md border border-border md:block">
|
||||
<table class="min-w-full divide-y divide-border" aria-label="Ward patients">
|
||||
<thead class="sticky top-0 bg-surface-muted">
|
||||
<tr>
|
||||
<SortableHeader
|
||||
label="Room"
|
||||
@@ -88,10 +88,10 @@ function onRowFocus(index) {
|
||||
align="right"
|
||||
@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
|
||||
</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
|
||||
</th>
|
||||
<SortableHeader
|
||||
@@ -102,13 +102,13 @@ function onRowFocus(index) {
|
||||
align="right"
|
||||
@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
|
||||
</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
|
||||
</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
|
||||
</th>
|
||||
<SortableHeader
|
||||
@@ -128,7 +128,7 @@ function onRowFocus(index) {
|
||||
</tr>
|
||||
</thead>
|
||||
<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"
|
||||
>
|
||||
<PatientRow
|
||||
@@ -136,7 +136,7 @@ function onRowFocus(index) {
|
||||
:key="patient.encounterId"
|
||||
:patient="patient"
|
||||
: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)"
|
||||
@activate="goToPatient(patient.encounterId)"
|
||||
/>
|
||||
|
||||
@@ -16,14 +16,14 @@ const filterOptions = [
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<span class="sr-only">Search patients</span>
|
||||
<input
|
||||
:value="searchInput"
|
||||
type="search"
|
||||
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)"
|
||||
>
|
||||
</label>
|
||||
|
||||
@@ -11,8 +11,8 @@ export const TIMELINE_EVENT_TYPES = [
|
||||
export function timelineEventStyles(type) {
|
||||
const styles = {
|
||||
observation: {
|
||||
dot: 'bg-blue-500',
|
||||
border: 'border-blue-500',
|
||||
dot: 'bg-brand',
|
||||
border: 'border-brand',
|
||||
badge: 'info',
|
||||
},
|
||||
alert: {
|
||||
|
||||
@@ -2,16 +2,16 @@ import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const THEMES = {
|
||||
light: {
|
||||
text: '#6b7280',
|
||||
grid: 'rgba(0, 0, 0, 0.06)',
|
||||
title: '#374151',
|
||||
accentLine: '#111827',
|
||||
text: '#64748b',
|
||||
grid: 'rgba(15, 23, 42, 0.08)',
|
||||
title: '#334155',
|
||||
accentLine: '#0f172a',
|
||||
},
|
||||
dark: {
|
||||
text: '#9ca3af',
|
||||
grid: 'rgba(255, 255, 255, 0.08)',
|
||||
title: '#d1d5db',
|
||||
accentLine: '#e5e7eb',
|
||||
text: '#94a3b8',
|
||||
grid: 'rgba(241, 245, 249, 0.08)',
|
||||
title: '#cbd5e1',
|
||||
accentLine: '#e2e8f0',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,79 @@
|
||||
@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 {
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -10,6 +83,9 @@
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100svh;
|
||||
font-family: var(--font-sans);
|
||||
background-color: var(--surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#app {
|
||||
|
||||
@@ -102,7 +102,7 @@ function pct(rate) {
|
||||
Scenario
|
||||
</span>
|
||||
<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 ?? ''"
|
||||
@change="changeScenario"
|
||||
>
|
||||
@@ -148,7 +148,7 @@ function pct(rate) {
|
||||
</Card>
|
||||
<Card>
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
@@ -118,7 +118,7 @@ onMounted(load)
|
||||
type="button"
|
||||
class="border-b-2 px-4 py-2 text-sm font-medium transition"
|
||||
: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'"
|
||||
@click="switchTab('audit')"
|
||||
>
|
||||
@@ -128,7 +128,7 @@ onMounted(load)
|
||||
type="button"
|
||||
class="border-b-2 px-4 py-2 text-sm font-medium transition"
|
||||
: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'"
|
||||
@click="switchTab('phi')"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRouter } from 'vue-router'
|
||||
import { useDepartmentsStore } from '@/stores/departments'
|
||||
import { usePolling } from '@/composables/usePolling'
|
||||
import DepartmentCard from '@/components/departments/DepartmentCard.vue'
|
||||
import Card from '@/components/ui/Card.vue'
|
||||
import Badge from '@/components/ui/Badge.vue'
|
||||
import Skeleton from '@/components/ui/Skeleton.vue'
|
||||
|
||||
@@ -20,13 +19,32 @@ function onSelectDepartment(filterValue) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-8">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold dark:text-white">Department Overview</h1>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Unit-level snapshot across active patients, acuity, alerts, and sepsis bundles.
|
||||
</p>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-ink-secondary"
|
||||
role="group"
|
||||
aria-label="Department summary"
|
||||
>
|
||||
<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 class="flex flex-wrap gap-2">
|
||||
<Badge v-if="totals.criticalCount > 0" variant="critical">
|
||||
@@ -38,37 +56,10 @@ function onSelectDepartment(filterValue) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ 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>
|
||||
<p v-if="error" class="text-sm text-severity-critical">{{ error }}</p>
|
||||
|
||||
<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
|
||||
v-for="department in departments"
|
||||
:key="department.key"
|
||||
|
||||
@@ -42,7 +42,7 @@ async function openDetail(gateway) {
|
||||
type="button"
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium transition duration-150"
|
||||
: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'"
|
||||
@click="store.setStatusFilter(tab.value)"
|
||||
>
|
||||
@@ -95,7 +95,7 @@ async function openDetail(gateway) {
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
<h3 class="mb-2 text-sm font-medium text-gray-500">Recent sync batches</h3>
|
||||
<ul class="space-y-2">
|
||||
|
||||
@@ -35,18 +35,18 @@ async function submit() {
|
||||
</script>
|
||||
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">VigilCare</h1>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Sign in to continue</p>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">VigilCare</h1>
|
||||
<p class="mt-1 text-sm text-ink-muted">Sign in to continue</p>
|
||||
</div>
|
||||
|
||||
<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
|
||||
</span>
|
||||
<input
|
||||
@@ -54,12 +54,12 @@ async function submit() {
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
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 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
|
||||
</span>
|
||||
<input
|
||||
@@ -67,11 +67,11 @@ async function submit() {
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
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>
|
||||
|
||||
<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 }}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ onMounted(load)
|
||||
<input
|
||||
v-model="includeResolved"
|
||||
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"
|
||||
>
|
||||
Include resolved
|
||||
@@ -119,7 +119,7 @@ onMounted(load)
|
||||
<RouterLink
|
||||
v-if="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
|
||||
</RouterLink>
|
||||
|
||||
@@ -32,18 +32,15 @@ const summaryLine = computed(() => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<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="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<p class="text-sm text-ink-secondary">{{ summaryLine }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<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>
|
||||
</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" />
|
||||
<EmptyState v-else-if="sortedBundles.length === 0" message="No active sepsis bundles" />
|
||||
|
||||
@@ -259,9 +259,9 @@ onBeforeUnmount(() => {
|
||||
v-for="opt in availableSpeeds"
|
||||
:key="opt.label"
|
||||
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
|
||||
? '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'"
|
||||
:aria-pressed="speed === opt.multiplier"
|
||||
@click="speed = opt.multiplier"
|
||||
@@ -269,7 +269,7 @@ onBeforeUnmount(() => {
|
||||
<span class="font-medium">{{ opt.label }}</span>
|
||||
<span
|
||||
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)">
|
||||
{{ wallClockFor(opt.multiplier) }} for selected scenario
|
||||
@@ -298,7 +298,7 @@ onBeforeUnmount(() => {
|
||||
type="button"
|
||||
class="border-b-2 px-4 py-2 text-sm font-medium transition"
|
||||
: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'"
|
||||
:aria-selected="activeTab === 'sessions'"
|
||||
@click="switchTab('sessions')"
|
||||
@@ -309,7 +309,7 @@ onBeforeUnmount(() => {
|
||||
type="button"
|
||||
class="border-b-2 px-4 py-2 text-sm font-medium transition"
|
||||
: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'"
|
||||
:aria-selected="activeTab === 'scenarios'"
|
||||
@click="switchTab('scenarios')"
|
||||
@@ -371,7 +371,7 @@ onBeforeUnmount(() => {
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
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>
|
||||
|
||||
|
||||
@@ -53,20 +53,19 @@ function onMobileSortChange(event) {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-bold dark:text-white">Virtual Ward</h1>
|
||||
<div class="mb-3 flex flex-wrap items-center justify-end gap-3">
|
||||
<Badge v-if="criticalCount > 0" variant="critical">
|
||||
{{ criticalCount }} critical
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<label class="mb-4 block sm:hidden">
|
||||
<span class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<label class="mb-3 block sm:hidden">
|
||||
<span class="mb-1.5 block text-xs font-medium text-ink-muted">
|
||||
Department
|
||||
</span>
|
||||
<select
|
||||
: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"
|
||||
>
|
||||
<option v-for="dept in departments" :key="dept.value" :value="dept.value">
|
||||
@@ -77,13 +76,13 @@ function onMobileSortChange(event) {
|
||||
|
||||
<WardToolbar @export-handoff="showHandoff = true" />
|
||||
|
||||
<label class="mb-4 block md:hidden">
|
||||
<span class="mb-2 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<label class="mb-3 block md:hidden">
|
||||
<span class="mb-1.5 block text-xs font-medium text-ink-muted">
|
||||
Sort by
|
||||
</span>
|
||||
<select
|
||||
: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"
|
||||
>
|
||||
<option v-for="option in WARD_SORT_FIELDS" :key="option.key" :value="option.key">
|
||||
|
||||
@@ -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 {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts}'],
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user