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>
1016 lines
35 KiB
Markdown
1016 lines
35 KiB
Markdown
# 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](#1-current-state-audit)
|
||
2. [Design Principles](#2-design-principles)
|
||
3. [Color System](#3-color-system)
|
||
4. [Typography](#4-typography)
|
||
5. [Spacing & Layout Grid](#5-spacing--layout-grid)
|
||
6. [Elevation & Shadow](#6-elevation--shadow)
|
||
7. [Component Library](#7-component-library)
|
||
8. [Page Specifications](#8-page-specifications)
|
||
9. [Motion & Animation](#9-motion--animation)
|
||
10. [Icons](#10-icons)
|
||
11. [Implementation Checklist](#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)
|
||
|
||
```css
|
||
/* 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
|
||
|
||
```css
|
||
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 (600–1024px)**
|
||
- 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: 3–5 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 (tertiary / destructive link)
|
||
|
||
```
|
||
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 |
|
||
|
||
```css
|
||
/* 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.
|
||
|
||
```bash
|
||
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:**
|
||
```tsx
|
||
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
|
||
|
||
```bash
|
||
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`)
|
||
|
||
```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/`.*
|