From 5d08d9353dc1a9fa06bafff140583636fcd5fa4b Mon Sep 17 00:00:00 2001 From: domverse Date: Thu, 26 Feb 2026 17:30:08 +0100 Subject: [PATCH] Add PRD for design system implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7-phase implementation plan: foundation tokens → shared components → all 5 pages. Defines acceptance criteria per phase, exact file list (17 files changed, 7 new components), dependency notes (lucide-react, clsx, tailwind-merge), and explicit non-goals. No implementation yet. Co-Authored-By: Claude Sonnet 4.6 --- flight-comparator/docs/PRD_DESIGN_SYSTEM.md | 862 ++++++++++++++++++++ 1 file changed, 862 insertions(+) create mode 100644 flight-comparator/docs/PRD_DESIGN_SYSTEM.md diff --git a/flight-comparator/docs/PRD_DESIGN_SYSTEM.md b/flight-comparator/docs/PRD_DESIGN_SYSTEM.md new file mode 100644 index 0000000..b65628f --- /dev/null +++ b/flight-comparator/docs/PRD_DESIGN_SYSTEM.md @@ -0,0 +1,862 @@ +# PRD: Design System Implementation + +**Flight Radar Web Interface — Visual Redesign** + +| | | +|---|---| +| **Version** | 1.0 | +| **Status** | Ready for Implementation | +| **Date** | February 2026 | +| **Design reference** | `docs/DESIGN_SYSTEM.md` | +| **Target** | Claude Code Implementation | + +--- + +## 1. Overview + +The Flight Radar web interface was built as a functional prototype. It works correctly but has no visual design system: navigation links run together into an unreadable string, stat cards are unstyled white boxes, forms lack any grouping or hierarchy, and the application is unusable on mobile devices. + +This PRD specifies the complete implementation of the Material Design 3–inspired design system defined in `docs/DESIGN_SYSTEM.md`. The result must be a production-quality UI that a non-technical user would consider polished and trustworthy. + +**No new features are added in this PRD.** Every existing function is preserved. This is a pure UI/UX replacement. + +--- + +## 2. Goals + +- Implement the full design token system (colours, typography, spacing, elevation) from `docs/DESIGN_SYSTEM.md §3–§6` +- Replace the broken top navigation with a proper sidebar on desktop and a bottom navigation bar on mobile +- Redesign all 5 pages using the component specifications in `docs/DESIGN_SYSTEM.md §7–§8` +- Build a reusable component library that makes future pages easy to build consistently +- Achieve a fully functional, accessible mobile experience at 375px viewport width +- Pass a manual accessibility review: keyboard navigation, colour contrast (WCAG AA), and focus management + +## 3. Non-Goals + +- No new API endpoints or backend changes +- No dark mode (design tokens must be structured to support it later, but it need not be active) +- No animations that require a JS animation library (CSS transitions only) +- No redesign of the `AirportSearch` dropdown's behaviour — only its visual styling +- No changes to `api.ts`, `App.tsx`, routing, or any Python backend file +- No new pages beyond the existing 5 + +--- + +## 4. Success Criteria + +| Criterion | Measure | +|-----------|---------| +| Navigation is readable | Nav items are separated, active state is visually distinct, works on 375px | +| Stats are scannable | Each stat card has an icon, colour accent, and label — no two look identical | +| Scan list is parseable | Destination, status chip, date range, and flight count are each in distinct visual zones | +| Form feels structured | Field groups are visually separated, labels float on focus, help text is present | +| Empty states have actions | Every zero-data view has an illustration, headline, and a CTA button | +| Loading has feedback | Every async wait shows a skeleton or spinner — never a blank area | +| Mobile is functional | All 5 pages are usable at 375px — no horizontal overflow, no overlapping elements | +| Accessibility baseline | All interactive elements ≥ 44×44px, focus rings visible, colour not sole differentiator | + +--- + +## 5. Constraints + +- **Tech stack is fixed:** React 19, TypeScript strict mode, Tailwind CSS v4, Vite 7 +- **No CSS-in-JS:** all styling via Tailwind utility classes + custom CSS variables in `index.css` +- **Tailwind v4 `@theme` syntax:** token definitions use `@theme { }` block, not `tailwind.config.js` +- **Type-only imports:** `import type { X }` required for all interface/type imports (TypeScript strict) +- **No jQuery, no Bootstrap, no external UI framework** (MUI, Chakra, Ant Design etc.) +- **Three allowed new dependencies:** `lucide-react`, `clsx`, `tailwind-merge` + +--- + +## 6. Implementation Phases + +The work is divided into 5 phases that must be executed in order, because each phase depends on the previous one. A phase is complete only when all its acceptance criteria pass. + +--- + +### Phase 1 — Foundation (tokens, fonts, layout shell) + +**Objective:** Every page already looks better before a single page component is touched, because the global tokens and navigation are correct. + +#### 1.1 Design tokens + +**File:** `frontend/src/index.css` + +Replace the existing 3-line Tailwind import with: + +```css +@import "tailwindcss"; + +@theme { + /* Brand colours */ + --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-on-tertiary: #3B2A00; + --color-error: #D93025; + --color-error-container: #FDECEA; + --color-on-error: #FFFFFF; + + /* Surface roles */ + --color-bg: #F8F9FA; + --color-surface: #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; + + /* Semantic status colours (used via CSS variables, not Tailwind classes) */ + --color-status-completed-bg: #E6F4EA; + --color-status-completed-text: #137333; + --color-status-completed-border: #A8D5B5; + --color-status-running-bg: #E8F0FE; + --color-status-running-text: #1557B0; + --color-status-running-border: #A8C7FA; + --color-status-pending-bg: #FEF7E0; + --color-status-pending-text: #7A5200; + --color-status-pending-border: #F9D659; + --color-status-failed-bg: #FDECEA; + --color-status-failed-text: #A50E0E; + --color-status-failed-border: #F5C6C6; + + /* Shape */ + --radius-xs: 4px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 20px; + --radius-full: 9999px; + + /* Typography */ + --font-sans: 'Google Sans', 'Roboto', system-ui, -apple-system, sans-serif; + --font-mono: 'Roboto Mono', 'JetBrains Mono', ui-monospace, monospace; + + /* Elevation 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); +} + +/* Base resets */ +body { + background-color: var(--color-bg); + color: var(--color-on-surface); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; +} + +/* Skeleton shimmer */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +.skeleton { + background: linear-gradient(90deg, #E8EAED 25%, #F1F3F4 50%, #E8EAED 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +/* Slide-up toast animation (keep existing) */ +@keyframes slide-up { + from { transform: translateY(100%); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} +.animate-slide-up { animation: slide-up 0.3s ease-out; } + +/* Spin for loading icons */ +@keyframes spin { + to { transform: rotate(360deg); } +} +.animate-spin-slow { animation: spin 0.6s linear infinite; } + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} +``` + +#### 1.2 Google Fonts + +**File:** `frontend/index.html` + +Add inside ``: + +```html + + + +``` + +#### 1.3 New dependencies + +```bash +npm install lucide-react clsx tailwind-merge +``` + +#### 1.4 Layout shell redesign + +**File:** `frontend/src/components/Layout.tsx` + +Full replacement. The new layout has two distinct structures based on viewport: + +**Desktop (≥ 1024px):** +``` +┌──────────┬──────────────────────────────────────────┐ +│ Sidebar │ Top bar (64px) │ +│ (256px) ├──────────────────────────────────────────┤ +│ │ │ +│ Logo │ wrapped in │ +│ Nav │ max-w-5xl, px-6, py-8 │ +│ │ │ +└──────────┴──────────────────────────────────────────┘ +``` + +**Mobile (< 1024px):** +``` +┌──────────────────────────────────────────────────┐ +│ Top bar (56px): logo + page title │ +├──────────────────────────────────────────────────┤ +│ │ +│ px-4 py-4, pb-24 (room for nav) │ +│ │ +├──────────────────────────────────────────────────┤ +│ Bottom nav bar (56px + safe-area-bottom) │ +└──────────────────────────────────────────────────┘ +``` + +**Sidebar nav item spec:** +- Height: 48px, padding: 0 16px, border-radius: 28px (full bleed within sidebar) +- Active: `bg-primary-container text-on-primary-container font-medium` +- Inactive: `text-on-surface-variant hover:bg-surface-2` +- Icon: 20px Lucide icon, left-aligned, `mr-3` +- The active pill is the full row width minus 12px horizontal margin on each side + +**Bottom nav item spec:** +- 4 items equally spaced +- Active: icon in Primary colour, 32×16px Primary Container pill behind icon, label in Primary +- Inactive: icon + label in `on-surface-variant` +- Label: 12px, 500 weight + +**Nav items and routes:** +``` +Icon Label Route +LayoutDashboard Dashboard / +ScanSearch Scans /scans +MapPin Airports /airports +ScrollText Logs /logs +``` + +**Top bar (desktop):** +- Height: 64px, `bg-surface border-b border-outline shadow-1` +- Left: empty (sidebar is present) +- Center/Left: current page title via `useLocation()` lookup +- Right: `+ New Scan` filled button (navigates to `/scans`), hidden on `/scans` page itself + +**Top bar (mobile):** +- Height: 56px +- Left: ✈ `PlaneTakeoff` icon (20px, Primary) + "Flight Radar" text (title-lg) +- Right: `+ New Scan` icon button (`Plus` icon, 40×40px) on dashboard only + +**Phase 1 acceptance criteria:** +- [ ] Sidebar is visible on desktop, hidden on mobile +- [ ] Bottom nav is visible on mobile, hidden on desktop +- [ ] Active nav item is visually distinct from inactive +- [ ] No horizontal scrollbar at any viewport width ≥ 320px +- [ ] Google Sans font is loading and applied +- [ ] `bg-primary` Tailwind class resolves to `#1A73E8` + +--- + +### Phase 2 — Shared Component Library + +**Objective:** Create all reusable components before touching any page. Pages are then assembled from these building blocks. + +All new component files live in `frontend/src/components/`. + +#### 2.1 `Button.tsx` + +Props: `variant: 'filled' | 'outlined' | 'text' | 'icon'`, `size: 'sm' | 'md'` (default `md`), `loading?: boolean`, `icon?: LucideIcon`, `iconPosition?: 'left' | 'right'`, `disabled?`, `onClick?`, `type?`, `className?`, `children` + +Spec per variant — see `docs/DESIGN_SYSTEM.md §7.2`. + +The `loading` prop replaces children with a 16px spinner icon; button width is preserved via `min-w` matching the expected label width. + +#### 2.2 `StatusChip.tsx` + +Props: `status: 'completed' | 'running' | 'pending' | 'failed'` + +Spec: `docs/DESIGN_SYSTEM.md §7.3`. + +The chip reads its colours directly from the CSS variables `--color-status-{status}-bg` etc., applied inline or via a class map. The `running` status dot pulses using a CSS animation class. + +Icon mapping: +``` +completed → CheckCircle2 (16px) +running → Loader2 (16px, animate-spin-slow) +pending → Clock (16px) +failed → XCircle (16px) +``` + +#### 2.3 `StatCard.tsx` + +Props: `label: string`, `value: number | string`, `icon: LucideIcon`, `variant: 'default' | 'primary' | 'secondary' | 'tertiary' | 'error'`, `trend?: string` + +Spec: `docs/DESIGN_SYSTEM.md §7.4`. + +Icon circle size: 40×40px. Colours per variant: +``` +default #E8F0FE bg, Primary icon +primary #E8F0FE bg, Primary icon +secondary #E6F4EA bg, Secondary icon +tertiary #FEF7E0 bg, Tertiary icon +error #FDECEA bg, Error icon +``` + +Value typography: 28px (headline-md), 400 weight, `text-on-surface`. + +#### 2.4 `EmptyState.tsx` + +Props: `icon: LucideIcon`, `title: string`, `description?: string`, `action?: { label: string; onClick: () => void }` + +Layout: centred column, icon at 64px in a 96×96px circle with `bg-surface-2`, headline-sm title, body-md description in `text-on-surface-variant`, optional filled primary button. + +#### 2.5 `SkeletonCard.tsx` + +Props: `rows?: number` (default 3), `showHeader?: boolean` + +Renders an animated shimmer card placeholder. Uses the `.skeleton` CSS class from Phase 1. + +Variants to export separately: +- `` — matches StatCard dimensions +- `` — matches the scan list card height (72px) +- `` — matches a table row (52px) + +#### 2.6 `SegmentedButton.tsx` + +Props: `options: Array<{ value: string; label: string; icon?: LucideIcon }>`, `value: string`, `onChange: (value: string) => void` + +Spec: `docs/DESIGN_SYSTEM.md §7.7`. + +The active segment shows a `Check` icon (16px) that slides in from the left using a CSS opacity+translate transition. + +#### 2.7 `AirportChip.tsx` + +Props: `code: string`, `onRemove: () => void` + +Spec: `docs/DESIGN_SYSTEM.md §7.8`. + +Shows a `PlaneTakeoff` icon (14px) before the code. The `×` button uses an `X` Lucide icon. + +#### 2.8 `Toast.tsx` (update existing) + +The existing `Toast.tsx` already handles dismissal logic. Update **only** the visual styling to match `docs/DESIGN_SYSTEM.md §7.12`. + +Changes needed: +- New width (360px desktop / full-width mobile), border-radius 12px, shadow-3 +- Position: bottom-right on desktop (`fixed bottom-6 right-6`), bottom-center on mobile +- Four variants: `success`, `error`, `info`, `warning` — each with distinct icon and colour per the design spec +- Keep the existing `animate-slide-up` class + +**Phase 2 acceptance criteria:** +- [ ] All 8 components render without TypeScript errors (`npm run build` passes) +- [ ] `StatusChip` shows correct colour and icon for all 4 statuses +- [ ] `SkeletonCard` shimmers +- [ ] `SegmentedButton` check icon appears on the active segment +- [ ] `Button` loading state preserves button width + +--- + +### Phase 3 — Dashboard page + +**File:** `frontend/src/pages/Dashboard.tsx` + +**Objective:** Replace the current text-dump dashboard with a stats + card-list layout using Phase 2 components. + +#### 3.1 Loading state + +While `loading === true`, render: +- 5× `` in the stats grid +- 5× `` in the recent scans section + +Do **not** show a spinner or "Loading…" text. + +#### 3.2 Stats grid + +Replace the 5 plain white boxes with `` components: + +``` + + + + + +``` + +Grid: `grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4` + +#### 3.3 Recent scans section + +Header row: `"Recent Scans"` (title-lg, `text-on-surface`) + `"View all →"` text button (right-aligned, links to a future full scan list — for now it links to `/scans`). + +Each scan renders as a card (not a ``). Card structure: + +``` +┌──────────────────────────────────────────────────────┐ +│ BDS → FMM, DUS ● completed → │ +│ 26 Feb 2026 – 27 May 2026 · 1 adult · Economy │ +│ 182 routes · 50 flights found │ +└──────────────────────────────────────────────────────┘ +``` + +- Arrow `→` (`ArrowRight`, 16px, `text-on-surface-variant`) right-aligned +- On hover: card lifts (`shadow-2`, `translateY(-2px)`) via CSS transition 150ms +- `` wraps the entire card (no nested interactive elements inside except the chip) +- Date string: `formatDate` replaced with a relative label where applicable (e.g. "Today", "Yesterday", or absolute date) + +#### 3.4 Empty state + +When `scans.length === 0`: +``` + navigate('/scans') }} +/> +``` + +**Phase 3 acceptance criteria:** +- [ ] Skeleton shows while loading, disappears when data arrives +- [ ] Each of 5 stat cards has a distinct icon and colour accent +- [ ] Scan cards are separated, hoverable, and fully clickable +- [ ] Status chip is visually separate from destination text (no more `"FMM,DUScompleted"`) +- [ ] Empty state renders with button when no scans exist + +--- + +### Phase 4 — Create Scan form + +**File:** `frontend/src/pages/Scans.tsx` + +**Objective:** Transform the flat HTML form into a structured, guided form with clear visual grouping. + +#### 4.1 Page structure + +``` +"New Scan" (headline-md) [← Cancel — text button] + +┌ Section: Origin ─────────────────────────────────┐ +│ Origin Airport │ +│ [AirportSearch input — full width] │ +│ Help text: "3-letter IATA code (e.g. BDS)" │ +└───────────────────────────────────────────────────┘ + +┌ Section: Destination ────────────────────────────┐ +│ [ 🌍 By Country | ✈ By Airports ] ← SegmentedButton │ +│ │ +│ [Country input — if country mode] │ +│ [AirportSearch + AirportChip list — if airports] │ +└───────────────────────────────────────────────────┘ + +┌ Section: Parameters ─────────────────────────────┐ +│ [Search Window] [Seat Class] │ +│ [Adults] │ +└───────────────────────────────────────────────────┘ + + [Create Scan — filled button] +``` + +Each `┌ Section ┐` is a `bg-surface rounded-lg shadow-1 p-6` card with a section label in `label-sm uppercase text-on-surface-variant tracking-wider mb-4`. + +#### 4.2 Destination mode toggle + +Replace the two `