08 — Frontend Design Guidelines
Status: populated Last updated: 2026-04-18 Companion: 09 frontend-workflows · 01 enterprise-architecture · 17 technology-stack · 13 security-compliance-tenancy · 16 offline-first
1. Surfaces and app shells
Ghasi-eHealth ships multiple coordinated UI surfaces — each inherits one design system, tokens, and accessibility baseline, but presents density and navigation tuned to the persona.
| Surface | Target persona | Tech | Density | Offline capability |
|---|---|---|---|---|
| Clinician Web (EHR) | Doctor, nurse, charge nurse, resident | Next.js 16 (App Router), MUI v6, TanStack Query, Zustand | Comfortable / Compact toggle | Partial (worklists + chart read) |
| Registration Station (Desktop) | Reg. clerk, cashier, triage nurse | Electron + Vite, React Router, better-sqlite3, Windows/macOS/Linux | Compact (dense forms) | Full offline (SQLite outbox) |
| Patient Portal (Web) | Patient, caregiver, guardian | Next.js 16, MUI v6, next-auth | Comfortable, generous spacing | Read-only cache of last-seen |
| Patient Mobile (iOS/Android) | Patient | React Native / Expo (Expo Router), Keycloak OIDC | Thumb-first, large hit targets | Full offline read, queued writes |
| Provider Mobile (iOS/Android) | Field nurse, CHW, outreach | React Native / Expo, Realm local DB, MAC-signed sync | Dense (field-ops) | Full offline clinic |
| Lab / Pharmacy Portals | Lab tech, pharmacist | Next.js 16 | Compact | Partial (queue replay) |
| Virtual Care Room | Clinician + patient | Next.js + Jitsi SDK (WebRTC) | Distraction-minimised | Online-only |
| Platform Admin Console | Tenant admin, super-admin | Next.js 16 | Compact tables, audit-log heavy | Online-only |
All surfaces share the same design tokens, component primitives, and i18n bundle. Divergence is allowed only for density mode, navigation pattern, and offline affordances.
2. Frontend clean architecture (shared layout)
/app # Next.js route segments (App Router)
/components # Presentational components
/components/server # Server components (RSC)
/hooks # Orchestration (TanStack Query wrappers)
/services # API ports (typed clients per service)
/state # Zustand stores per domain
/lib/domain # Pure TS primitives — Money, FHIR refs, ABAC eval, terminology codes, RRULE
/lib/adapters # IndexedDB (Dexie), Service Worker, offline outbox, Realm (mobile)
/fhir # FHIR resource helpers, builders, validators
/i18n # ICU messages per locale
/styles # Tokens, tailwind preset, global base
/types # Shared TS types mirroring service DTOs
Rules:
lib/domainhas no React, Next.js, or fetch. Pure TypeScript.services/defines ports;lib/adapters/supplies concrete implementations bound at bootstrap.- Components depend on
hooks/+services/only. Never directfetchin a component. - FHIR resource shapes are imported from
@ghasi/shared-types— no ad-hoc types.
3. Design tokens
Tokens live in CSS custom properties and are exported as a Tailwind preset and MUI theme builder. All component styling must consume tokens — no raw hex, no magic spacing.
3.1 Color — clinical semantic palette
:root {
/* Surfaces */
--color-bg: oklch(98% 0.005 260);
--color-surface: oklch(99% 0.003 260);
--color-surface-alt: oklch(95% 0.01 260);
/* Text */
--color-text: oklch(20% 0.01 260);
--color-text-muted: oklch(42% 0.02 260);
--color-text-inverse: oklch(98% 0 0);
/* Brand (platform identity) */
--color-brand: oklch(52% 0.14 235);
--color-brand-fg: oklch(99% 0 0);
/* Clinical semantic — red / amber / green traffic */
--color-critical: oklch(52% 0.22 25); /* stat orders, alerts, allergies */
--color-warning: oklch(72% 0.18 80); /* due soon, caution */
--color-success: oklch(55% 0.16 145); /* normal, verified, in-range */
--color-info: oklch(58% 0.13 235); /* informational */
/* Clinical status-specific (safety-critical, never dual-meaning) */
--color-stat: var(--color-critical); /* STAT order label */
--color-abnormal-h: oklch(48% 0.20 25); /* High flag on result */
--color-abnormal-l: oklch(48% 0.20 240); /* Low flag on result */
--color-allergy: oklch(48% 0.22 15); /* Allergy banner */
--color-in-range: var(--color-success);
/* Charting */
--color-chart-1: oklch(60% 0.14 235);
--color-chart-2: oklch(60% 0.14 155);
--color-chart-3: oklch(60% 0.14 55);
--color-chart-4: oklch(60% 0.14 320);
}
.theme-dark {
--color-bg: oklch(14% 0.01 260);
--color-surface: oklch(18% 0.01 260);
--color-text: oklch(95% 0.005 260);
/* ... semantic tokens re-tuned for ≥ 4.5:1 contrast in dark */
}
.theme-high-contrast {
/* WCAG 2.2 AAA — used by accessibility toggle */
--color-text: oklch(5% 0 0);
--color-bg: oklch(100% 0 0);
--color-brand: oklch(30% 0.2 235);
}
Rules:
- Semantic tokens are one-directional —
--color-criticalis used only for critical. Never to stylize branding. - Red/green alone is not the carrier of meaning — every status badge pairs color with icon and text label (colorblind policy).
- Dark mode is opt-in; daytime clinical workflows default light. Never auto-switch based on OS — ward lighting and patient-in-bed contexts differ.
3.2 Typography — bi-directional
:root {
--font-sans-ltr: 'InterVariable', system-ui, sans-serif;
--font-sans-rtl: 'Vazirmatn', 'Noto Naskh Arabic', 'InterVariable', sans-serif;
--font-mono: 'JetBrainsMono', 'Courier New', monospace;
--font-numeric: 'InterVariable'; /* tabular-nums for vitals and dose */
--text-xs: clamp(0.72rem, 0.7rem + 0.1vw, 0.78rem);
--text-sm: clamp(0.82rem, 0.8rem + 0.15vw, 0.9rem);
--text-base: clamp(0.94rem, 0.9rem + 0.25vw, 1.05rem);
--text-lg: clamp(1.08rem, 1rem + 0.4vw, 1.25rem);
--text-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
--text-h2: clamp(1.5rem, 1.3rem + 1vw, 2rem);
--text-h1: clamp(2rem, 1.7rem + 1.5vw, 3rem);
--line-height-tight: 1.18;
--line-height-base: 1.55;
--line-height-loose: 1.75; /* for Dari/Pashto paragraph blocks */
}
html[dir='rtl'] { font-family: var(--font-sans-rtl); }
html[dir='ltr'] { font-family: var(--font-sans-ltr); }
Rules:
html[dir]is set by the server per locale.- Every layout uses logical properties:
padding-inline,margin-inline-start,inset-inline-end,text-align: start | end. Noleft/right. - Direction-implying icons (chevrons, undo/redo) flipped via
[dir='rtl'] .flip-on-rtl { transform: scaleX(-1); }. - Mixed-direction blocks (English drug name inside Dari sentence) use Unicode bidi isolates (
<bdi>,isolate-override). Tested in Storybook bidi stories. - Clinical numeric readouts (vitals, dose) always use
font-variant-numeric: tabular-numsto keep table columns aligned.
3.3 Space, radius, shadow, motion
:root {
--space-0: 0; --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem;
--space-4: 1rem; --space-5: 1.25rem; --space-6: 1.5rem; --space-8: 2rem;
--space-10: 2.5rem; --space-12: 3rem; --space-16: 4rem;
--radius-xs: 2px; --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-pill: 999px;
--shadow-1: 0 1px 2px oklch(0% 0 0 / 0.06), 0 1px 4px oklch(0% 0 0 / 0.04);
--shadow-2: 0 4px 16px oklch(0% 0 0 / 0.08);
--shadow-focus: 0 0 0 3px oklch(52% 0.14 235 / 0.35);
--duration-instant: 80ms;
--duration-fast: 150ms;
--duration-base: 220ms;
--duration-slow: 360ms;
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-entrance: cubic-bezier(0, 0, 0, 1);
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1.2);
}
3.4 Density modes
Clinician workflows demand high information density, but patient-facing surfaces must breathe. A single data-density="compact|comfortable" attribute on the root switches token multipliers.
| Mode | Default for | Row height | Font base | Button padding-inline |
|---|---|---|---|---|
comfortable | Patient portal, patient mobile, onboarding | 48 px | --text-base | --space-5 |
compact | Clinician web, registration station, admin console | 32 px | --text-sm | --space-3 |
Clinicians can toggle modes per device via user preference; preference is synced in the user profile (not per-session).
4. Iconography
- Primary set: FontAwesome Pro Sharp (licensed) — consistent stroke weight across the platform.
- Clinical custom set: bespoke SVG icons under
@ghasi/icons-clinicalfor clinical primitives FontAwesome lacks (syringe types, specimen tubes, dose calculator, stethoscope/pulse-ox). 24 × 24 grid, 1.5 px stroke, variable stroke weight on hover. - Never raster — SVG only, currentColor-driven.
- Directional semantics —
fa-arrow-righttagged.flip-on-rtl. Non-directional icons (heart, check, warning) never flipped. - Pairing rule — every status pairs icon + color + short text label. Icons alone are not the carrier of meaning (colorblind / low-contrast).
5. Layout grid
- Desktop clinician (≥ 1280 px): 12-column grid, 24 px gutter, 80 px side rails. Chart pages use a 3-region layout: 280 px left rail (problem/allergy/med summary), fluid center (active content), 320 px right rail (orders/results pane).
- Laptop (1024 – 1279 px): 12-column, 16 px gutter, right rail collapses to a tab strip.
- Tablet (768 – 1023 px): 8-column, single primary + drawer left rail.
- Phone (< 768 px): 4-column, bottom tab nav, drawers for secondary.
- Print (A4 / Letter): stripped theme, tokens downshift (serif typography, 11 pt base, black only unless semantic color carries meaning — e.g., allergy banner).
6. Accessibility baseline
Target WCAG 2.2 Level AA platform-wide; aspire AAA on critical read/print paths (allergy banner, medication list, allergy printout).
| Concern | Rule |
|---|---|
| Contrast | ≥ 4.5:1 body, ≥ 3:1 large text and UI components |
| Keyboard | 100% of flows reachable via keyboard; visible focus ring; Esc closes any dialog |
| Screen reader | Semantic HTML first; ARIA only where semantics are not natively expressible; live regions for result arrival and order confirmation |
| Reduced motion | All motion gated on @media (prefers-reduced-motion) |
| Target size | Minimum 24 × 24 CSS px (WCAG 2.5.8); clinical primary buttons 44 × 44 |
| Language | lang attr per passage; bilingual strings in forms tagged with <bdi> |
| Form errors | aria-describedby to message; programmatic label on every input |
| Live regions | Vital-sign alerts and critical-result arrival announced via role="alert" |
| Zoom | 200% zoom + 320 px reflow without loss of function |
| Media | Captions on all educational video; transcripts downloadable |
| Allergy banner | Always at top of chart, high-contrast, present on print; announced first by screen readers |
Automated gate: axe-playwright on every PR. Violations at serious or critical block merge. Manual NVDA + VoiceOver + TalkBack review per release.
7. Internationalization
- Locales:
en,ps-AF(Pashto, RTL),fa-AF(Dari, RTL),ar-AE(Arabic, RTL), plus futureuz,tk,ur. - Format: ICU MessageFormat; plural + gender rules supported.
- Calendars: Gregorian (storage + display default), Solar Hijri (
jalali, Afghanistan civil calendar), Hijri lunar (religious). User preference selects display calendar; storage is ISO-8601 UTC always. - Numbers and dates:
Intl.NumberFormat/Intl.DateTimeFormat; Arabic-Indic digits honored by locale. Clinical numeric fields always in Latin digits withlang="en"override to prevent miscommunication. - Fallback chain: User pref → facility default → tenant default →
en.
8. Component system
8.1 Primitives (shared across all surfaces)
Button, IconButton, Link, Input, NumericInput, Textarea, Select, Combobox, Autocomplete, Checkbox, Radio, Switch, Slider, DatePicker (Gregorian + Jalali + Hijri), TimePicker, DurationPicker, Dialog, Drawer, Tooltip, Popover, Toast, Banner, Skeleton, Avatar, Badge, Tag, Chip, Card, Surface, Divider, Stack, Cluster, Grid, Tabs, Accordion, Tree.
8.2 Clinical composites
- AllergyBanner — mandatory top-of-chart banner; renders even on print; high-contrast; announces allergen list.
- VitalsPanel — tabular vitals with in-range / out-of-range visual encoding (icon + color + label).
- OrderEntryCard — unified order entry for lab / medication / radiology / referral with STAT emphasis and dose-safety warnings.
- ResultsTable — FHIR
Observation-driven; H/L/CRIT flags; reference-range sparkline. - MedicationList — active meds with reconciliation state, therapeutic class grouping.
- ChartTimeline — longitudinal chart navigator; encounter selector; problem / med / visit pin.
- EncounterHeader — patient demographics condensed; provider identity; location; encounter type.
- ConsentPrompt — structured consent with versioned policy text.
- SpecimenLabel — print-ready label component; QR + barcode; tolerant to RTL facility names.
8.3 Patterns
- Compound components —
Tabs.Root / Tabs.List / Tabs.Trigger / Tabs.Contentpattern for shared state. - Headless first — primitives wrap Radix UI for accessibility; MUI v6 provides layout/shell surface.
- Every form field supports optional
langoverride anddiroverride for clinically significant Latin content inside RTL forms.
9. Offline UI patterns
- Sync Status Pill in app shell — states:
online,syncing,offline,paused,conflicts. Click opens Sync Center. - Offline Banner (non-blocking) on partial-offline surfaces — "Writing locally. Will sync when connected."
- Outbox Drawer — pending mutations list with per-aggregate conflict resolution UI.
- Conflict Resolver — side-by-side diff with
Keep Mine / Keep Server / Mergeper per-aggregate policy (see 16 offline-first §5). - Bundle / Cache Manager — storage usage, pinned patients, eviction policy, emergency purge.
- Tamper Warning — modal-blocking when signed sync envelope fails verification; mandatory re-enrol.
- Optimistic UI — mutations apply locally and mark with a pending-dot until reconciled.
10. AI affordances (ai-gateway-service)
Ghasi-eHealth positions AI as explicit, auditable assistance — not autonomous action.
| Rule | Rationale |
|---|---|
| Explicit trigger | No AI action without an explicit user click. Never auto-populate clinical fields. |
| Provenance badge | Every AI-produced artifact shows model, prompt template version, time, and reviewer (if signed). |
| HITL signature | Clinician must accept or reject. Accept = counter-signature. Rejection is logged. |
| Streaming UX | Token-by-token with a visible Cancel button. |
| Refusal UX | Neutral, non-judgmental message; user can rephrase. |
| Local-vs-cloud indicator | When offline AI is used (reduced-capability local model), badge says "Local model — limited accuracy". |
| Cost visibility | Admin view shows per-tenant AI usage and budget. |
| Acceptance pattern | AI suggestions rendered with subtle dashed border until accepted. |
11. Page architecture
- Layouts — App shell (sidebar nav + top bar + sync pill); minimal shell (auth, consent intake); immersive shell (virtual visit room); print shell (clinical document).
- Loading — route-level Suspense + skeletons; never spinners over already-rendered content.
- Empty states — every list has an opinionated empty state with one clear next action, localized to the tenant's likely context (e.g., "No patients waiting. Register new patient").
- Error states — problem+json codes mapped to user-friendly messages with recovery affordance; never raw stack. Never leak PHI into error copy.
- Notifications — toast for ephemeral; in-app inbox for persistent;
communication-servicehandles outbound.
12. Performance budgets
| Page | LCP (p75) | INP | CLS | JS (gzip) |
|---|---|---|---|---|
| Patient portal home | < 1.8 s | < 200 ms | < 0.05 | < 160 kb |
| Clinician chart open | < 2.5 s (PHI-heavy) | < 200 ms | < 0.1 | < 320 kb |
| Registration form | < 1.5 s | < 150 ms | < 0.05 | < 200 kb |
| Virtual care room | < 3 s (WebRTC negotiation in-flight) | — | — | < 450 kb |
| Admin console | < 2.5 s | < 200 ms | < 0.1 | < 380 kb |
Techniques: route-level code-split, dynamic import of heavy editors (rich-text notes, imaging viewer), explicit width/height on hero media, preload only critical font weight, no third-party scripts in clinical shells.
13. Testing
- Component tests: Vitest + Testing Library; behavior-based.
- Visual regression: Playwright screenshots in LTR + RTL + dark + high-contrast across 320, 768, 1024, 1440, 1920.
- E2E: Playwright + Detox (mobile) per journey catalogue in 07 epics §4.
- Accessibility: axe-playwright gate; serious/critical violations block merge.
- Offline:
context.setOffline(true)+ IndexedDB/Realm fixtures + service-worker tests. - Bidi: Every component has an RTL story in Storybook; visual regression in both directions.
14. State management conventions
- Server data: TanStack Query — typed query keys per service; mutations feed the offline outbox adapter on offline-capable surfaces.
- Client UI state: Zustand stores per surface (chart store, order entry store, sync store, virtual-care store).
- URL state: filters, sort, pagination, active tab, patient context live in URL search params.
- Form state: React Hook Form + Zod schemas mirroring backend DTOs.
- No global Redux store.
15. Rationale
A single token set + component system lets 27 services converge on one clinical UX with predictable behaviour in Pashto, Dari, Arabic, and English. Direction-agnostic CSS makes RTL a layout attribute, not a branch. Density modes let the same React tree serve both a patient on a phone and a busy triage nurse at a workstation. Offline, AI, and accessibility are treated as first-class design concerns because in the Afghanistan reference context they are the difference between a platform that works in a district hospital and one that does not.
16. Open questions
- Decide whether the desktop registration station Electron app should host the same Next.js bundle (webview) or ship a tailored React+Vite bundle for startup speed.
- Final form-factor for provider mobile on low-end Android (< 3 GB RAM) in outreach contexts.