Files
ciaovolo/flight-comparator/docs/DESIGN_SYSTEM.md
domverse b0a93bf824 Add comprehensive Material Design 3 design system document
Covers: color tokens, typography scale, spacing grid, 13 components
(nav sidebar/bottom, buttons, status chips, stat cards, data table,
forms, segmented toggle, toasts, empty states, skeletons, progress),
page-by-page specs for all 5 views, motion guidelines, Lucide icon
catalogue, and Tailwind v4 implementation checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 17:23:55 +01:00

35 KiB
Raw Permalink Blame History

Flight Radar — Design System

Version 1.0 · February 2026

This document is the authoritative reference for the visual design and UX of the Flight Radar web application. It covers the full design system: principles, tokens, components, and page-level specifications — ready for direct implementation in Tailwind CSS v4 + React.


Table of Contents

  1. Current State Audit
  2. Design Principles
  3. Color System
  4. Typography
  5. Spacing & Layout Grid
  6. Elevation & Shadow
  7. Component Library
  8. Page Specifications
  9. Motion & Animation
  10. Icons
  11. Implementation Checklist

1. Current State Audit

What's broken today

Area Problem Impact
Navigation Nav links butted together with no gaps: renders as DashboardScansAirportsLogs Unreadable, looks broken
Dashboard stats Five identical white boxes with no accent, icon, or trend Zero visual hierarchy
Scan list items Status badge glued to destination: BDS → FMM,DUScompleted Parsing required by user
Form Flat input fields, no grouping, no visual section breaks Feels like a raw HTML form
Mode toggle Full-width button pair looks like a toggle switch gone wrong Confusing affordance
Airports page Empty white canvas with an input box — no state, no branding Dead end feeling
Logs page Wall of monospaced text with no log-level differentiation No scanability
Mobile Nav links overflow and collapse poorly Unusable on small screens
Loading states Plain text "Loading…" or invisible spinner No feedback
Empty states Plain text only, no illustration or call to action Abandoned feeling
Colors Two blues + basic status colours, no system Incoherent palette
Typography Browser default font, no size scale Low credibility
Branding ✈️ emoji as logo Not scalable, not professional

What's good and must be kept

  • Tailwind CSS v4 already installed — just needs design tokens
  • React Router working, page structure sound
  • Component boundaries (AirportSearch, Layout, pages) are clean
  • Status badge color logic is correct
  • Auto-refresh on running scans is a great UX feature
  • Expandable flight rows in the route table are the right pattern

2. Design Principles

1. Data first, chrome second

Flight data is complex (many numbers, dates, airports). The UI must recede and let data breathe. Avoid decorative elements that compete with content.

2. Material Design 3 — adapted, not copied

Use MD3's color system (tonal palettes, surface roles), elevation model (tonal elevation, not drop shadows), and component shapes (rounded corners). Ditch components that don't fit a desktop tool (FABs, bottom nav for desktop).

3. Mobile-first, desktop-optimised

Start every layout from 320 px. Desktop adds a sidebar and wider data tables — it doesn't redesign the page.

4. Instant feedback everywhere

Every async action shows a skeleton, spinner, or progress indicator. Every destructive or slow action is confirmed. Success and error are always communicated via toast.

5. One primary action per view

Each page has exactly one primary CTA (blue filled button). Everything else is secondary or ghost.


3. Color System

Brand palette (MD3 tonal palette — primary hue: blue 220°)

Primary         #1A73E8    (Google Blue — familiar, trustworthy for a data tool)
On Primary      #FFFFFF
Primary Cont.   #D2E3FC    (primary container — used for active nav, chips)
On Primary Cont.#003E8C

Secondary       #0F9D58    (green — price / success / confirmed flights)
On Secondary    #FFFFFF
Secondary Cont. #C8E6C9

Tertiary        #F4B400    (amber — warnings, pending states)
On Tertiary     #3B2A00
Tertiary Cont.  #FFF0C0

Error           #D93025    (red — failed, error)
Error Cont.     #FDECEA

Surface         #F8F9FA    (page background — very light grey)
Surface 1       #FFFFFF    (card background — level 1)
Surface 2       #F1F3F4    (table row alternate / subtle separators)
Surface Variant #E8EAED    (input backgrounds, chip backgrounds)
On Surface      #202124    (primary text)
On Surface Var. #5F6368    (secondary text, labels, metadata)
Outline         #DADCE0    (borders, dividers)
Outline Var.    #F1F3F4    (very subtle separators)

Semantic status colours

Status Background Text Border Usage
completed #E6F4EA #137333 #A8D5B5 Scan finished, route confirmed
running #E8F0FE #1557B0 #A8C7FA Scan in progress
pending #FEF7E0 #7A5200 #F9D659 Queued, not started
failed #FDECEA #A50E0E #F5C6C6 Error state

Dark mode (implement via CSS variables)

/* light (default) */
:root {
  --color-bg: #F8F9FA;
  --color-surface: #FFFFFF;
  --color-surface-2: #F1F3F4;
  --color-on-surface: #202124;
  --color-on-surface-var: #5F6368;
  --color-outline: #DADCE0;
  --color-primary: #1A73E8;
  --color-secondary: #0F9D58;
}

/* dark */
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #1C1B1F;
    --color-surface: #2C2B30;
    --color-surface-2: #36343B;
    --color-on-surface: #E6E1E5;
    --color-on-surface-var: #938F99;
    --color-outline: #49454F;
    --color-primary: #A8C7FA;
    --color-secondary: #81C995;
  }
}

4. Typography

Font stack

font-family: 'Google Sans', 'Roboto', system-ui, -apple-system, sans-serif;
font-family-mono: 'Roboto Mono', 'JetBrains Mono', monospace; /* logs, IATA codes */

Load from Google Fonts: Google Sans (300, 400, 500) + Roboto Mono (400)

Type scale (MD3 aligned)

Token Size Weight Line height Usage
display-sm 36px / 2.25rem 400 44px Page hero (empty states)
headline-lg 32px / 2rem 400 40px
headline-md 28px / 1.75rem 400 36px Page title
headline-sm 24px / 1.5rem 400 32px Section title
title-lg 22px / 1.375rem 500 28px Card title
title-md 16px / 1rem 500 24px List item title, form label
title-sm 14px / 0.875rem 500 20px Table header, tab label
body-lg 16px / 1rem 400 24px Body copy, descriptions
body-md 14px / 0.875rem 400 20px Secondary body, list metadata
body-sm 12px / 0.75rem 400 16px Captions, helper text
label-lg 14px / 0.875rem 500 20px Button labels
label-md 12px / 0.75rem 500 16px Badge labels, chip labels
label-sm 11px / 0.6875rem 500 16px Overlines, micro labels
mono-md 13px / 0.8125rem 400 20px IATA codes, log messages

5. Spacing & Layout Grid

Spacing scale (4px base)

1 →  4px    (tight gaps: icon-to-text, badge padding)
2 →  8px    (small gaps: form field helper text)
3 →  12px   (medium-small: list item inner padding)
4 →  16px   (base unit: card inner padding mobile)
5 →  20px   (—)
6 →  24px   (card inner padding desktop, section gaps)
8 →  32px   (between major sections)
10 → 40px   (—)
12 → 48px   (page top padding)
16 → 64px   (large empty state illustrations)

Layout grid

Mobile (< 600px)

  • 1 column, 16px margins, 16px gutters
  • Full-width cards
  • Bottom navigation bar (56px)

Tablet (6001024px)

  • 8 columns, 24px margins, 24px gutters
  • Side rail navigation (72px collapsed)
  • Cards: 2-column grid for stats

Desktop (> 1024px)

  • 12 columns, 24px margins, 24px gutters
  • Fixed sidebar navigation (256px)
  • Max content width: 1280px
  • Cards: 35 column grid for stats

Sidebar layout (desktop)

┌──────────┬────────────────────────────────┐
│          │  Top bar (64px, search + user) │
│ Sidebar  ├────────────────────────────────┤
│ (256px)  │                                │
│          │  Main content area             │
│  Logo    │  max-w: 1024px, centered       │
│  Nav     │  px: 24px                      │
│  items   │                                │
│          │                                │
│  ──────  │                                │
│  Logs    │                                │
└──────────┴────────────────────────────────┘

6. Elevation & Shadow

MD3 uses tonal elevation (surface tint) as primary depth cue, with subtle shadows for resting state only.

Level Shadow Tint overlay Usage
0 none 0% Page background
1 0 1px 2px rgba(0,0,0,.08) 5% primary Cards (resting)
2 0 2px 6px rgba(0,0,0,.10) 8% primary Dropdowns, popovers
3 0 4px 12px rgba(0,0,0,.12) 11% primary Modals, dialogs
4 0 6px 20px rgba(0,0,0,.14) 12% primary Navigation drawer

7. Component Library

7.1 Navigation

Sidebar (desktop ≥ 1024px)

Width: 256px (expanded) / 72px (collapsed — future)
Background: Surface (white)
Border-right: 1px solid Outline (#DADCE0)

Logo area (64px tall):
  ✈ icon (24px, Primary blue) + "Flight Radar" (title-lg, On Surface)

Nav item (48px tall, full width):
  - Inactive: transparent bg, On Surface Var text, 16px side padding
  - Active: Primary Container bg (#D2E3FC), On Primary Container text,
            bold icon, rounded 28px pill shape (full bleed)
  - Hover: Surface 2 bg, On Surface text
  - Icon: 20px, left-aligned
  - Label: body-md 500 weight, 12px left of icon

Section divider:
  Between main items and Logs: 1px divider + "Developer" label (label-sm, muted)

Items (in order):
  🏠 Dashboard      → /
  🔍 Scans          → /scans
  ✈  Airports       → /airports
  ── (divider)
  📋 Logs           → /logs

Top app bar (desktop)

Height: 64px
Background: Surface (white)
Border-bottom: 1px solid Outline
Shadow: Level 1 on scroll

Left: (sidebar occupies)
Center: Page title (headline-sm, On Surface) — OR breadcrumb
Right: + New Scan button (filled, primary)

Bottom navigation (mobile < 600px)

Height: 80px (includes safe area)
Background: Surface
Border-top: 1px solid Outline
Shadow: Level 2 (upward)

4 items: Dashboard | Scans | Airports | Logs
Each: icon (24px) + label (label-md) stacked
Active: Primary colour icon + Primary Container indicator pill behind icon (56×32px)
Inactive: On Surface Var colour

7.2 Buttons

Filled (primary action — one per page)

Background: Primary (#1A73E8)
Text: On Primary (#FFFFFF)
Height: 40px
Padding: 0 24px
Border-radius: 20px (fully rounded — MD3 style)
Font: label-lg (14px/500)
Shadow: Level 1
Hover: darken 8% + Level 2 shadow
Pressed: darken 12%, Level 1
Disabled: 38% opacity

Icons: optional leading icon at 18px

Outlined (secondary action)

Border: 1px solid Outline (#DADCE0)
Text: Primary (#1A73E8)
Background: transparent
Same sizing/radius as filled
Hover: Primary Container bg (#D2E3FC)
Text: Primary or Error
No border, no background
Hover: surface tint
Padding: 0 12px

Icon button

Size: 40×40px
Shape: circle
Background: transparent → Surface 2 on hover
Icon: 20px, On Surface Var → On Surface on hover

7.3 Status Chip / Badge

Height: 24px
Padding: 0 10px
Border-radius: 12px (pill)
Font: label-md (12px/500)
Border: 1px solid (matching border column from §3)

┌──────────────────────┐
│  ● completed         │  green bg, green text, green dot
│  ↻ running           │  blue bg, blue text, spinning dot (CSS animation)
│  ⧖ pending           │  amber bg, amber text, static dot
│  ✕ failed            │  red bg, red text, X icon
└──────────────────────┘

Implementation: The colored dot/icon is a 6px circle or SVG icon on the left at 8px margin-right.


7.4 Stat Card

Used on Dashboard (5 cards) and Scan Details (3 cards).

┌────────────────────────┐
│ ┌──┐                   │
│ │ 🔍│  Total Scans     │  ← icon in tinted circle (40px), label (body-sm, muted)
│ └──┘                   │
│                        │
│  54                    │  ← number (display-sm or headline-md)
│                        │
│  +3 today              │  ← optional trend (body-sm, secondary/error)
└────────────────────────┘

Padding: 24px
Border-radius: 16px (MD3 large shape)
Background: Surface (white)
Shadow: Level 1
Width: fluid (grid cell)

Icon circle: 40×40px, border-radius 20px
  Total: #E8F0FE bg, Primary icon
  Pending: #FEF7E0 bg, Tertiary icon
  Running: #E8F0FE bg, Primary icon (animated pulse)
  Completed: #E6F4EA bg, Secondary icon
  Failed: #FDECEA bg, Error icon

7.5 List / Scan Card

Replaces the current flat <Link> row in the dashboard.

┌──────────────────────────────────────────────────────────┐
│  BDS → FMM, DUS              ● completed       25.2.2026  │
│  26 Feb 2026  27 May 2026 · 1 adult · Economy           │
│                                                           │
│  182 routes  ·  50 flights found              →          │
└──────────────────────────────────────────────────────────┘

Padding: 16px 20px
Border-radius: 12px (on hover, or always)
Background: Surface
Hover: Surface 2 bg + Level 2 shadow (lift effect)
Divider: 1px inside, OR 8px gap between cards (prefer cards)
Cursor: pointer

Route arrow (→): On Surface Var, shifts right on hover (transform)

Status chip: right-aligned in first row, always visible
Date metadata: body-sm, On Surface Var
Stats line: body-sm, On Surface Var, shown only when > 0

7.6 Text Input & Form Field

MD3 "Outlined" input variant.

Height: 56px
Border: 1px solid Outline
Border-radius: 4px (corners) — MD3 uses 4px for inputs (not pills)
Background: transparent (on Surface bg it appears inset)
Padding: 16px

Labels: floating label (HTML standard — use <label> + peer CSS)
  - Default: body-md, On Surface Var, positioned at 50% vertical
  - Focused / has value: body-sm, Primary colour, floats to top border
  - Error: body-sm, Error colour

Focus ring: 2px solid Primary (replaces native outline)
Error state: border → Error, label → Error, helper text → Error

Helper text: body-sm, On Surface Var, 4px below input

Select: same styling, chevron icon replaces browser default
  (use appearance-none + background-image SVG chevron)

Number input: hide browser spin buttons via CSS, add custom +/- icon buttons
  on right side (16px icon, icon-button style)

7.7 Toggle / Segmented Button

Replaces the current "Search by Country / Search by Airports" two-button row.

MD3 Segmented Button:

┌─────────────────────┬─────────────────────┐
│  🌍  By Country     │  ✈  By Airports     │
└─────────────────────┴─────────────────────┘

Height: 40px
Border: 1px solid Outline, border-radius: 20px (outer container)
Inner divider: 1px solid Outline

Active segment:
  Background: Secondary Container (#C8E6C9)
  Text: On Secondary Container (#00391A)
  Checkmark icon: 18px, appears left of label with slide-in

Inactive segment:
  Background: transparent
  Text: On Surface
  Hover: Surface 2

Font: label-lg

7.8 Airport Chip (selected airport tags)

Replaces current blue rounded-full divs.

MD3 Input Chip:

┌──────────────────┐
│  ✈ BDS     ✕    │
└──────────────────┘

Height: 32px
Padding: 0 8px 0 12px
Border-radius: 8px
Border: 1px solid Outline
Background: Surface 2
Font: label-lg (14px/500)
Icon left: 16px airport icon, On Surface Var colour
✕ button right: 18px, On Surface Var → Error on hover

On hover: Level 1 shadow, border → Primary

7.9 Data Table

Used in Scan Details (routes) and Airports (search results).

Header row:
  Background: Surface 2 (#F1F3F4)
  Text: label-sm (11px/500, uppercase, tracking-wider)
  Colour: On Surface Var
  Padding: 12px 16px
  Sortable columns: trailing sort icon (chevron-up / chevron-down, 16px)
    Active sort column: Primary colour icon + underline

Body row:
  Height: 52px (dense) / 64px (comfortable — use comfortable)
  Padding: 0 16px
  Border-bottom: 1px solid Outline Var
  Hover: Surface 2 bg (transition 150ms)
  Cursor: pointer (if clickable)

Expanded sub-row:
  Background: #F8FDF9 (very light green tint — differentiates from route rows)
  Indented 32px on first column
  Sub-table: label-sm headers, body-md cells
  Price column: Secondary colour (#0F9D58), 500 weight
  Transition: smooth height expand (max-height animation)

Price cell:
  Font: body-md 500
  Colour: Secondary (#0F9D58) — min price
  Colour: On Surface Var — avg/max price
  Align: right (numbers should be right-aligned)

IATA code cell:
  Font: mono-md, Primary colour
  Background: Primary Container chip inline

Pagination controls:
  Previous / Next: Outlined buttons (smaller: height 32px)
  Page indicator: body-sm, On Surface Var
  Positioned: space-between, 16px top padding

7.10 Empty States

Every zero-data state needs an illustration + message + action.

Layout: centered column, 64px top padding

┌──────────────────────────────────────────┐
│                                          │
│          [SVG illustration 160px]        │
│                                          │
│         No scans yet                     │  headline-sm
│                                          │
│   Create your first scan to discover     │  body-md, On Surface Var
│   flight routes and prices.             │
│                                          │
│         [+ Create Scan]                  │  Filled primary button
│                                          │
└──────────────────────────────────────────┘

Illustrations (inline SVG, use a consistent line style):
  - Dashboard empty: airplane on runway
  - No results: magnifying glass, nothing found
  - Scan failed: broken connection / cloud with X
  - No logs: clipboard empty

7.11 Loading States

Skeleton screens (preferred over spinners for content)

Shimmer animation: linear-gradient sliding left→right, 1.5s loop
Colour: #E8EAED → #F1F3F4 → #E8EAED

Dashboard skeleton:
  - 5 stat card rectangles (full size, rounded-16)
  - 8 list item rows (height 72px, with inner shape blocks)

Scan detail skeleton:
  - 3 stat cards
  - Table with 5 ghost rows

Pattern: use <div className="animate-pulse bg-surface-2 rounded-{n}" style={{height, width}} />

Progress bar (running scan)

Height: 4px (slim, at very top of progress card)
Background: Primary Container
Fill: Primary (#1A73E8)
Border-radius: 2px
Animation: ease-in-out width transition on each poll

Card around progress:
  Subtle pulsing border: 1px solid Primary, animation 1s ease-in-out infinite
  Label: "Scanning routes..." with spinning icon
  Sub-label: "23 / 182 routes · auto-refreshing"

Inline spinner (button loading state)

16px × 16px, Primary colour (white when inside filled button)
Animation: 600ms linear spin
Replaces button label text; button stays same width

7.12 Toast Notifications

Replaces the current inline error/success banners in the form.

Position: bottom-right (desktop), bottom-center (mobile)
Width: 360px (desktop), calc(100vw - 32px) (mobile)
Border-radius: 12px
Shadow: Level 3

┌────────────────────────────────────────┐
│  ✓  Scan created successfully! ID: 54  │  [✕]
└────────────────────────────────────────┘

Variants:
  Success: Secondary bg (#E6F4EA), Secondary icon, On Surface text
  Error:   Error Container bg (#FDECEA), Error icon, On Surface text
  Info:    Primary Container bg (#D2E3FC), Info icon, On Surface text
  Warning: Tertiary Container bg (#FFF0C0), Warning icon, On Surface text

Duration: 4s auto-dismiss (success), 8s (error), manual dismiss button always
Animation: slide up from bottom + fade in (300ms), fade out (200ms)

7.13 Progress Indicator (Scan Running)

Replaces the current progress bar card.

┌────────────────────────────────────────────────────────┐
│  ↻  Scanning in progress...                            │
│  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━     38% │
│  23 of 60 routes  ·  Auto-refreshing every 3s          │
└────────────────────────────────────────────────────────┘

Card: Surface, Level 1 shadow, border-radius 16px
Border: 1px animated Primary (pulse)
Progress bar: slim (4px), Primary fill, Surface 2 track
Percentage: body-sm, right-aligned
Icon: spinning ↻ in Primary colour, 20px

8. Page Specifications

8.1 Dashboard

Layout: sidebar + main content area with max-w-5xl

Header:

Row: "Dashboard" (headline-md) + "+ New Scan" (filled button, right)

Stats row: 5 columns on desktop, 2+3 on tablet, 1 col on mobile

┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
│ 🔍 Total     │ ⧖ Pending    │ ↻ Running    │ ✓ Completed  │ ✕ Failed     │
│ 54           │ 0            │ 0            │ 7            │ 3            │
└──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘

Recent Scans section:

  • Section title "Recent Scans" + "View all →" link (text button, right)
  • Card list (not a table) — each item is a Scan Card (§7.5)
  • 10 items max, then "View all" pagination

Empty state: when no scans exist, show centered empty state illustration (§7.10)


8.2 Create Scan (Scans page)

Layout: narrow centered form, max-w-2xl, on Surface background with Surface card

Page title: "New Scan" (headline-md)

Form structure (vertical stepper feel — not a real wizard, just visual grouping):

Section 1: Origin
  ┌───────────────────────────────────┐
  │  Origin Airport                   │
  │  [BDS ▼ Brindisi Airport    ✕]   │  ← AirportSearch as filled input chip
  └───────────────────────────────────┘

Section 2: Destination
  ┌─── Segmented Button ─────────────┐
  │  🌍 By Country  |  ✈ By Airports │
  └───────────────────────────────────┘

  [if country mode]
  ┌───────────────────────────────────┐
  │  Country Code                     │
  │  DE ▼  (with dropdown of common) │
  └───────────────────────────────────┘

  [if airports mode]
  [AirportSearch] → produces chips below
  [BDS ✕] [FMM ✕] [DUS ✕]

Section 3: Search Parameters
  ┌───────────────┬───────────────────┐
  │ Window        │ Seat Class        │
  │ [3 months +-] │ [Economy ▼]       │
  └───────────────┴───────────────────┘
  ┌───────────────────────────────────┐
  │ Adults  [1 +-]                    │
  └───────────────────────────────────┘

Actions (bottom, right-aligned):
  [Cancel — outlined] [Create Scan — filled]

Validation feedback: red outlined input + error helper text below field (not a banner)


8.3 Scan Details

Breadcrumb: ← Dashboard / Scan #54

Header card:

┌────────────────────────────────────────────────────────────────┐
│  BDS → FMM, DUS                              ● completed       │
│  26 Feb 2026  27 May 2026  ·  1 adult  ·  Economy            │
│                                                                │
│  Created: 25 Feb 2026, 20:51                                   │
└────────────────────────────────────────────────────────────────┘

Stats row (3 cards):

┌────────────┬──────────────┬──────────────┐
│ Total      │ Routes       │ Total        │
│ Routes     │ Scanned      │ Flights      │
│ 182        │ 182          │ 50           │
└────────────┴──────────────┴──────────────┘

Progress card (only when running/pending): see §7.13

Routes table:

  • Sortable columns: Destination, Flights, Min Price (default sort: Min Price ASC)
  • Each route row: expandable → sub-table of individual flights
  • Expand icon: chevron-right → chevron-down (16px, smooth rotate animation)
  • IATA code in chip style (inline, Primary Container)
  • Min price highlighted in Secondary green
  • Sub-table background: very light green tint

Mobile table: horizontal scroll, sticky first column (Destination)


8.4 Airports

Page title: "Airports" + helper text "Search the airport database"

Search bar: full-width, 56px tall, with leading search icon (16px, On Surface Var)

Results:

  • List card (not table on mobile, table on desktop)
  • Desktop table: IATA | Airport Name | City | Country | [Copy] button
  • IATA cell: mono-md, Primary colour, fixed 48px wide
  • Copy button → icon button (copy icon), shows "Copied!" tooltip 2s

Empty state on load: show popular airports or a "Start typing to search" message with search icon illustration


8.5 Logs

Page title: "System Logs" + auto-refresh toggle (icon button, top right)

Filters row (horizontal):

[All Levels ▼]   [Search messages...    🔍]   [Clear ✕]

Log list:

  • Monospaced font for message text (mono-md)
  • Level badge: left-aligned, fixed 72px wide (fills the badge slot)
  • Timestamp: body-sm, right-aligned, fixed width
  • Module/Function/Line: body-sm, On Surface Var, inline separated by ·
  • Log message: body-md, mono-md font, wraps on overflow
┌─────────────────────────────────────────────────────────────────────┐
│ INFO   Flight Radar API v2.0 startup complete          22:57:33     │
│        api_server · lifespan · line 310                             │
├─────────────────────────────────────────────────────────────────────┤
│ ERROR  Connection timeout on BDS→JFK                  22:57:31     │
│        searcher_v3 · search_route · line 142                        │
└─────────────────────────────────────────────────────────────────────┘
  • ERROR rows: very subtle red-tinted row background
  • WARNING rows: very subtle amber-tinted row background

9. Motion & Animation

All transitions respect prefers-reduced-motion.

Interaction Duration Easing Notes
Page transition 200ms ease-out Fade + 8px slide up
Card hover lift 150ms ease-out Shadow level 1→2, translateY(-2px)
Button press 100ms ease-in Scale 0.98
Expand route row 250ms ease-in-out max-height 0→auto
Toast enter 300ms cubic-bezier(0.34,1.56,0.64,1) Spring slide up
Toast exit 200ms ease-in Fade + slide down
Spinner 600ms linear infinite rotate
Progress bar 300ms ease-in-out width transition
Skeleton shimmer 1500ms ease-in-out background-position loop
Status dot pulse 1000ms ease-in-out opacity 1→0.4→1 (running only)
Nav active pill 200ms ease-out width/position transition
/* Reduced motion override */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

10. Icons

Use Lucide React — lightweight, consistent, MIT licensed, tree-shakeable.

npm install lucide-react

Icon catalogue

Context Icon Lucide name
Dashboard nav LayoutDashboard LayoutDashboard
Scans nav ScanSearch ScanSearch
Airports nav MapPin MapPin
Logs nav ScrollText ScrollText
New scan Plus Plus
Search Search Search
Sort asc ChevronUp ChevronUp
Sort desc ChevronDown ChevronDown
Expand row ChevronRight ChevronRight
Copy Copy Copy
Close / remove X X
Status: completed CheckCircle2 CheckCircle2
Status: running Loader2 (animated) Loader2
Status: pending Clock Clock
Status: failed XCircle XCircle
Price (min) TrendingDown TrendingDown
Flight / origin PlaneTakeoff PlaneTakeoff
Route arrow ArrowRight ArrowRight
Back button ArrowLeft ArrowLeft
Auto-refresh RefreshCw RefreshCw
Adults Users Users
Seat class Armchair Armchair
Calendar Calendar Calendar
Error AlertCircle AlertCircle
Warning AlertTriangle AlertTriangle
Info Info Info

Usage pattern:

import { CheckCircle2 } from 'lucide-react';
<CheckCircle2 size={20} className="text-secondary" aria-hidden="true" />

Always add aria-hidden="true" for decorative icons. Use aria-label for icon-only buttons.


11. Implementation Checklist

Dependencies to add

npm install lucide-react
# Optional but recommended:
npm install clsx           # conditional class merging
npm install tailwind-merge # safe Tailwind class merging

Tailwind v4 custom tokens (add to index.css)

@import "tailwindcss";

@theme {
  /* Colors */
  --color-primary: #1A73E8;
  --color-primary-container: #D2E3FC;
  --color-on-primary: #FFFFFF;
  --color-on-primary-container: #003E8C;
  --color-secondary: #0F9D58;
  --color-secondary-container: #C8E6C9;
  --color-on-secondary: #FFFFFF;
  --color-on-secondary-container: #00391A;
  --color-tertiary: #F4B400;
  --color-tertiary-container: #FFF0C0;
  --color-error: #D93025;
  --color-error-container: #FDECEA;
  --color-surface: #FFFFFF;
  --color-surface-1: #FFFFFF;
  --color-surface-2: #F1F3F4;
  --color-surface-variant: #E8EAED;
  --color-on-surface: #202124;
  --color-on-surface-variant: #5F6368;
  --color-outline: #DADCE0;
  --color-outline-variant: #F1F3F4;
  --color-bg: #F8F9FA;

  /* Border radius */
  --radius-xs: 4px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-full: 9999px;

  /* Shadows */
  --shadow-1: 0 1px 2px rgba(0,0,0,.08);
  --shadow-2: 0 2px 6px rgba(0,0,0,.10);
  --shadow-3: 0 4px 12px rgba(0,0,0,.12);

  /* Typography */
  --font-sans: 'Google Sans', 'Roboto', system-ui, sans-serif;
  --font-mono: 'Roboto Mono', 'JetBrains Mono', monospace;
}

Files to create / update (implementation order)

  1. index.css — design tokens (above)
  2. index.html — add Google Fonts link for Google Sans + Roboto Mono
  3. src/components/Layout.tsx — sidebar + bottom nav + top bar
  4. src/components/StatusChip.tsx — new reusable status badge
  5. src/components/StatCard.tsx — new reusable stat card
  6. src/components/Button.tsx — Button component with variants
  7. src/components/Input.tsx — outlined input with floating label
  8. src/components/Toast.tsx — update existing to new design
  9. src/components/EmptyState.tsx — new component
  10. src/components/SkeletonCard.tsx — loading skeleton
  11. src/pages/Dashboard.tsx — use new components
  12. src/pages/Scans.tsx — stepper form layout
  13. src/pages/ScanDetails.tsx — new table + expand animation
  14. src/pages/Airports.tsx — table + better empty state
  15. src/pages/Logs.tsx — monospaced log rows + level colours

Accessibility requirements

  • All interactive elements: minimum 44×44px touch target
  • Focus rings: 2px solid Primary, 2px offset (visible on all backgrounds)
  • Color is never the only differentiator (icons + text confirm status)
  • aria-live="polite" on dynamic regions (scan progress, search results)
  • role="status" on loading indicators
  • Keyboard navigation: Tab order matches visual order; Escape closes modals/dropdowns
  • Screen reader labels on all icon-only buttons
  • Table headers with scope="col" and sortable aria-sort attributes

This design system is a living document. Update it when adding new components or changing tokens. The canonical implementation lives in frontend/src/.