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:
+132
-208
@@ -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
@@ -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",
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user