From 0a68c2a5a5142c0b6bb85837984ce43d1faf9f04 Mon Sep 17 00:00:00 2001 From: Andrew Charlwood Date: Thu, 5 Feb 2026 01:46:58 +0000 Subject: [PATCH] feat: update design tokens for SaaS redesign (Task 5.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Typography: Reduce sizes (Display 32→28, H1 24→18, H2 20→16, Caption 12→11) - Spacing: Tighten scale by ~25% (SM 8→6, MD 12→8, LG 16→12, etc.) - Shadows: Lighter values for modern feel - Colors: Modernize semantic colors (#10B981 success, #EF4444 error) - Layout: TOP_BAR_HEIGHT 64→48px, new FILTER_STRIP_HEIGHT 48px New style helpers added: - compact_kpi_card_style/value/label - 50% smaller KPI cards - kpi_badge_style - inline pill variant for zero-height KPIs - filter_strip_style - horizontal single-row container - compact_dropdown_trigger_style - 32px height triggers - chart_container_style/wrapper - full-width flex-grow - top_bar_style/tab/logo - compact 48px top bar All tokens verified via import and Reflex compile. --- IMPLEMENTATION_PLAN.md | 340 ++++++++++++------------------- pathways_app/styles.py | 449 +++++++++++++++++++++++++++++++++-------- 2 files changed, 493 insertions(+), 296 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 7f2a66e..e9c0eec 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -1,22 +1,23 @@ -# Implementation Plan - Pathway Data Architecture +# Implementation Plan - UI Redesign Phase ## Project Overview -Pre-compute patient treatment pathways from Snowflake and store in SQLite for fast Reflex filtering. This replaces the current simplified `prepare_chart_data()` with full pathway hierarchy support. +Redesign the HCD Analysis application with a modern SaaS-style interface. The current NHS-dashboard style is functional but dated. This phase focuses on: -**Architecture**: Snowflake → Pathway Processing → SQLite (pre-computed) → Reflex (filter & view) +1. **Modern SaaS aesthetic** - Clean, ambitious design that looks like a premium product +2. **Maximized chart space** - The icicle chart is the hero; everything else supports it +3. **Compact controls** - Filters and KPIs should be efficient, not sprawling +4. **Full-width layout** - Use the entire viewport width -**Key Benefits**: -- Performance: Pathway calculation done once during data refresh, not on every filter -- Simplicity: Reflex filters pre-computed data with simple SQL WHERE clauses -- Full Pathways: Sequential treatment pathways (drug_0 → drug_1 → drug_2...) with statistics +**Design Philosophy**: +- Thematically aligned with blue color scheme but NOT constrained by NHS branding +- Think Stripe, Linear, Vercel - clean, spacious, confident +- Data visualization is the star; chrome should be minimal -**Design Reference**: See `PATHWAY_DATA_ARCHITECTURE_PLAN.md` for detailed architecture, schema, and data flow. - -**Source Code**: -- Existing analysis: `analysis/pathway_analyzer.py` -- Existing visualization: `visualization/plotly_generator.py` -- Existing Reflex app: `pathways_app/app_v2.py` +**Source Files**: +- `pathways_app/pathways_app.py` - Main Reflex application +- `pathways_app/styles.py` - Design tokens and style helpers +- `DESIGN_SYSTEM.md` - Design specifications ## Quality Checks @@ -24,222 +25,145 @@ Run after each task: ```bash # Syntax check for Python files -python -m py_compile +python -m py_compile pathways_app/pathways_app.py # Import verification -python -c "from import " +python -c "from pathways_app.pathways_app import app" -# For Reflex changes -cd pathways_app && timeout 60 python -m reflex run 2>&1 | head -30 +# Reflex compile +python -m reflex compile ``` -## Phase 1: Schema & Data Pipeline Foundation +## Phase 5: UI Redesign -### 1.1 Extend Database Schema -- [x] Add `pathway_date_filters` table with 6 pre-defined combinations: - - `all_6mo`, `all_12mo`, `1yr_6mo`, `1yr_12mo`, `2yr_6mo`, `2yr_12mo` -- [x] Add `pathway_nodes` table with: - - Hierarchy structure (parents, ids, labels, level) - - Patient counts and costs (value, cost, costpp, cost_pp_pa) - - Date ranges (first_seen, last_seen, first_seen_parent, last_seen_parent) - - Treatment statistics (average_spacing, average_administered, avg_days) - - Denormalized filter columns (trust_name, directory, drug_sequence) - - Foreign key to date_filter_id -- [x] Add `pathway_refresh_log` table for tracking refresh status -- [x] Create indexes for efficient filtering -- [x] Verify schema with: `python -c "from data_processing.schema import *"` +### 5.1 Update Design System for Modern SaaS +- [x] Update `DESIGN_SYSTEM.md` with new specifications: + - Reduce top bar height from 64px to 48px + - Define compact filter row (single horizontal strip) + - Define compact KPI card dimensions (reduce padding, font sizes) + - Add full-width chart container specs +- [x] Update `pathways_app/styles.py` tokens to match: + - Typography: DISPLAY 32→28px, H1 24→18px, H2 20→16px, CAPTION 12→11px + - Spacing: SM 8→6px, MD 12→8px, LG 16→12px, XL 24→16px, XXL 32→24px, XXXL 48→32px + - Shadows: Lighter values (0.04, 0.06, 0.08 opacity) + - Colors: Modernized semantic colors (SUCCESS #10B981, etc.) + - Layout: TOP_BAR_HEIGHT 64→48px, FILTER_STRIP_HEIGHT 48px +- [x] Add new style helpers: + - `compact_kpi_card_style()` - 12px padding, 24px value font + - `compact_kpi_value_style()` / `compact_kpi_label_style()` - matching text styles + - `kpi_badge_style()` - inline pill/badge variant (zero height impact) + - `filter_strip_style()` - 48px horizontal container + - `compact_dropdown_trigger_style()` - 32px height triggers + - `searchable_dropdown_panel_style()` / `searchable_dropdown_item_style()` - compact items + - `chart_container_style()` / `chart_wrapper_style()` - full-width, flex-grow + - `top_bar_style()` / `top_bar_tab_style()` / `logo_style()` - 48px top bar +- [x] Verify: `python -c "from pathways_app.styles import *"` - PASSED -### 1.2 Create Pathway Pipeline Module -- [x] Create `data_processing/pathway_pipeline.py` with: - - `fetch_and_transform_data()` - Snowflake fetch + UPID/drug/directory transformations - - `process_pathway_for_date_filter(df, date_filter_config)` - Single filter processing - - `extract_denormalized_fields(ice_df)` - Extract trust, directory, drug_sequence from ids - - `convert_to_records(ice_df, date_filter_id)` - Convert ice_df to list of dicts for SQLite -- [x] Integrate with existing `analysis/pathway_analyzer.py` functions -- [x] Verify: `python -c "from data_processing.pathway_pipeline import *"` +### 5.2 Compact Filter Section (50-67% height reduction) +- [ ] Redesign filter_section() as a single horizontal strip: + - All filters in ONE row: Date dropdowns | Drugs | Indications | Directorates + - Remove "Filters" header (saves vertical space) + - Use smaller dropdown triggers (height: 32px instead of 40px) + - Use icon-only labels where possible +- [ ] Reduce searchable_dropdown() panel heights: + - Max item list height: 150px (was 200px) + - Smaller search input + - Tighter spacing (4px gaps instead of 8px) +- [ ] Make filter dropdowns collapsible/expandable (optional advanced feature) +- [ ] Verify: Filter section height ≤ 60px when collapsed -### 1.3 Create Migration Script -- [x] Create script to set up new tables in existing `data/pathways.db` - - Note: Existing `python -m data_processing.migrate` handles this (updated in Task 1.1) -- [x] Pre-populate `pathway_date_filters` with 6 combinations - - Note: Auto-populated via INSERT OR REPLACE in PATHWAY_DATE_FILTERS_SCHEMA -- [x] Verify migration runs cleanly on fresh database - - Verified: All 3 pathway tables created, 6 date filters populated correctly +### 5.3 Compact KPI Cards (50% reduction) +- [ ] Reduce KPI card dimensions: + - Padding: 12px (was 24px) + - Value font size: 24px (was 32px) + - Label font size: 11px (was 12px) +- [ ] Make KPIs a single compact row: + - All 4 KPIs in horizontal strip + - Minimal vertical footprint + - Consider inline layout: "12,345 patients | £45.2M cost | 89 drugs | 7 trusts" +- [ ] Alternative: KPI badges/pills instead of cards +- [ ] Verify: KPI row height ≤ 48px -## Phase 2: CLI Refresh Command +### 5.4 Full-Width Chart Layout +- [ ] Remove PAGE_MAX_WIDTH constraint for chart container +- [ ] Chart should stretch to viewport width (with small padding: 16px each side) +- [ ] Update chart height calculation: + - Use CSS calc() or flex-grow to fill remaining viewport height + - Minimum height: 500px + - Target: viewport height minus (top bar + filters + KPIs + padding) +- [ ] Update Plotly layout: + - Remove fixed height=600, use responsive sizing + - Reduce margins further for maximum chart area +- [ ] Verify: Chart fills available space on 1920x1080 display -### 2.1 Create Refresh Command -- [x] Create `cli/refresh_pathways.py` with: - - Uses DATE_FILTER_CONFIGS and compute_date_ranges from pathway_pipeline.py - - `refresh_pathways(minimum_patients, provider_codes, ...)` main function - - `insert_pathway_records()` for SQLite insertion - - `log_refresh_start/complete/failed()` for refresh tracking -- [x] Implement refresh flow: - 1. Fetch ALL data from Snowflake (full date range) via fetch_and_transform_data() - 2. Apply transformations (UPID, drug names, directory) - handled by pipeline - 3. Clear existing pathway_nodes via clear_pathway_nodes() - 4. For each of 6 date filter configs: filter → process → insert - 5. Update pathway_refresh_log -- [x] Add CLI argument parsing (--minimum-patients, --provider-codes, --dry-run, --verbose) -- [x] Verify: `python -m cli.refresh_pathways --help` +### 5.5 Top Bar Refinement +- [ ] Reduce top bar height to 48px (was 64px) +- [ ] Simplify chart tabs - smaller pills or just text links +- [ ] Consider moving data freshness indicator inline with filters +- [ ] Make logo smaller (28px instead of 36px) +- [ ] Verify: Top bar is minimal but functional -### 2.2 Test Refresh Pipeline -- [x] Run refresh with Snowflake data - - Successfully fetched 656,695 records from Snowflake in ~7s - - Transformed to 519,848 records after UPID/drug/directory processing -- [x] Verify all 6 date_filter_ids populated in pathway_nodes - - Note: Only `all_6mo` has data (293 nodes) due to test data freshness - - Other filters (all_12mo, 1yr_*, 2yr_*) have no matching data in current Snowflake snapshot - - This is expected — the pipeline works, data just doesn't match date filters -- [x] Verify pathway structure matches original `generate_icicle_chart()` output - - Structure verified: N&WICS - TRUST - DIRECTORY - DRUG - PATHWAY levels - - 8 trusts, 14 directories represented correctly -- [x] Verify patient counts are correct (compare with original app) - - Sample: QEH RHEUMATOLOGY has 591 patients — consistent with expected volumes -- [x] Document estimated processing time (expect 6-12 minutes for 440K records) - - Actual: ~6.2 minutes (371.7s) for 656K → 519K → 293 nodes - - Breakdown: Snowflake fetch 7s, Transformations ~6min, Pathway processing ~30s - -## Phase 3: Reflex Integration - -### 3.1 Update AppState -- [x] Replace date picker state with dropdown state: - - `selected_initiated: str = "all"` ("all", "1yr", "2yr") - - `selected_last_seen: str = "6mo"` ("6mo", "12mo") - - Added `initiated_options` and `last_seen_options` for dropdown rendering - - Added `set_initiated_filter()` and `set_last_seen_filter()` event handlers -- [x] Add `date_filter_id` computed property: `f"{selected_initiated}_{selected_last_seen}"` -- [x] Rewrite `load_pathway_data()` to query `pathway_nodes` table: - - Base filter: `WHERE date_filter_id = ?` - - Trust/directory/drug filters on denormalized columns - - Updated all filter handlers to call `load_pathway_data()` instead of `apply_filters()` -- [x] Add `recalculate_parent_totals()` for filtered hierarchies -- [x] Update KPI calculations from root node data - - KPIs now extracted from root node (level 0) in pathway_nodes - - `unique_patients`, `total_cost`, `total_drugs` updated from query results - -### 3.2 Update Icicle Figure -- [x] Update `icicle_figure` computed property to use all pathway_nodes columns -- [x] Match original 10-field customdata structure: - - values, colours, costs, costpp - - first_seen, last_seen, first_seen_parent, last_seen_parent - - average_spacing, cost_pp_pa -- [x] Restore full hover/text templates from `visualization/plotly_generator.py` -- [x] Verify chart renders correctly with treatment statistics - - Note: Structure validated via code inspection, visual verification pending Task 3.3 UI completion - -### 3.3 Update UI Components -- [x] Replace date pickers with select dropdowns: - - Initiated: "All years", "Last 2 years", "Last 1 year" - - Last Seen: "Last 6 months", "Last 12 months" - - Note: Created `initiated_filter_dropdown()` and `last_seen_filter_dropdown()` components using `rx.select.root` pattern -- [x] Add "Data refreshed: X ago" indicator from pathway_refresh_log - - Note: Already implemented in top_bar() using `last_updated_display` computed property - - Uses pathway_refresh_log.completed_at via `load_pathway_data()` -- [x] Update filter section layout - - Replaced `date_range_picker` calls with new dropdown components - - Simplified filter section layout with cleaner structure -- [x] Verify UI compiles and renders correctly - - python -m py_compile: PASS - - Import check: PASS - - python -m reflex compile: PASS (11.095 seconds) - -## Phase 4: Testing & Validation - -### 4.1 End-to-End Validation -- [x] **Pathway hierarchy matches original**: Compare specific pathway ids structure - - Verified: 6 levels (Root → Trust → Directory → Drug → Pathway steps) - - 293 nodes total for all_6mo filter -- [x] **Patient counts match**: Compare root patient count for same date range - - Root: 11,118 patients, £130.5M total cost - - ~32% of fact_interventions patients (filtered by last 6 months) -- [x] **Treatment statistics display correctly**: Verify "Average treatment duration" hover data - - average_spacing, cost_pp_pa, first_seen, last_seen populated for drug nodes - - Sample: ADALIMUMAB shows 35.6 treatments, £3,384/patient/annum -- [x] **Drug filtering works**: Filter to FARICIMAB, verify correct pathways shown - - drug_sequence column populated for LIKE pattern matching - - Sample sequences: OMALIZUMAB, ADALIMUMAB, INFLIXIMAB, ETANERCEPT -- [x] **Chart renders with all tooltip data**: Verify 10-field customdata structure - - All 10 fields present: value, colour, cost, costpp, first_seen, last_seen, - first_seen_parent, last_seen_parent, average_spacing, cost_pp_pa - -### 4.2 Performance Testing -- [x] Measure filter change response time (target: <500ms) - - Actual: ~51ms (10% of budget) — queries 2-4ms + chart 47ms -- [x] Measure initial page load (target: <2s including data load) - - Actual: ~51ms (2.5% of budget) -- [x] Verify chart interaction (zoom, hover) is smooth with no lag - - 293 nodes well within Plotly's 10K+ capability -- [x] Test with full dataset - - 440K fact_interventions → 293 pathway_nodes (pre-computed) - - Database queries: all <5ms (100x under target) - - Chart generation: ~48ms average - -### 4.3 Documentation -- [x] Update CLAUDE.md with new architecture - - Added Pathway Data Architecture section with date filter table - - Updated package structure with cli/ and pathway_pipeline.py - - Added CLI module documentation - - Added pathway pipeline documentation - - Updated data flow diagrams (pre-computed vs legacy) - - Added pathway tables to database schema -- [x] Document CLI usage for `refresh_pathways` - - Added CLI commands section with examples - - Documented refresh workflow (fetch → transform → process → insert) -- [x] Update README with new run instructions - - Note: No separate README exists — CLAUDE.md serves as primary documentation - - Added database migration command to run instructions - - Added CLI refresh command to run instructions -- [x] Document any breaking changes from original app - - Added "Breaking Changes from Original App" section - - Documented date filter changes (pickers → dropdowns) - - Documented data refresh model changes - - Documented state variable changes - - Documented icicle chart enhancements +### 5.6 Visual Polish +- [ ] Add subtle hover states to interactive elements +- [ ] Ensure consistent focus rings for accessibility +- [ ] Test responsive behavior at common breakpoints (1366, 1920, 2560px widths) +- [ ] Remove any unused styles from styles.py +- [ ] Verify: No visual regressions, app looks cohesive ## Completion Criteria All tasks marked `[x]` AND: -- [x] App compiles without errors (`reflex run` succeeds) - - Verified: `python -m reflex compile` succeeds in 2.8s -- [x] All 6 date filter combinations work correctly - - Verified: Code handles all 6 filters (all_6mo, all_12mo, 1yr_6mo, 1yr_12mo, 2yr_6mo, 2yr_12mo) - - Note: Only `all_6mo` has data currently (other filters have no matching records in Snowflake) - - This is a data freshness issue, not a code issue — pipeline correctly processes all filters -- [x] Drug/directory/trust filters work with instant updates - - Verified: Query time <5ms for all filter combinations -- [x] KPIs display correct numbers matching filter state - - Verified: unique_patients=11,118, total_cost=£130.5M from root node -- [x] Icicle chart renders with full pathway data and statistics - - Verified: 10-field customdata structure, all fields populated -- [x] Treatment duration and dosing information displays in tooltips - - Verified: average_spacing contains full dosing info string -- [x] No console errors during normal operation - - Verified: python -m py_compile passes, imports successful, reflex compile succeeds - - Note: Interactive browser testing requires manual verification -- [x] Verified with real patient data from Snowflake - - Verified: 656K records fetched, 293 pathway nodes generated +- [ ] App compiles without errors (`reflex compile` succeeds) +- [ ] Filter section height ≤ 60px (measured visually) +- [ ] KPI row height ≤ 48px (measured visually) +- [ ] Top bar height = 48px +- [ ] Chart stretches to full viewport width (minus 32px total padding) +- [ ] Chart fills remaining vertical space (min 500px) +- [ ] Design feels like modern SaaS, not NHS dashboard +- [ ] All interactive elements have appropriate hover/focus states ## Reference -### Date Filter Combinations +### Current Layout (to be improved) +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Top Bar (64px) - Logo, Tabs, Freshness │ +├─────────────────────────────────────────────────────────────────┤ +│ Filter Section (~200px) - Headers, Date dropdowns, Multi-select│ +├─────────────────────────────────────────────────────────────────┤ +│ KPI Cards (~100px) - 4 large cards in row │ +├─────────────────────────────────────────────────────────────────┤ +│ Chart (~600px fixed) - Constrained width │ +└─────────────────────────────────────────────────────────────────┘ +``` -| ID | Initiated | Last Seen | Default | -|----|-----------|-----------|---------| -| `all_6mo` | All years | Last 6 months | Yes | -| `all_12mo` | All years | Last 12 months | No | -| `1yr_6mo` | Last 1 year | Last 6 months | No | -| `1yr_12mo` | Last 1 year | Last 12 months | No | -| `2yr_6mo` | Last 2 years | Last 6 months | No | -| `2yr_12mo` | Last 2 years | Last 12 months | No | +### Target Layout +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Logo │ Tabs │ │ Freshness │ 48px +├─────────────────────────────────────────────────────────────────┤ +│ [Date▾] [Date▾] [Drugs▾] [Indications▾] [Directories▾] │ KPIs │ 48-60px +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ CHART │ flex-grow +│ (full width) │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Key Measurements +| Element | Current | Target | +|---------|---------|--------| +| Top bar | 64px | 48px | +| Filter section | ~200px | ≤60px | +| KPI row | ~100px | ≤48px (or merged with filters) | +| Chart | 600px fixed, constrained width | flex-grow, full width | +| Total overhead | ~364px | ~156px (57% reduction) | ### Key Files | File | Purpose | |------|---------| -| `data_processing/schema.py` | Database schema definitions | -| `data_processing/pathway_pipeline.py` | New pathway processing pipeline | -| `cli/refresh_pathways.py` | CLI refresh command | -| `analysis/pathway_analyzer.py` | Existing pathway analysis logic | -| `visualization/plotly_generator.py` | Existing chart generation | -| `pathways_app/app_v2.py` | Reflex application | +| `pathways_app/pathways_app.py` | Main Reflex application | +| `pathways_app/styles.py` | Design tokens and style helpers | +| `DESIGN_SYSTEM.md` | Design specifications | diff --git a/pathways_app/styles.py b/pathways_app/styles.py index 890c6d2..0f3882a 100644 --- a/pathways_app/styles.py +++ b/pathways_app/styles.py @@ -1,67 +1,73 @@ """ -Design tokens and style helpers for HCD Analysis v2. +Design tokens and style helpers for HCD Analysis v2.1 (SaaS Redesign). All visual styling should use these tokens for consistency. Import: from pathways_app.styles import Colors, Spacing, Typography, etc. + +Updated to match DESIGN_SYSTEM.md v2.1 with: +- Tighter spacing (25% reduction) +- Smaller typography (reduced headline sizes) +- Compact component variants for filters/KPIs +- Full-width chart support """ class Colors: """Color palette from DESIGN_SYSTEM.md""" - # Primary Blues (NHS-inspired, modernized) - HERITAGE_BLUE = "#003087" # Deep headers, authoritative accents - PRIMARY = "#0066CC" # Main actions, links, focus states - VIBRANT = "#1E88E5" # Highlights, hover states, chart primary - SKY = "#4FC3F7" # Accents, progress bars, secondary elements - PALE = "#E3F2FD" # Subtle backgrounds, card tints + # Primary Blues (NHS-inspired, used sparingly) + HERITAGE_BLUE = "#003087" # Top bar background, strong accents + PRIMARY = "#0066CC" # Interactive elements, links, focus states + VIBRANT = "#1E88E5" # Hover states, active elements + SKY = "#4FC3F7" # Subtle accents, progress indicators + PALE = "#E3F2FD" # Selected states, subtle backgrounds - # Neutrals (warm-tinted for clinical warmth) - SLATE_900 = "#1E293B" # Primary text + # Neutrals (refined for modern feel) + SLATE_900 = "#0F172A" # Primary text (slightly darker) SLATE_700 = "#334155" # Secondary text SLATE_500 = "#64748B" # Muted text, placeholders SLATE_300 = "#CBD5E1" # Borders, dividers - SLATE_100 = "#F1F5F9" # Card backgrounds, hover states - WHITE = "#FFFFFF" # Page background + SLATE_100 = "#F8FAFC" # Backgrounds (slightly lighter) + WHITE = "#FFFFFF" # Card/modal backgrounds - # Semantic Colors - SUCCESS = "#059669" # Positive states, confirmations - WARNING = "#D97706" # Caution states, alerts - ERROR = "#DC2626" # Error states, destructive actions - INFO = "#0284C7" # Informational (matches primary family) + # Semantic Colors (modernized) + SUCCESS = "#10B981" # Positive (modern green) + WARNING = "#F59E0B" # Caution + ERROR = "#EF4444" # Errors + INFO = "#3B82F6" # Informational # Chart Palette CHART_SERIES = ["#003087", "#0066CC", "#1E88E5", "#4FC3F7", "#90CAF9"] - CHART_CATEGORICAL = ["#0066CC", "#059669", "#D97706", "#8B5CF6", "#EC4899"] + CHART_CATEGORICAL = ["#0066CC", "#10B981", "#F59E0B", "#8B5CF6", "#EC4899"] class Typography: - """Typography tokens from DESIGN_SYSTEM.md""" + """Typography tokens from DESIGN_SYSTEM.md v2.1 - REDUCED sizes""" # Font families FONT_FAMILY = "Inter, system-ui, -apple-system, sans-serif" FONT_MONO = "JetBrains Mono, monospace" - # Display: Page titles - DISPLAY_SIZE = "32px" - DISPLAY_WEIGHT = "700" + # Display: Page titles (REDUCED from 32px) + DISPLAY_SIZE = "28px" + DISPLAY_WEIGHT = "600" DISPLAY_TRACKING = "-0.02em" DISPLAY_LINE_HEIGHT = "1.2" - # Heading 1: Section headers - H1_SIZE = "24px" + # Heading 1: Section headers (REDUCED from 24px) + H1_SIZE = "18px" H1_WEIGHT = "600" H1_TRACKING = "-0.01em" H1_LINE_HEIGHT = "1.3" - # Heading 2: Card titles - H2_SIZE = "20px" + # Heading 2: Card titles (REDUCED from 20px) + H2_SIZE = "16px" H2_WEIGHT = "600" H2_TRACKING = "normal" H2_LINE_HEIGHT = "1.4" # Heading 3: Subsections - H3_SIZE = "16px" + H3_SIZE = "14px" H3_WEIGHT = "600" H3_TRACKING = "normal" H3_LINE_HEIGHT = "1.4" @@ -76,55 +82,66 @@ class Typography: BODY_SMALL_WEIGHT = "400" BODY_SMALL_LINE_HEIGHT = "1.5" - # Caption: Labels, metadata - CAPTION_SIZE = "12px" + # Caption: Labels, metadata (REDUCED from 12px) + CAPTION_SIZE = "11px" CAPTION_WEIGHT = "500" CAPTION_LINE_HEIGHT = "1.4" # Mono: Data values, codes MONO_SIZE = "13px" - MONO_WEIGHT = "400" + MONO_WEIGHT = "500" MONO_LINE_HEIGHT = "1.5" class Spacing: - """Spacing scale from DESIGN_SYSTEM.md""" + """Spacing scale from DESIGN_SYSTEM.md v2.1 - TIGHTER values (~25% reduction)""" - XS = "4px" # Tight internal padding - SM = "8px" # Between related elements - MD = "12px" # Standard gaps - LG = "16px" # Section padding - XL = "24px" # Card padding - XXL = "32px" # Major section gaps - XXXL = "48px" # Page margins + XS = "4px" # Tight gaps + SM = "6px" # Between related elements (was 8px) + MD = "8px" # Standard gaps (was 12px) + LG = "12px" # Section padding (was 16px) + XL = "16px" # Card padding (was 24px) + XXL = "24px" # Major gaps (was 32px) + XXXL = "32px" # Page margins (was 48px) class Radii: """Border radius values from DESIGN_SYSTEM.md""" - SM = "4px" # Small elements, inputs - MD = "8px" # Buttons, small cards - LG = "12px" # Cards, modals + SM = "4px" # Small elements + MD = "6px" # Inputs, buttons + LG = "8px" # Cards XL = "16px" # Large containers - FULL = "9999px" # Pills, avatars + FULL = "9999px" # Pills, badges class Shadows: - """Shadow values from DESIGN_SYSTEM.md""" + """Shadow values from DESIGN_SYSTEM.md v2.1 - LIGHTER values""" - SM = "0 1px 2px rgba(0,0,0,0.05)" # Subtle elevation - MD = "0 1px 3px rgba(0,0,0,0.08)" # Cards at rest - LG = "0 4px 6px rgba(0,0,0,0.1)" # Cards on hover, dropdowns + SM = "0 1px 2px rgba(0,0,0,0.04)" # Subtle (lighter) + MD = "0 1px 3px rgba(0,0,0,0.06)" # Cards at rest + LG = "0 4px 8px rgba(0,0,0,0.08)" # Dropdowns, hover XL = "0 10px 15px rgba(0,0,0,0.1)" # Modals, popovers class Transitions: - """Transition values from DESIGN_SYSTEM.md""" + """Transition values from DESIGN_SYSTEM.md v2.1 - FASTER (150ms)""" + DEFAULT = "150ms ease-out" COLOR = "150ms ease-out" - TRANSFORM = "200ms ease-out" - SHADOW = "200ms ease-out" - OPACITY = "200ms ease-in-out" + TRANSFORM = "150ms ease-out" + SHADOW = "150ms ease-out" + OPACITY = "150ms ease-in-out" + + +# ============================================================================== +# Layout constants - UPDATED for SaaS redesign +# ============================================================================== + +TOP_BAR_HEIGHT = "48px" # Reduced from 64px +FILTER_STRIP_HEIGHT = "48px" # Single row filter strip +PAGE_MAX_WIDTH = "1600px" # Keep for content areas (not chart) +PAGE_PADDING = Spacing.XXXL # 32px # ============================================================================== @@ -137,8 +154,8 @@ def card_style(hoverable: bool = False) -> dict: - Background: White - Border: 1px Slate 300 - - Border radius: lg (12px) - - Padding: xl (24px) + - Border radius: lg (8px) + - Padding: xl (16px - reduced) - Shadow: md at rest, lg on hover """ base_style = { @@ -164,18 +181,12 @@ def card_style(hoverable: bool = False) -> dict: def button_primary_style() -> dict: """ Primary button styling following DESIGN_SYSTEM.md specifications. - - - Background: Primary Blue - - Text: White - - Border radius: md (8px) - - Padding: 10px 20px - - Hover: Vibrant Blue background, slight scale (1.02) """ return { "background_color": Colors.PRIMARY, "color": Colors.WHITE, "border_radius": Radii.MD, - "padding": "10px 20px", + "padding": "8px 16px", "font_weight": "500", "font_size": Typography.BODY_SIZE, "cursor": "pointer", @@ -191,18 +202,13 @@ def button_primary_style() -> dict: def button_secondary_style() -> dict: """ Secondary button styling following DESIGN_SYSTEM.md specifications. - - - Background: White - - Border: 1px Primary Blue - - Text: Primary Blue - - Hover: Pale Blue background """ return { "background_color": Colors.WHITE, "color": Colors.PRIMARY, "border": f"1px solid {Colors.PRIMARY}", "border_radius": Radii.MD, - "padding": "10px 20px", + "padding": "8px 16px", "font_weight": "500", "font_size": Typography.BODY_SIZE, "cursor": "pointer", @@ -216,17 +222,13 @@ def button_secondary_style() -> dict: def button_ghost_style() -> dict: """ Ghost button styling following DESIGN_SYSTEM.md specifications. - - - Background: transparent - - Text: Primary Blue - - Hover: Pale Blue background """ return { "background_color": "transparent", "color": Colors.PRIMARY, "border": "none", "border_radius": Radii.MD, - "padding": "10px 20px", + "padding": "8px 16px", "font_weight": "500", "font_size": Typography.BODY_SIZE, "cursor": "pointer", @@ -240,19 +242,13 @@ def button_ghost_style() -> dict: def input_style() -> dict: """ Form input styling following DESIGN_SYSTEM.md specifications. - - - Height: 40px - - Border: 1px Slate 300 - - Border radius: md (8px) - - Focus: 2px Primary Blue ring - - Placeholder: Slate 500 """ return { - "height": "40px", + "height": "32px", "border": f"1px solid {Colors.SLATE_300}", "border_radius": Radii.MD, "padding": f"0 {Spacing.MD}", - "font_size": Typography.BODY_SIZE, + "font_size": Typography.BODY_SMALL_SIZE, "font_family": Typography.FONT_FAMILY, "color": Colors.SLATE_900, "background_color": Colors.WHITE, @@ -268,13 +264,13 @@ def input_style() -> dict: } +# ============================================================================== +# KPI Card styles - COMPACT variants for v2.1 +# ============================================================================== + def kpi_card_style() -> dict: """ - KPI card styling following DESIGN_SYSTEM.md specifications. - - - Large mono number: 32-48px, Slate 900 - - Label: Caption size, Slate 500 - - Background: White or Pale Blue tint + Standard KPI card styling (legacy, larger). """ return { "background_color": Colors.WHITE, @@ -287,7 +283,7 @@ def kpi_card_style() -> dict: def kpi_value_style() -> dict: - """Style for the large number in a KPI card.""" + """Style for the large number in a KPI card (legacy).""" return { "font_family": Typography.FONT_MONO, "font_size": "32px", @@ -298,7 +294,7 @@ def kpi_value_style() -> dict: def kpi_label_style() -> dict: - """Style for the label in a KPI card.""" + """Style for the label in a KPI card (legacy).""" return { "font_size": Typography.CAPTION_SIZE, "font_weight": Typography.CAPTION_WEIGHT, @@ -307,6 +303,228 @@ def kpi_label_style() -> dict: } +def compact_kpi_card_style() -> dict: + """ + COMPACT KPI card styling for v2.1 redesign. + + - Smaller padding (12px) + - Smaller value font (24px) + - Reduced visual weight + """ + return { + "background_color": Colors.WHITE, + "border": f"1px solid {Colors.SLATE_300}", + "border_radius": Radii.LG, + "padding": Spacing.LG, # 12px instead of 16px + "box_shadow": Shadows.SM, + "text_align": "center", + "min_width": "100px", + } + + +def compact_kpi_value_style() -> dict: + """Style for the value in a COMPACT KPI card.""" + return { + "font_family": Typography.FONT_MONO, + "font_size": "24px", # Reduced from 32px + "font_weight": "600", + "color": Colors.SLATE_900, + "line_height": "1.2", + } + + +def compact_kpi_label_style() -> dict: + """Style for the label in a COMPACT KPI card.""" + return { + "font_size": Typography.CAPTION_SIZE, # 11px + "font_weight": Typography.CAPTION_WEIGHT, + "color": Colors.SLATE_500, + "margin_top": Spacing.XS, # 4px tighter + } + + +def kpi_badge_style() -> dict: + """ + KPI as inline pill/badge (Option A from design system). + Zero extra height - embeds in filter row. + + Example: "12,345 patients" + """ + return { + "display": "inline-flex", + "align_items": "center", + "gap": Spacing.XS, + "padding": f"{Spacing.XS} {Spacing.LG}", # 4px 12px + "background_color": Colors.SLATE_100, + "border_radius": Radii.FULL, # Pill shape + } + + +def kpi_badge_value_style() -> dict: + """Style for value text in KPI badge.""" + return { + "font_family": Typography.FONT_MONO, + "font_size": "14px", + "font_weight": "600", + "color": Colors.SLATE_900, + } + + +def kpi_badge_label_style() -> dict: + """Style for label text in KPI badge.""" + return { + "font_size": Typography.CAPTION_SIZE, + "font_weight": "400", + "color": Colors.SLATE_500, + } + + +# ============================================================================== +# Filter strip styles - NEW for v2.1 redesign +# ============================================================================== + +def filter_strip_style() -> dict: + """ + Horizontal single-row filter container style. + + - Height: 48px + - All filters inline + - Slate 100 background (or transparent) + """ + return { + "display": "flex", + "align_items": "center", + "height": FILTER_STRIP_HEIGHT, + "gap": Spacing.LG, # 12px between filter groups + "padding": f"0 {Spacing.XL}", # 16px horizontal padding + "background_color": Colors.SLATE_100, + "border_bottom": f"1px solid {Colors.SLATE_300}", + "width": "100%", + } + + +def compact_dropdown_trigger_style() -> dict: + """ + Compact dropdown trigger for filter strip. + + - Height: 32px + - Padding: 8px 12px + - Smaller font: 13px + """ + return { + "height": "32px", + "padding": f"{Spacing.MD} {Spacing.LG}", # 8px 12px + "border": f"1px solid {Colors.SLATE_300}", + "border_radius": Radii.MD, + "font_size": Typography.BODY_SMALL_SIZE, # 13px + "font_family": Typography.FONT_FAMILY, + "color": Colors.SLATE_900, + "background_color": Colors.WHITE, + "cursor": "pointer", + "display": "flex", + "align_items": "center", + "gap": Spacing.SM, + "transition": f"border-color {Transitions.COLOR}", + "_hover": { + "border_color": Colors.PRIMARY, + } + } + + +def searchable_dropdown_panel_style() -> dict: + """ + Dropdown panel for searchable multi-select. + + - Max height: 200px for items + - Compact item spacing + """ + return { + "background_color": Colors.WHITE, + "border": f"1px solid {Colors.SLATE_300}", + "border_radius": Radii.LG, + "box_shadow": Shadows.LG, + "min_width": "240px", + "max_width": "320px", + "z_index": "50", + "overflow": "hidden", + } + + +def searchable_dropdown_item_style(selected: bool = False) -> dict: + """ + Individual item in searchable dropdown. + + - Tighter padding: 6px 8px + - Visual selected state + """ + base = { + "padding": f"{Spacing.SM} {Spacing.MD}", # 6px 8px + "font_size": Typography.BODY_SMALL_SIZE, + "cursor": "pointer", + "display": "flex", + "align_items": "center", + "gap": Spacing.SM, + "transition": f"background-color {Transitions.COLOR}", + } + + if selected: + base.update({ + "background_color": Colors.PALE, + "color": Colors.PRIMARY, + }) + else: + base.update({ + "background_color": Colors.WHITE, + "color": Colors.SLATE_900, + "_hover": { + "background_color": Colors.SLATE_100, + } + }) + + return base + + +# ============================================================================== +# Chart container styles - NEW for v2.1 redesign +# ============================================================================== + +def chart_container_style() -> dict: + """ + Full-width, flex-grow chart wrapper. + + - Width: full viewport minus padding (16px each side) + - Height: fills remaining space (min 500px) + - No max-width constraint + """ + return { + "width": "100%", + "padding": f"0 {Spacing.XL}", # 16px horizontal padding + "flex": "1", + "min_height": "500px", + "display": "flex", + "flex_direction": "column", + } + + +def chart_wrapper_style(overhead_height: str = "96px") -> dict: + """ + Inner chart wrapper with calculated height. + + Args: + overhead_height: Total height of fixed elements above chart + (top bar + filter strip = 48px + 48px = 96px default) + """ + return { + "width": "100%", + "height": f"calc(100vh - {overhead_height})", + "min_height": "500px", + } + + +# ============================================================================== +# Typography helper functions +# ============================================================================== + def text_display() -> dict: """Display text style for page titles.""" return { @@ -400,9 +618,64 @@ def text_mono() -> dict: # ============================================================================== -# Layout constants +# Top bar styles - NEW for v2.1 redesign # ============================================================================== -TOP_BAR_HEIGHT = "64px" -PAGE_MAX_WIDTH = "1600px" -PAGE_PADDING = Spacing.XXXL +def top_bar_style() -> dict: + """ + Top bar container style. + + - Height: 48px (reduced from 64px) + - Heritage Blue background + """ + return { + "height": TOP_BAR_HEIGHT, + "background_color": Colors.HERITAGE_BLUE, + "display": "flex", + "align_items": "center", + "justify_content": "space_between", + "padding": f"0 {Spacing.XL}", + "width": "100%", + } + + +def top_bar_tab_style(active: bool = False) -> dict: + """ + Tab/pill style for top bar navigation. + + - Height: 28px + - Smaller pills + """ + base = { + "height": "28px", + "padding": f"{Spacing.XS} {Spacing.LG}", # 4px 12px + "border_radius": Radii.MD, + "font_size": Typography.BODY_SMALL_SIZE, + "font_weight": "500", + "cursor": "pointer", + "transition": f"background-color {Transitions.COLOR}", + } + + if active: + base.update({ + "background_color": Colors.WHITE, + "color": Colors.HERITAGE_BLUE, + }) + else: + base.update({ + "background_color": "transparent", + "color": Colors.WHITE, + "_hover": { + "background_color": "rgba(255,255,255,0.1)", + } + }) + + return base + + +def logo_style() -> dict: + """Logo style for top bar - 28px height (reduced from 36px).""" + return { + "height": "28px", + "width": "auto", + }