feature: Design System, App Shell, and Login Redesign
This commit is contained in:
Generated
+13
-13
@@ -1397,9 +1397,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2768,9 +2768,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -3005,9 +3005,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3024,7 +3024,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"nanoid": "^3.3.17",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -3780,9 +3780,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
||||
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
|
||||
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -4,75 +4,100 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--vc-navy: #08254A;
|
||||
--vc-primary: #155EEF;
|
||||
--vc-primary-hover: #0B4FD6;
|
||||
--vc-soft-blue: #EEF4FF;
|
||||
--vc-bg: #F7F9FC;
|
||||
--vc-surface: #FFFFFF;
|
||||
--vc-text-strong: #101828;
|
||||
--vc-text: #344054;
|
||||
--vc-text-secondary: #667085;
|
||||
--vc-text-disabled: #98A2B3;
|
||||
--vc-border: #E4E7EC;
|
||||
--vc-border-strong: #D0D5DD;
|
||||
--vc-success: #079455;
|
||||
--vc-success-bg: #ECFDF3;
|
||||
--vc-warning: #DC6803;
|
||||
--vc-warning-bg: #FFFAEB;
|
||||
--vc-error: #D92D20;
|
||||
--vc-error-bg: #FEF3F2;
|
||||
--vc-critical: #B42318;
|
||||
--vc-critical-bg: #FEE4E2;
|
||||
--vc-radius-input: 8px;
|
||||
--vc-radius-card: 10px;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply font-sans;
|
||||
@apply font-sans text-ink bg-canvas;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply bg-primary-600 text-white px-4 py-2 rounded-md
|
||||
@apply bg-primary-600 text-white px-4 py-2 rounded-input
|
||||
hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-colors;
|
||||
}
|
||||
.btn-secondary {
|
||||
@apply bg-white text-primary-700 border border-primary-300 px-4 py-2 rounded-md
|
||||
@apply bg-surface text-primary-700 border border-primary-300 px-4 py-2 rounded-input
|
||||
hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-colors;
|
||||
}
|
||||
.btn-danger {
|
||||
@apply bg-clinical-danger text-white px-4 py-2 rounded-md
|
||||
hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed
|
||||
@apply bg-clinical-danger text-white px-4 py-2 rounded-input
|
||||
hover:bg-clinical-critical disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-colors;
|
||||
}
|
||||
.form-input {
|
||||
@apply block w-full rounded-md border border-gray-300 px-4 py-2
|
||||
@apply block w-full rounded-input border border-line-strong px-4 py-2 text-ink
|
||||
focus:border-primary-500 focus:ring-2 focus:ring-primary-500
|
||||
disabled:bg-gray-100 disabled:text-gray-500;
|
||||
disabled:bg-canvas disabled:text-ink-disabled;
|
||||
}
|
||||
.page-container {
|
||||
@apply p-4 sm:p-6 lg:p-8 max-w-4xl mx-auto;
|
||||
}
|
||||
.app-header {
|
||||
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between
|
||||
px-4 py-4 sm:px-6 sm:py-4 bg-white border-b shadow-sm;
|
||||
px-4 py-3 sm:px-6 h-auto min-h-16 bg-surface border-b border-line;
|
||||
}
|
||||
.app-header-title {
|
||||
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4;
|
||||
@apply flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4 min-w-0;
|
||||
}
|
||||
.app-header-actions {
|
||||
@apply flex flex-wrap items-center gap-2 sm:gap-4;
|
||||
}
|
||||
.card {
|
||||
@apply bg-white rounded-md border border-gray-200 p-4 sm:p-6;
|
||||
@apply bg-surface rounded-card border border-line p-4 sm:p-6 shadow-card;
|
||||
}
|
||||
.card-elevated {
|
||||
@apply bg-white rounded-lg shadow-md p-4 sm:p-6;
|
||||
@apply bg-surface rounded-card border border-line p-4 sm:p-6 shadow-card;
|
||||
}
|
||||
.split-pane {
|
||||
@apply grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-8
|
||||
h-auto lg:h-[calc(100vh-4rem)] min-h-0;
|
||||
h-auto lg:flex-1 lg:min-h-0 min-h-0;
|
||||
}
|
||||
.nav-link {
|
||||
@apply text-sm text-gray-500 hover:text-gray-800 transition-colors;
|
||||
@apply text-sm text-ink-secondary hover:text-ink-strong transition-colors;
|
||||
}
|
||||
.nav-link.router-link-active {
|
||||
@apply text-gray-900 font-medium;
|
||||
@apply text-ink-strong font-medium;
|
||||
}
|
||||
.status-badge {
|
||||
@apply px-2 py-1 rounded-full text-xs font-medium;
|
||||
@apply px-2 py-1 rounded-control text-xs font-medium;
|
||||
}
|
||||
.ocr-banner {
|
||||
@apply bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-blue-800;
|
||||
@apply bg-primary-50 border border-primary-100 rounded-input p-3 text-sm text-primary-800;
|
||||
}
|
||||
.ocr-high {
|
||||
@apply border-l-[3px] border-l-green-500;
|
||||
@apply border-l-[3px] border-l-clinical-safe;
|
||||
}
|
||||
.ocr-medium {
|
||||
@apply border-l-[3px] border-l-yellow-500;
|
||||
@apply border-l-[3px] border-l-clinical-warning;
|
||||
}
|
||||
.ocr-low {
|
||||
@apply border-l-[3px] border-l-red-500;
|
||||
@apply border-l-[3px] border-l-clinical-danger;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,15 @@
|
||||
<template>
|
||||
<div class="app-header">
|
||||
<div class="app-header-title">
|
||||
<router-link to="/" class="flex items-center gap-2 shrink-0">
|
||||
<svg viewBox="0 0 32 32" class="w-6 h-6" aria-hidden="true">
|
||||
<path fill="currentColor" class="text-primary-700" d="M16 2 4 6.5v8.2c0 8 5.1 14.6 12 15.3 6.9-.7 12-7.3 12-15.3V6.5z"/>
|
||||
<path fill="white" d="M16 8.5c-3.6 0-6.6 2.8-6.6 6.3 0 3.4 2.7 6.4 6.6 9.6 3.9-3.2 6.6-6.2 6.6-9.6 0-3.5-3-6.3-6.6-6.3m0 3.4c1.7 0 3.1 1.3 3.1 2.9s-1.4 2.9-3.1 2.9-3.1-1.3-3.1-2.9 1.4-2.9 3.1-2.9"/>
|
||||
</svg>
|
||||
<span class="font-bold text-primary-800 tracking-tight">VigilCare</span>
|
||||
</router-link>
|
||||
<h1 class="text-lg font-semibold">{{ title }}</h1>
|
||||
<nav class="flex gap-3 ml-4">
|
||||
<router-link v-if="auth.canIntake" to="/intake" class="nav-link">Intake</router-link>
|
||||
<router-link v-if="auth.canIntake" to="/cover-sheets" class="nav-link">Cover Sheets</router-link>
|
||||
<router-link v-if="auth.canEntry" to="/entry" class="nav-link">Entry</router-link>
|
||||
<router-link v-if="auth.canVerify" to="/verification" class="nav-link">Verification</router-link>
|
||||
<router-link v-if="auth.canApprove" to="/approval" class="nav-link">Approval</router-link>
|
||||
<router-link v-if="auth.canLiveCapture" to="/live-capture" class="nav-link">Live Capture</router-link>
|
||||
<router-link to="/patients" class="nav-link">Patient History</router-link>
|
||||
<router-link v-if="auth.canSupervise" to="/dashboard" class="nav-link">Queue Dashboard</router-link>
|
||||
<router-link v-if="auth.canSupervise" to="/fhir-explorer" class="nav-link">FHIR Explorer</router-link>
|
||||
</nav>
|
||||
<h1 class="text-lg font-semibold text-ink-strong">{{ title }}</h1>
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
<div class="app-header-actions">
|
||||
<slot name="actions" />
|
||||
<span class="text-sm text-gray-600">{{ auth.userFullName }}</span>
|
||||
<button
|
||||
type="button"
|
||||
@click="auth.logout()"
|
||||
class="text-sm text-gray-500 hover:text-gray-800"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
defineProps<{ title: string }>()
|
||||
|
||||
const auth = useAuthStore()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="flex h-screen min-h-0 bg-canvas text-ink">
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
:class="[
|
||||
'flex flex-col shrink-0 bg-navy text-white transition-[width] duration-200 ease-out',
|
||||
collapsed ? 'w-16' : 'w-60',
|
||||
]"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex items-center h-16 border-b border-white/10 shrink-0',
|
||||
collapsed ? 'justify-center px-2' : 'gap-2.5 px-4',
|
||||
]"
|
||||
>
|
||||
<svg viewBox="0 0 32 32" class="w-7 h-7 shrink-0" aria-hidden="true">
|
||||
<path fill="#155EEF" d="M16 2 4 6.5v8.2c0 8 5.1 14.6 12 15.3 6.9-.7 12-7.3 12-15.3V6.5z"/>
|
||||
<path fill="white" d="M16 8.5c-3.6 0-6.6 2.8-6.6 6.3 0 3.4 2.7 6.4 6.6 9.6 3.9-3.2 6.6-6.2 6.6-9.6 0-3.5-3-6.3-6.6-6.3m0 3.4c1.7 0 3.1 1.3 3.1 2.9s-1.4 2.9-3.1 2.9-3.1-1.3-3.1-2.9 1.4-2.9 3.1-2.9"/>
|
||||
</svg>
|
||||
<div v-if="!collapsed" class="min-w-0 leading-tight">
|
||||
<p class="font-bold tracking-tight text-sm truncate">VigilCare</p>
|
||||
<p class="text-[11px] text-white/60 truncate">Records</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 overflow-y-auto py-3 px-2 space-y-4">
|
||||
<div v-if="workspaceItems.length">
|
||||
<p
|
||||
v-if="!collapsed"
|
||||
class="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/40"
|
||||
>
|
||||
Workspace
|
||||
</p>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="item in workspaceItems" :key="item.to">
|
||||
<router-link
|
||||
:to="item.to"
|
||||
:title="collapsed ? item.label : undefined"
|
||||
:class="navItemClass(item.to)"
|
||||
>
|
||||
<span class="shrink-0 w-5 h-5 flex items-center justify-center" aria-hidden="true">
|
||||
<component :is="item.icon" />
|
||||
</span>
|
||||
<span v-if="!collapsed" class="truncate">{{ item.label }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="adminItems.length">
|
||||
<p
|
||||
v-if="!collapsed"
|
||||
class="px-2 mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/40"
|
||||
>
|
||||
Administration
|
||||
</p>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="item in adminItems" :key="item.to">
|
||||
<router-link
|
||||
:to="item.to"
|
||||
:title="collapsed ? item.label : undefined"
|
||||
:class="navItemClass(item.to)"
|
||||
>
|
||||
<span class="shrink-0 w-5 h-5 flex items-center justify-center" aria-hidden="true">
|
||||
<component :is="item.icon" />
|
||||
</span>
|
||||
<span v-if="!collapsed" class="truncate">{{ item.label }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="shrink-0 border-t border-white/10 p-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2.5 rounded-input px-2.5 py-2 text-sm text-white/70 hover:bg-white/10 hover:text-white transition-colors"
|
||||
:class="collapsed ? 'justify-center' : ''"
|
||||
:aria-expanded="!collapsed"
|
||||
:aria-label="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 shrink-0 transition-transform"
|
||||
:class="collapsed ? 'rotate-180' : ''"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span v-if="!collapsed">Collapse</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main column -->
|
||||
<div class="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<header class="flex items-center justify-end gap-4 h-16 shrink-0 px-4 sm:px-6 bg-surface border-b border-line">
|
||||
<span class="text-sm text-ink-secondary truncate">{{ auth.userFullName }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm text-ink-secondary hover:text-ink-strong transition-colors"
|
||||
@click="auth.logout()"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 min-h-0 overflow-auto">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const COLLAPSE_KEY = 'vigilcare_sidebar_collapsed'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
|
||||
const collapsed = ref(localStorage.getItem(COLLAPSE_KEY) === '1')
|
||||
|
||||
watch(collapsed, (value) => {
|
||||
localStorage.setItem(COLLAPSE_KEY, value ? '1' : '0')
|
||||
})
|
||||
|
||||
function toggleCollapsed() {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
function iconPath(d: string) {
|
||||
return () =>
|
||||
h(
|
||||
'svg',
|
||||
{ viewBox: '0 0 20 20', fill: 'currentColor', class: 'w-5 h-5' },
|
||||
[h('path', { 'fill-rule': 'evenodd', 'clip-rule': 'evenodd', d })],
|
||||
)
|
||||
}
|
||||
|
||||
const icons = {
|
||||
intake: iconPath(
|
||||
'M3 4.5A1.5 1.5 0 014.5 3h11A1.5 1.5 0 0117 4.5v11a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 013 15.5v-11zM5.5 6a.5.5 0 000 1h9a.5.5 0 000-1h-9zm0 3a.5.5 0 000 1h9a.5.5 0 000-1h-9zm0 3a.5.5 0 000 1h6a.5.5 0 000-1h-6z',
|
||||
),
|
||||
cover: iconPath(
|
||||
'M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm1 3.5A.5.5 0 015.5 6h9a.5.5 0 010 1h-9A.5.5 0 015 6.5zM5.5 9a.5.5 0 000 1h5a.5.5 0 000-1h-5z',
|
||||
),
|
||||
entry: iconPath(
|
||||
'M3.5 3A1.5 1.5 0 002 4.5v11A1.5 1.5 0 003.5 17h13a1.5 1.5 0 001.5-1.5v-11A1.5 1.5 0 0016.5 3h-13zM5 7.5A.5.5 0 015.5 7h9a.5.5 0 010 1h-9A.5.5 0 015 7.5zm0 3A.5.5 0 015.5 10h9a.5.5 0 010 1h-9A.5.5 0 015 10.5zm0 3A.5.5 0 015.5 13h5a.5.5 0 010 1h-5A.5.5 0 015 13.5z',
|
||||
),
|
||||
verify: iconPath(
|
||||
'M10 2a8 8 0 100 16 8 8 0 000-16zm3.53 6.47a.75.75 0 00-1.06-1.06L9 10.88 7.53 9.41a.75.75 0 10-1.06 1.06l2 2a.75.75 0 001.06 0l4-4z',
|
||||
),
|
||||
approve: iconPath(
|
||||
'M10 1.5l7.5 3v5.25c0 4.5-2.9 8.25-7.5 9.25-4.6-1-7.5-4.75-7.5-9.25V4.5L10 1.5zm3.03 6.03a.75.75 0 00-1.06-1.06L9 9.44 7.53 7.97a.75.75 0 10-1.06 1.06l2 2a.75.75 0 001.06 0l3.5-3.5z',
|
||||
),
|
||||
live: iconPath(
|
||||
'M4 5a2 2 0 012-2h8a2 2 0 012 2v6a2 2 0 01-2 2H9.414l-2.707 2.707A1 1 0 015 15.293V13H6a2 2 0 01-2-2V5zm3.5 2.5a.75.75 0 000 1.5h5a.75.75 0 000-1.5h-5z',
|
||||
),
|
||||
patients: iconPath(
|
||||
'M10 2a4 4 0 100 8 4 4 0 000-8zM3.5 16.5a6.5 6.5 0 0113 0 .75.75 0 01-.75.75h-11.5a.75.75 0 01-.75-.75z',
|
||||
),
|
||||
dashboard: iconPath(
|
||||
'M3 4.5A1.5 1.5 0 014.5 3h3A1.5 1.5 0 019 4.5v3A1.5 1.5 0 017.5 9h-3A1.5 1.5 0 013 7.5v-3zM11 4.5A1.5 1.5 0 0112.5 3h3A1.5 1.5 0 0117 4.5v3A1.5 1.5 0 0115.5 9h-3A1.5 1.5 0 0111 7.5v-3zM3 12.5A1.5 1.5 0 014.5 11h3A1.5 1.5 0 019 12.5v3A1.5 1.5 0 017.5 17h-3A1.5 1.5 0 013 15.5v-3zM11 12.5a1.5 1.5 0 011.5-1.5h3a1.5 1.5 0 011.5 1.5v3a1.5 1.5 0 01-1.5 1.5h-3a1.5 1.5 0 01-1.5-1.5v-3z',
|
||||
),
|
||||
fhir: iconPath(
|
||||
'M4.5 3A1.5 1.5 0 003 4.5v11A1.5 1.5 0 004.5 17h11a1.5 1.5 0 001.5-1.5v-11A1.5 1.5 0 0015.5 3h-11zM6 7.25a.75.75 0 01.75-.75h6.5a.75.75 0 010 1.5h-6.5A.75.75 0 016 7.25zm0 3a.75.75 0 01.75-.75h6.5a.75.75 0 010 1.5h-6.5A.75.75 0 016 10.25zm0 3a.75.75 0 01.75-.75h3.5a.75.75 0 010 1.5h-3.5A.75.75 0 016 13.25z',
|
||||
),
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
label: string
|
||||
to: string
|
||||
icon: () => ReturnType<typeof h>
|
||||
show: boolean
|
||||
}
|
||||
|
||||
const workspaceItems = computed<NavItem[]>(() =>
|
||||
[
|
||||
{ label: 'Intake', to: '/intake', icon: icons.intake, show: auth.canIntake },
|
||||
{ label: 'Cover Sheets', to: '/cover-sheets', icon: icons.cover, show: auth.canIntake },
|
||||
{ label: 'Data Entry', to: '/entry', icon: icons.entry, show: auth.canEntry },
|
||||
{ label: 'Verification', to: '/verification', icon: icons.verify, show: auth.canVerify },
|
||||
{ label: 'Clinical Approval', to: '/approval', icon: icons.approve, show: auth.canApprove },
|
||||
{ label: 'Live Capture', to: '/live-capture', icon: icons.live, show: auth.canLiveCapture },
|
||||
{ label: 'Patient History', to: '/patients', icon: icons.patients, show: auth.isAuthenticated },
|
||||
].filter((item) => item.show),
|
||||
)
|
||||
|
||||
const adminItems = computed<NavItem[]>(() =>
|
||||
[
|
||||
{ label: 'Queue Dashboard', to: '/dashboard', icon: icons.dashboard, show: auth.canSupervise },
|
||||
{ label: 'FHIR Explorer', to: '/fhir-explorer', icon: icons.fhir, show: auth.canSupervise },
|
||||
].filter((item) => item.show),
|
||||
)
|
||||
|
||||
function isActive(path: string): boolean {
|
||||
if (path === '/patients') {
|
||||
return route.path === '/patients' || route.path.startsWith('/patients/')
|
||||
}
|
||||
if (path === '/entry') {
|
||||
return route.path === '/entry' || route.path.startsWith('/entry/')
|
||||
}
|
||||
if (path === '/verification') {
|
||||
return route.path === '/verification' || route.path.startsWith('/verification/')
|
||||
}
|
||||
if (path === '/approval') {
|
||||
return route.path === '/approval' || route.path.startsWith('/approval/')
|
||||
}
|
||||
return route.path === path || route.path.startsWith(`${path}/`)
|
||||
}
|
||||
|
||||
function navItemClass(path: string): string {
|
||||
const base =
|
||||
'flex items-center gap-2.5 rounded-input text-sm transition-colors ' +
|
||||
(collapsed.value ? 'justify-center px-2 py-2.5' : 'px-2.5 py-2')
|
||||
if (isActive(path)) {
|
||||
return `${base} bg-primary-600 text-white font-medium`
|
||||
}
|
||||
return `${base} text-white/75 hover:bg-white/10 hover:text-white`
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<AppShell>
|
||||
<router-view />
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppShell from '../components/AppShell.vue'
|
||||
</script>
|
||||
@@ -8,102 +8,118 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('../views/LoginView.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/intake',
|
||||
name: 'Intake',
|
||||
component: () => import('../views/IntakeView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['INTAKE_CLERK', 'ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: '/cover-sheets',
|
||||
name: 'CoverSheets',
|
||||
component: () => import('../views/CoverSheetView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['INTAKE_CLERK', 'ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: '/entry',
|
||||
name: 'EntryQueue',
|
||||
component: () => import('../views/EntryView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['DATA_ENTRY_CLERK', 'ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: '/entry/:batchId',
|
||||
name: 'EntryBatch',
|
||||
component: () => import('../views/EntryView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['DATA_ENTRY_CLERK', 'ADMINISTRATOR'] },
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/verification',
|
||||
name: 'VerificationQueue',
|
||||
component: () => import('../views/VerificationView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['VERIFIER', 'CLINICAL_APPROVER', 'ADMINISTRATOR'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/verification/:batchId',
|
||||
name: 'VerificationBatch',
|
||||
component: () => import('../views/VerificationView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['VERIFIER', 'CLINICAL_APPROVER', 'ADMINISTRATOR'],
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/approval',
|
||||
name: 'ApprovalQueue',
|
||||
component: () => import('../views/ApprovalView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['CLINICAL_APPROVER', 'ADMINISTRATOR'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/approval/:batchId',
|
||||
name: 'ApprovalBatch',
|
||||
component: () => import('../views/ApprovalView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['CLINICAL_APPROVER', 'ADMINISTRATOR'],
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/patients/:patientId/history',
|
||||
name: 'PatientHistory',
|
||||
component: () => import('../views/PatientHistoryView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/patients',
|
||||
name: 'PatientSearch',
|
||||
component: () => import('../views/PatientHistoryView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/live-capture',
|
||||
name: 'LiveCapture',
|
||||
component: () => import('../views/LiveCaptureView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['CLINICIAN', 'ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/QueueDashboardView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: '/fhir-explorer',
|
||||
name: 'FhirExplorer',
|
||||
component: () => import('../views/FhirExplorerView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('../layouts/AuthenticatedLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: () => {
|
||||
const auth = useAuthStore()
|
||||
return auth.isAuthenticated
|
||||
? getDefaultRouteForRole(auth.userRole)
|
||||
: '/login'
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'intake',
|
||||
name: 'Intake',
|
||||
component: () => import('../views/IntakeView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['INTAKE_CLERK', 'ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: 'cover-sheets',
|
||||
name: 'CoverSheets',
|
||||
component: () => import('../views/CoverSheetView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['INTAKE_CLERK', 'ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: 'entry',
|
||||
name: 'EntryQueue',
|
||||
component: () => import('../views/EntryView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['DATA_ENTRY_CLERK', 'ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: 'entry/:batchId',
|
||||
name: 'EntryBatch',
|
||||
component: () => import('../views/EntryView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['DATA_ENTRY_CLERK', 'ADMINISTRATOR'] },
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: 'verification',
|
||||
name: 'VerificationQueue',
|
||||
component: () => import('../views/VerificationView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['VERIFIER', 'CLINICAL_APPROVER', 'ADMINISTRATOR'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'verification/:batchId',
|
||||
name: 'VerificationBatch',
|
||||
component: () => import('../views/VerificationView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['VERIFIER', 'CLINICAL_APPROVER', 'ADMINISTRATOR'],
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: 'approval',
|
||||
name: 'ApprovalQueue',
|
||||
component: () => import('../views/ApprovalView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['CLINICAL_APPROVER', 'ADMINISTRATOR'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'approval/:batchId',
|
||||
name: 'ApprovalBatch',
|
||||
component: () => import('../views/ApprovalView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['CLINICAL_APPROVER', 'ADMINISTRATOR'],
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: 'patients/:patientId/history',
|
||||
name: 'PatientHistory',
|
||||
component: () => import('../views/PatientHistoryView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: 'patients',
|
||||
name: 'PatientSearch',
|
||||
component: () => import('../views/PatientHistoryView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'live-capture',
|
||||
name: 'LiveCapture',
|
||||
component: () => import('../views/LiveCaptureView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['CLINICIAN', 'ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/QueueDashboardView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['ADMINISTRATOR'] },
|
||||
},
|
||||
{
|
||||
path: 'fhir-explorer',
|
||||
name: 'FhirExplorer',
|
||||
component: () => import('../views/FhirExplorerView.vue'),
|
||||
meta: { requiresAuth: true, roles: ['ADMINISTRATOR'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/login',
|
||||
},
|
||||
]
|
||||
@@ -139,4 +155,4 @@ router.beforeEach((to, _from, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
export default router
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen lg:h-screen flex flex-col">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="Clinical Approval">
|
||||
<template #subtitle>
|
||||
<span v-if="currentBatch" class="text-sm text-gray-500">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="Cover Sheets" />
|
||||
|
||||
<div class="p-4 sm:p-6 lg:p-8 max-w-6xl mx-auto flex-1 w-full">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen lg:h-screen flex flex-col">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="Data Entry">
|
||||
<template #subtitle>
|
||||
<span v-if="currentBatch" class="text-sm text-gray-500">
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<!-- Queue view (no batch selected) -->
|
||||
<div v-if="!batchId" class="flex-1 p-4 sm:p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Entry Queue</h2>
|
||||
<h2 class="text-xl font-semibold mb-4">Data Entry Queue</h2>
|
||||
<BatchList
|
||||
:batches="batchStore.batches"
|
||||
:loading="batchStore.loading"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="FHIR Explorer" />
|
||||
|
||||
<div class="p-4 sm:p-6 lg:p-8 max-w-6xl mx-auto flex-1 w-full space-y-6">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="Intake" />
|
||||
|
||||
<div class="page-container flex-1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="Live Capture" />
|
||||
|
||||
<div class="flex-1 p-4 sm:p-6 lg:p-8 max-w-5xl mx-auto w-full">
|
||||
|
||||
@@ -1,40 +1,100 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 px-4 py-8">
|
||||
<div class="max-w-md w-full bg-white rounded-lg shadow-md p-6 sm:p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900">VigilCare Records</h1>
|
||||
<p class="text-gray-600 mt-2">Digitization Workstation</p>
|
||||
<div class="min-h-screen flex flex-col lg:flex-row bg-canvas">
|
||||
<!-- Brand panel -->
|
||||
<aside
|
||||
class="relative flex flex-col justify-between bg-navy text-white px-8 py-10 lg:w-[45%] lg:min-h-screen lg:px-12 lg:py-14"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 opacity-[0.07]"
|
||||
aria-hidden="true"
|
||||
style="background-image: radial-gradient(circle at 20% 20%, #155EEF 0%, transparent 45%), radial-gradient(circle at 80% 70%, #EEF4FF 0%, transparent 40%);"
|
||||
/>
|
||||
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg viewBox="0 0 32 32" class="w-9 h-9 shrink-0" aria-hidden="true">
|
||||
<path fill="#155EEF" d="M16 2 4 6.5v8.2c0 8 5.1 14.6 12 15.3 6.9-.7 12-7.3 12-15.3V6.5z"/>
|
||||
<path fill="white" d="M16 8.5c-3.6 0-6.6 2.8-6.6 6.3 0 3.4 2.7 6.4 6.6 9.6 3.9-3.2 6.6-6.2 6.6-9.6 0-3.5-3-6.3-6.6-6.3m0 3.4c1.7 0 3.1 1.3 3.1 2.9s-1.4 2.9-3.1 2.9-3.1-1.3-3.1-2.9 1.4-2.9 3.1-2.9"/>
|
||||
</svg>
|
||||
<div class="leading-tight">
|
||||
<p class="font-bold text-lg tracking-tight">VigilCare</p>
|
||||
<p class="text-sm text-white/60">Records</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 lg:mt-20 max-w-md">
|
||||
<h1 class="text-[2rem] sm:text-[2.5rem] lg:text-[2.625rem] font-bold leading-tight tracking-tight">
|
||||
Digitize. Verify. Trust.
|
||||
</h1>
|
||||
<p class="mt-4 text-base text-white/70 leading-relaxed">
|
||||
Scanned clinical records become governed, structured data your teams can verify and trust.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="e.g. intake1, entry1, verifier1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
<!-- Restrained flow hint (desktop) -->
|
||||
<div
|
||||
class="relative hidden lg:flex items-center gap-3 mt-16 text-sm text-white/50"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="rounded-input border border-white/15 bg-white/5 px-3 py-1.5">Paper record</span>
|
||||
<span class="text-white/30">→</span>
|
||||
<span class="rounded-input border border-white/15 bg-white/5 px-3 py-1.5">Verified digital</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Auth panel -->
|
||||
<div class="flex flex-1 items-center justify-center px-4 py-10 sm:px-8">
|
||||
<div class="w-full max-w-[440px] bg-surface border border-line rounded-card shadow-card p-6 sm:p-8">
|
||||
<div class="mb-8">
|
||||
<h2 class="text-[1.75rem] font-semibold text-ink-strong leading-tight">Sign in</h2>
|
||||
<p class="mt-2 text-sm text-ink-secondary">
|
||||
Use your VigilCare workstation credentials.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="text-clinical-danger text-sm">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<form @submit.prevent="handleLogin" class="space-y-5">
|
||||
<div>
|
||||
<label for="login-username" class="block text-sm font-semibold text-ink mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="login-username"
|
||||
v-model="username"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
class="form-input"
|
||||
placeholder="e.g. intake1, entry1, verifier1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="login-password" class="block text-sm font-semibold text-ink mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="login-password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full" :disabled="loading">
|
||||
{{ loading ? 'Signing in...' : 'Sign In' }}
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="rounded-input bg-clinical-danger-bg border border-clinical-danger/20 px-3 py-2 text-sm text-clinical-danger"
|
||||
role="alert"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full" :disabled="loading">
|
||||
{{ loading ? 'Signing in...' : 'Sign In' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -60,4 +120,4 @@ async function handleLogin() {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="Patient History" />
|
||||
|
||||
<div class="flex-1 p-4 sm:p-6 lg:p-8 max-w-5xl mx-auto w-full">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="min-h-screen lg:h-screen flex flex-col">
|
||||
<AppHeader title="Supervisor Dashboard">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="Queue Dashboard">
|
||||
<template #actions>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen lg:h-screen flex flex-col">
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<AppHeader title="Verification">
|
||||
<template #subtitle>
|
||||
<span v-if="currentBatch" class="text-sm text-gray-500">
|
||||
|
||||
@@ -7,20 +7,35 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
navy: '#08254A',
|
||||
primary: {
|
||||
50: '#eff9f8',
|
||||
100: '#d9f0ee',
|
||||
300: '#92d2ca',
|
||||
400: '#5cb9ae',
|
||||
500: '#2f9d91',
|
||||
600: '#1f8075',
|
||||
700: '#17655d',
|
||||
800: '#124e48',
|
||||
50: '#EEF4FF',
|
||||
100: '#D1E0FF',
|
||||
300: '#84ADFF',
|
||||
400: '#528BFF',
|
||||
500: '#155EEF',
|
||||
600: '#155EEF',
|
||||
700: '#0B4FD6',
|
||||
800: '#0A3FAA',
|
||||
},
|
||||
surface: '#FFFFFF',
|
||||
canvas: '#F7F9FC',
|
||||
// Flat keys so @apply text-ink / border-line resolve (nested DEFAULT breaks @apply)
|
||||
ink: '#344054',
|
||||
'ink-strong': '#101828',
|
||||
'ink-secondary': '#667085',
|
||||
'ink-disabled': '#98A2B3',
|
||||
line: '#E4E7EC',
|
||||
'line-strong': '#D0D5DD',
|
||||
clinical: {
|
||||
safe: '#2f9e58',
|
||||
warning: '#c98a1b',
|
||||
danger: '#c2293f',
|
||||
safe: '#079455',
|
||||
'safe-bg': '#ECFDF3',
|
||||
warning: '#DC6803',
|
||||
'warning-bg': '#FFFAEB',
|
||||
danger: '#D92D20',
|
||||
'danger-bg': '#FEF3F2',
|
||||
critical: '#B42318',
|
||||
'critical-bg': '#FEE4E2',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
@@ -32,6 +47,16 @@ export default {
|
||||
'sans-serif',
|
||||
],
|
||||
},
|
||||
borderRadius: {
|
||||
control: '6px',
|
||||
input: '8px',
|
||||
card: '10px',
|
||||
panel: '12px',
|
||||
},
|
||||
boxShadow: {
|
||||
card: '0 1px 2px rgba(16, 24, 40, 0.04)',
|
||||
dialog: '0 8px 24px rgba(16, 24, 40, 0.12)',
|
||||
},
|
||||
spacing: {
|
||||
18: '4.5rem',
|
||||
22: '5.5rem',
|
||||
|
||||
Reference in New Issue
Block a user