feat: update design tokens for SaaS redesign (Task 5.1)

- 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.
This commit is contained in:
Andrew Charlwood
2026-02-05 01:46:58 +00:00
parent 27d2d603c3
commit 0a68c2a5a5
2 changed files with 493 additions and 296 deletions
+132 -208
View File
@@ -1,22 +1,23 @@
# Implementation Plan - Pathway Data Architecture # Implementation Plan - UI Redesign Phase
## Project Overview ## 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**: **Design Philosophy**:
- Performance: Pathway calculation done once during data refresh, not on every filter - Thematically aligned with blue color scheme but NOT constrained by NHS branding
- Simplicity: Reflex filters pre-computed data with simple SQL WHERE clauses - Think Stripe, Linear, Vercel - clean, spacious, confident
- Full Pathways: Sequential treatment pathways (drug_0 → drug_1 → drug_2...) with statistics - 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 Files**:
- `pathways_app/pathways_app.py` - Main Reflex application
**Source Code**: - `pathways_app/styles.py` - Design tokens and style helpers
- Existing analysis: `analysis/pathway_analyzer.py` - `DESIGN_SYSTEM.md` - Design specifications
- Existing visualization: `visualization/plotly_generator.py`
- Existing Reflex app: `pathways_app/app_v2.py`
## Quality Checks ## Quality Checks
@@ -24,222 +25,145 @@ Run after each task:
```bash ```bash
# Syntax check for Python files # Syntax check for Python files
python -m py_compile <file.py> python -m py_compile pathways_app/pathways_app.py
# Import verification # Import verification
python -c "from <module> import <class>" python -c "from pathways_app.pathways_app import app"
# For Reflex changes # Reflex compile
cd pathways_app && timeout 60 python -m reflex run 2>&1 | head -30 python -m reflex compile
``` ```
## Phase 1: Schema & Data Pipeline Foundation ## Phase 5: UI Redesign
### 1.1 Extend Database Schema ### 5.1 Update Design System for Modern SaaS
- [x] Add `pathway_date_filters` table with 6 pre-defined combinations: - [x] Update `DESIGN_SYSTEM.md` with new specifications:
- `all_6mo`, `all_12mo`, `1yr_6mo`, `1yr_12mo`, `2yr_6mo`, `2yr_12mo` - Reduce top bar height from 64px to 48px
- [x] Add `pathway_nodes` table with: - Define compact filter row (single horizontal strip)
- Hierarchy structure (parents, ids, labels, level) - Define compact KPI card dimensions (reduce padding, font sizes)
- Patient counts and costs (value, cost, costpp, cost_pp_pa) - Add full-width chart container specs
- Date ranges (first_seen, last_seen, first_seen_parent, last_seen_parent) - [x] Update `pathways_app/styles.py` tokens to match:
- Treatment statistics (average_spacing, average_administered, avg_days) - Typography: DISPLAY 32→28px, H1 24→18px, H2 20→16px, CAPTION 12→11px
- Denormalized filter columns (trust_name, directory, drug_sequence) - Spacing: SM 8→6px, MD 12→8px, LG 16→12px, XL 24→16px, XXL 32→24px, XXXL 48→32px
- Foreign key to date_filter_id - Shadows: Lighter values (0.04, 0.06, 0.08 opacity)
- [x] Add `pathway_refresh_log` table for tracking refresh status - Colors: Modernized semantic colors (SUCCESS #10B981, etc.)
- [x] Create indexes for efficient filtering - Layout: TOP_BAR_HEIGHT 64→48px, FILTER_STRIP_HEIGHT 48px
- [x] Verify schema with: `python -c "from data_processing.schema import *"` - [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 ### 5.2 Compact Filter Section (50-67% height reduction)
- [x] Create `data_processing/pathway_pipeline.py` with: - [ ] Redesign filter_section() as a single horizontal strip:
- `fetch_and_transform_data()` - Snowflake fetch + UPID/drug/directory transformations - All filters in ONE row: Date dropdowns | Drugs | Indications | Directorates
- `process_pathway_for_date_filter(df, date_filter_config)` - Single filter processing - Remove "Filters" header (saves vertical space)
- `extract_denormalized_fields(ice_df)` - Extract trust, directory, drug_sequence from ids - Use smaller dropdown triggers (height: 32px instead of 40px)
- `convert_to_records(ice_df, date_filter_id)` - Convert ice_df to list of dicts for SQLite - Use icon-only labels where possible
- [x] Integrate with existing `analysis/pathway_analyzer.py` functions - [ ] Reduce searchable_dropdown() panel heights:
- [x] Verify: `python -c "from data_processing.pathway_pipeline import *"` - 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 ### 5.3 Compact KPI Cards (50% reduction)
- [x] Create script to set up new tables in existing `data/pathways.db` - [ ] Reduce KPI card dimensions:
- Note: Existing `python -m data_processing.migrate` handles this (updated in Task 1.1) - Padding: 12px (was 24px)
- [x] Pre-populate `pathway_date_filters` with 6 combinations - Value font size: 24px (was 32px)
- Note: Auto-populated via INSERT OR REPLACE in PATHWAY_DATE_FILTERS_SCHEMA - Label font size: 11px (was 12px)
- [x] Verify migration runs cleanly on fresh database - [ ] Make KPIs a single compact row:
- Verified: All 3 pathway tables created, 6 date filters populated correctly - 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 ### 5.5 Top Bar Refinement
- [x] Create `cli/refresh_pathways.py` with: - [ ] Reduce top bar height to 48px (was 64px)
- Uses DATE_FILTER_CONFIGS and compute_date_ranges from pathway_pipeline.py - [ ] Simplify chart tabs - smaller pills or just text links
- `refresh_pathways(minimum_patients, provider_codes, ...)` main function - [ ] Consider moving data freshness indicator inline with filters
- `insert_pathway_records()` for SQLite insertion - [ ] Make logo smaller (28px instead of 36px)
- `log_refresh_start/complete/failed()` for refresh tracking - [ ] Verify: Top bar is minimal but functional
- [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`
### 2.2 Test Refresh Pipeline ### 5.6 Visual Polish
- [x] Run refresh with Snowflake data - [ ] Add subtle hover states to interactive elements
- Successfully fetched 656,695 records from Snowflake in ~7s - [ ] Ensure consistent focus rings for accessibility
- Transformed to 519,848 records after UPID/drug/directory processing - [ ] Test responsive behavior at common breakpoints (1366, 1920, 2560px widths)
- [x] Verify all 6 date_filter_ids populated in pathway_nodes - [ ] Remove any unused styles from styles.py
- Note: Only `all_6mo` has data (293 nodes) due to test data freshness - [ ] Verify: No visual regressions, app looks cohesive
- 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
## Completion Criteria ## Completion Criteria
All tasks marked `[x]` AND: All tasks marked `[x]` AND:
- [x] App compiles without errors (`reflex run` succeeds) - [ ] App compiles without errors (`reflex compile` succeeds)
- Verified: `python -m reflex compile` succeeds in 2.8s - [ ] Filter section height ≤ 60px (measured visually)
- [x] All 6 date filter combinations work correctly - [ ] KPI row height ≤ 48px (measured visually)
- Verified: Code handles all 6 filters (all_6mo, all_12mo, 1yr_6mo, 1yr_12mo, 2yr_6mo, 2yr_12mo) - [ ] Top bar height = 48px
- Note: Only `all_6mo` has data currently (other filters have no matching records in Snowflake) - [ ] Chart stretches to full viewport width (minus 32px total padding)
- This is a data freshness issue, not a code issue — pipeline correctly processes all filters - [ ] Chart fills remaining vertical space (min 500px)
- [x] Drug/directory/trust filters work with instant updates - [ ] Design feels like modern SaaS, not NHS dashboard
- Verified: Query time <5ms for all filter combinations - [ ] All interactive elements have appropriate hover/focus states
- [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
## Reference ## 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 | ### Target Layout
|----|-----------|-----------|---------| ```
| `all_6mo` | All years | Last 6 months | Yes | ┌─────────────────────────────────────────────────────────────────┐
| `all_12mo` | All years | Last 12 months | No | │ Logo │ Tabs │ │ Freshness │ 48px
| `1yr_6mo` | Last 1 year | Last 6 months | No | ├─────────────────────────────────────────────────────────────────┤
| `1yr_12mo` | Last 1 year | Last 12 months | No | │ [Date▾] [Date▾] [Drugs▾] [Indications▾] [Directories▾] │ KPIs │ 48-60px
| `2yr_6mo` | Last 2 years | Last 6 months | No | ├─────────────────────────────────────────────────────────────────┤
| `2yr_12mo` | Last 2 years | Last 12 months | No | │ │
│ 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 ### Key Files
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `data_processing/schema.py` | Database schema definitions | | `pathways_app/pathways_app.py` | Main Reflex application |
| `data_processing/pathway_pipeline.py` | New pathway processing pipeline | | `pathways_app/styles.py` | Design tokens and style helpers |
| `cli/refresh_pathways.py` | CLI refresh command | | `DESIGN_SYSTEM.md` | Design specifications |
| `analysis/pathway_analyzer.py` | Existing pathway analysis logic |
| `visualization/plotly_generator.py` | Existing chart generation |
| `pathways_app/app_v2.py` | Reflex application |
+361 -88
View File
@@ -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. All visual styling should use these tokens for consistency.
Import: from pathways_app.styles import Colors, Spacing, Typography, etc. 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: class Colors:
"""Color palette from DESIGN_SYSTEM.md""" """Color palette from DESIGN_SYSTEM.md"""
# Primary Blues (NHS-inspired, modernized) # Primary Blues (NHS-inspired, used sparingly)
HERITAGE_BLUE = "#003087" # Deep headers, authoritative accents HERITAGE_BLUE = "#003087" # Top bar background, strong accents
PRIMARY = "#0066CC" # Main actions, links, focus states PRIMARY = "#0066CC" # Interactive elements, links, focus states
VIBRANT = "#1E88E5" # Highlights, hover states, chart primary VIBRANT = "#1E88E5" # Hover states, active elements
SKY = "#4FC3F7" # Accents, progress bars, secondary elements SKY = "#4FC3F7" # Subtle accents, progress indicators
PALE = "#E3F2FD" # Subtle backgrounds, card tints PALE = "#E3F2FD" # Selected states, subtle backgrounds
# Neutrals (warm-tinted for clinical warmth) # Neutrals (refined for modern feel)
SLATE_900 = "#1E293B" # Primary text SLATE_900 = "#0F172A" # Primary text (slightly darker)
SLATE_700 = "#334155" # Secondary text SLATE_700 = "#334155" # Secondary text
SLATE_500 = "#64748B" # Muted text, placeholders SLATE_500 = "#64748B" # Muted text, placeholders
SLATE_300 = "#CBD5E1" # Borders, dividers SLATE_300 = "#CBD5E1" # Borders, dividers
SLATE_100 = "#F1F5F9" # Card backgrounds, hover states SLATE_100 = "#F8FAFC" # Backgrounds (slightly lighter)
WHITE = "#FFFFFF" # Page background WHITE = "#FFFFFF" # Card/modal backgrounds
# Semantic Colors # Semantic Colors (modernized)
SUCCESS = "#059669" # Positive states, confirmations SUCCESS = "#10B981" # Positive (modern green)
WARNING = "#D97706" # Caution states, alerts WARNING = "#F59E0B" # Caution
ERROR = "#DC2626" # Error states, destructive actions ERROR = "#EF4444" # Errors
INFO = "#0284C7" # Informational (matches primary family) INFO = "#3B82F6" # Informational
# Chart Palette # Chart Palette
CHART_SERIES = ["#003087", "#0066CC", "#1E88E5", "#4FC3F7", "#90CAF9"] CHART_SERIES = ["#003087", "#0066CC", "#1E88E5", "#4FC3F7", "#90CAF9"]
CHART_CATEGORICAL = ["#0066CC", "#059669", "#D97706", "#8B5CF6", "#EC4899"] CHART_CATEGORICAL = ["#0066CC", "#10B981", "#F59E0B", "#8B5CF6", "#EC4899"]
class Typography: class Typography:
"""Typography tokens from DESIGN_SYSTEM.md""" """Typography tokens from DESIGN_SYSTEM.md v2.1 - REDUCED sizes"""
# Font families # Font families
FONT_FAMILY = "Inter, system-ui, -apple-system, sans-serif" FONT_FAMILY = "Inter, system-ui, -apple-system, sans-serif"
FONT_MONO = "JetBrains Mono, monospace" FONT_MONO = "JetBrains Mono, monospace"
# Display: Page titles # Display: Page titles (REDUCED from 32px)
DISPLAY_SIZE = "32px" DISPLAY_SIZE = "28px"
DISPLAY_WEIGHT = "700" DISPLAY_WEIGHT = "600"
DISPLAY_TRACKING = "-0.02em" DISPLAY_TRACKING = "-0.02em"
DISPLAY_LINE_HEIGHT = "1.2" DISPLAY_LINE_HEIGHT = "1.2"
# Heading 1: Section headers # Heading 1: Section headers (REDUCED from 24px)
H1_SIZE = "24px" H1_SIZE = "18px"
H1_WEIGHT = "600" H1_WEIGHT = "600"
H1_TRACKING = "-0.01em" H1_TRACKING = "-0.01em"
H1_LINE_HEIGHT = "1.3" H1_LINE_HEIGHT = "1.3"
# Heading 2: Card titles # Heading 2: Card titles (REDUCED from 20px)
H2_SIZE = "20px" H2_SIZE = "16px"
H2_WEIGHT = "600" H2_WEIGHT = "600"
H2_TRACKING = "normal" H2_TRACKING = "normal"
H2_LINE_HEIGHT = "1.4" H2_LINE_HEIGHT = "1.4"
# Heading 3: Subsections # Heading 3: Subsections
H3_SIZE = "16px" H3_SIZE = "14px"
H3_WEIGHT = "600" H3_WEIGHT = "600"
H3_TRACKING = "normal" H3_TRACKING = "normal"
H3_LINE_HEIGHT = "1.4" H3_LINE_HEIGHT = "1.4"
@@ -76,55 +82,66 @@ class Typography:
BODY_SMALL_WEIGHT = "400" BODY_SMALL_WEIGHT = "400"
BODY_SMALL_LINE_HEIGHT = "1.5" BODY_SMALL_LINE_HEIGHT = "1.5"
# Caption: Labels, metadata # Caption: Labels, metadata (REDUCED from 12px)
CAPTION_SIZE = "12px" CAPTION_SIZE = "11px"
CAPTION_WEIGHT = "500" CAPTION_WEIGHT = "500"
CAPTION_LINE_HEIGHT = "1.4" CAPTION_LINE_HEIGHT = "1.4"
# Mono: Data values, codes # Mono: Data values, codes
MONO_SIZE = "13px" MONO_SIZE = "13px"
MONO_WEIGHT = "400" MONO_WEIGHT = "500"
MONO_LINE_HEIGHT = "1.5" MONO_LINE_HEIGHT = "1.5"
class Spacing: 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 XS = "4px" # Tight gaps
SM = "8px" # Between related elements SM = "6px" # Between related elements (was 8px)
MD = "12px" # Standard gaps MD = "8px" # Standard gaps (was 12px)
LG = "16px" # Section padding LG = "12px" # Section padding (was 16px)
XL = "24px" # Card padding XL = "16px" # Card padding (was 24px)
XXL = "32px" # Major section gaps XXL = "24px" # Major gaps (was 32px)
XXXL = "48px" # Page margins XXXL = "32px" # Page margins (was 48px)
class Radii: class Radii:
"""Border radius values from DESIGN_SYSTEM.md""" """Border radius values from DESIGN_SYSTEM.md"""
SM = "4px" # Small elements, inputs SM = "4px" # Small elements
MD = "8px" # Buttons, small cards MD = "6px" # Inputs, buttons
LG = "12px" # Cards, modals LG = "8px" # Cards
XL = "16px" # Large containers XL = "16px" # Large containers
FULL = "9999px" # Pills, avatars FULL = "9999px" # Pills, badges
class Shadows: 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 SM = "0 1px 2px rgba(0,0,0,0.04)" # Subtle (lighter)
MD = "0 1px 3px rgba(0,0,0,0.08)" # Cards at rest MD = "0 1px 3px rgba(0,0,0,0.06)" # Cards at rest
LG = "0 4px 6px rgba(0,0,0,0.1)" # Cards on hover, dropdowns LG = "0 4px 8px rgba(0,0,0,0.08)" # Dropdowns, hover
XL = "0 10px 15px rgba(0,0,0,0.1)" # Modals, popovers XL = "0 10px 15px rgba(0,0,0,0.1)" # Modals, popovers
class Transitions: 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" COLOR = "150ms ease-out"
TRANSFORM = "200ms ease-out" TRANSFORM = "150ms ease-out"
SHADOW = "200ms ease-out" SHADOW = "150ms ease-out"
OPACITY = "200ms ease-in-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 - Background: White
- Border: 1px Slate 300 - Border: 1px Slate 300
- Border radius: lg (12px) - Border radius: lg (8px)
- Padding: xl (24px) - Padding: xl (16px - reduced)
- Shadow: md at rest, lg on hover - Shadow: md at rest, lg on hover
""" """
base_style = { base_style = {
@@ -164,18 +181,12 @@ def card_style(hoverable: bool = False) -> dict:
def button_primary_style() -> dict: def button_primary_style() -> dict:
""" """
Primary button styling following DESIGN_SYSTEM.md specifications. 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 { return {
"background_color": Colors.PRIMARY, "background_color": Colors.PRIMARY,
"color": Colors.WHITE, "color": Colors.WHITE,
"border_radius": Radii.MD, "border_radius": Radii.MD,
"padding": "10px 20px", "padding": "8px 16px",
"font_weight": "500", "font_weight": "500",
"font_size": Typography.BODY_SIZE, "font_size": Typography.BODY_SIZE,
"cursor": "pointer", "cursor": "pointer",
@@ -191,18 +202,13 @@ def button_primary_style() -> dict:
def button_secondary_style() -> dict: def button_secondary_style() -> dict:
""" """
Secondary button styling following DESIGN_SYSTEM.md specifications. Secondary button styling following DESIGN_SYSTEM.md specifications.
- Background: White
- Border: 1px Primary Blue
- Text: Primary Blue
- Hover: Pale Blue background
""" """
return { return {
"background_color": Colors.WHITE, "background_color": Colors.WHITE,
"color": Colors.PRIMARY, "color": Colors.PRIMARY,
"border": f"1px solid {Colors.PRIMARY}", "border": f"1px solid {Colors.PRIMARY}",
"border_radius": Radii.MD, "border_radius": Radii.MD,
"padding": "10px 20px", "padding": "8px 16px",
"font_weight": "500", "font_weight": "500",
"font_size": Typography.BODY_SIZE, "font_size": Typography.BODY_SIZE,
"cursor": "pointer", "cursor": "pointer",
@@ -216,17 +222,13 @@ def button_secondary_style() -> dict:
def button_ghost_style() -> dict: def button_ghost_style() -> dict:
""" """
Ghost button styling following DESIGN_SYSTEM.md specifications. Ghost button styling following DESIGN_SYSTEM.md specifications.
- Background: transparent
- Text: Primary Blue
- Hover: Pale Blue background
""" """
return { return {
"background_color": "transparent", "background_color": "transparent",
"color": Colors.PRIMARY, "color": Colors.PRIMARY,
"border": "none", "border": "none",
"border_radius": Radii.MD, "border_radius": Radii.MD,
"padding": "10px 20px", "padding": "8px 16px",
"font_weight": "500", "font_weight": "500",
"font_size": Typography.BODY_SIZE, "font_size": Typography.BODY_SIZE,
"cursor": "pointer", "cursor": "pointer",
@@ -240,19 +242,13 @@ def button_ghost_style() -> dict:
def input_style() -> dict: def input_style() -> dict:
""" """
Form input styling following DESIGN_SYSTEM.md specifications. 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 { return {
"height": "40px", "height": "32px",
"border": f"1px solid {Colors.SLATE_300}", "border": f"1px solid {Colors.SLATE_300}",
"border_radius": Radii.MD, "border_radius": Radii.MD,
"padding": f"0 {Spacing.MD}", "padding": f"0 {Spacing.MD}",
"font_size": Typography.BODY_SIZE, "font_size": Typography.BODY_SMALL_SIZE,
"font_family": Typography.FONT_FAMILY, "font_family": Typography.FONT_FAMILY,
"color": Colors.SLATE_900, "color": Colors.SLATE_900,
"background_color": Colors.WHITE, "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: def kpi_card_style() -> dict:
""" """
KPI card styling following DESIGN_SYSTEM.md specifications. Standard KPI card styling (legacy, larger).
- Large mono number: 32-48px, Slate 900
- Label: Caption size, Slate 500
- Background: White or Pale Blue tint
""" """
return { return {
"background_color": Colors.WHITE, "background_color": Colors.WHITE,
@@ -287,7 +283,7 @@ def kpi_card_style() -> dict:
def kpi_value_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 { return {
"font_family": Typography.FONT_MONO, "font_family": Typography.FONT_MONO,
"font_size": "32px", "font_size": "32px",
@@ -298,7 +294,7 @@ def kpi_value_style() -> dict:
def kpi_label_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 { return {
"font_size": Typography.CAPTION_SIZE, "font_size": Typography.CAPTION_SIZE,
"font_weight": Typography.CAPTION_WEIGHT, "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: def text_display() -> dict:
"""Display text style for page titles.""" """Display text style for page titles."""
return { return {
@@ -400,9 +618,64 @@ def text_mono() -> dict:
# ============================================================================== # ==============================================================================
# Layout constants # Top bar styles - NEW for v2.1 redesign
# ============================================================================== # ==============================================================================
TOP_BAR_HEIGHT = "64px" def top_bar_style() -> dict:
PAGE_MAX_WIDTH = "1600px" """
PAGE_PADDING = Spacing.XXXL 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",
}