diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 6ab26c1..8e56cb4 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -119,16 +119,16 @@ Drawer selection → update_drug_selection → app-state store → load_pathway_ ## Phase 2: Static Layout ### 2.1 Header + sidebar components -- [ ] Create `dash_app/components/header.py` — `make_header()` function returning Dash HTML component +- [x] Create `dash_app/components/header.py` — `make_header()` function returning Dash HTML component - NHS logo, title "HCD Analysis", breadcrumb, data freshness indicator (status dot + record count + last updated) - Use CSS classes from `nhs.css`: `.top-header`, `.top-header__brand`, `.top-header__logo`, `.top-header__title`, etc. - Record count and last updated are `html.Span` with IDs for callback updates: `id="header-record-count"`, `id="header-last-updated"` -- [ ] Create `dash_app/components/sidebar.py` — `make_sidebar()` function +- [x] Create `dash_app/components/sidebar.py` — `make_sidebar()` function - Navigation items matching 01_nhs_classic.html sidebar (Pathway Overview active, Drug Selection, Trust Selection, Directory Selection, Indications, Cost Analysis, Export Data) - - SVG icons as raw HTML (copy from 01_nhs_classic.html) - - "Drug Selection" and "Indications" items trigger the dmc.Drawer (via callback, wired in Phase 4) + - SVG icons via data URI img elements (Dash doesn't support inline SVGs natively) + - "Drug Selection" (`id="sidebar-drug-selection"`) and "Indications" (`id="sidebar-indications"`) items have IDs for drawer callbacks (Phase 4) - Footer: "NHS Norfolk & Waveney ICB / High Cost Drugs Programme" -- **Checkpoint**: Components render in browser with correct NHS styling +- **Checkpoint**: Components render in browser with correct NHS styling ✓ ### 2.2 Main content area: KPI row + filter bar + chart card - [ ] Create `dash_app/components/kpi_row.py` — `make_kpi_row()` function diff --git a/dash_app/app.py b/dash_app/app.py index c4ada9b..8fbbfef 100644 --- a/dash_app/app.py +++ b/dash_app/app.py @@ -2,6 +2,9 @@ from dash import Dash, html, dcc import dash_mantine_components as dmc +from dash_app.components.header import make_header +from dash_app.components.sidebar import make_sidebar + app = Dash( __name__, suppress_callback_exceptions=True, @@ -21,13 +24,17 @@ app.layout = dmc.MantineProvider( dcc.Store(id="chart-data", storage_type="memory"), dcc.Store(id="reference-data", storage_type="session"), - # Placeholder layout — will be replaced by assembled components - html.Div( + # Page structure + make_header(), + make_sidebar(), + html.Main( className="main", - style={"marginLeft": "0", "marginTop": "0"}, children=[ html.H1("HCD Analysis", style={"color": "#003087"}), - html.P("Dash application scaffolding complete. Components will be added in subsequent phases."), + html.P( + "Layout scaffolding with header and sidebar. " + "KPIs, filter bar, and chart card will be added in Task 2.2." + ), ], ), ], diff --git a/dash_app/assets/nhs.css b/dash_app/assets/nhs.css index 06f7d44..5bb1a29 100644 --- a/dash_app/assets/nhs.css +++ b/dash_app/assets/nhs.css @@ -101,6 +101,7 @@ body { font-weight: 600; } .sidebar__item svg { width: 18px; height: 18px; flex-shrink: 0; } +.sidebar__icon { width: 18px; height: 18px; flex-shrink: 0; } .sidebar__footer { margin-top: auto; padding: 16px 20px; border-top: 1px solid var(--nhs-pale-grey); diff --git a/dash_app/components/header.py b/dash_app/components/header.py new file mode 100644 index 0000000..494faa6 --- /dev/null +++ b/dash_app/components/header.py @@ -0,0 +1,47 @@ +"""Top header bar component matching 01_nhs_classic.html design.""" +from dash import html + + +def make_header(): + """Return the fixed top header with NHS branding and data freshness indicators.""" + return html.Header( + className="top-header", + children=[ + # Brand area (NHS logo + title) with angled clip-path + html.Div( + className="top-header__brand", + children=[ + html.Div("NHS", className="top-header__logo"), + html.Div( + html.Div("HCD Analysis", className="top-header__title"), + ), + ], + ), + # Breadcrumb + html.Div( + className="top-header__breadcrumb", + children=[ + "Dashboard \u203A ", + html.Strong("Pathway Analysis"), + ], + ), + # Right side: status dot + record count + last updated + html.Div( + className="top-header__right", + children=[ + html.Span( + children=[ + html.Span(className="status-dot"), + html.Span("...", id="header-record-count"), + ], + ), + html.Span( + children=[ + "Last updated: ", + html.Span("...", id="header-last-updated"), + ], + ), + ], + ), + ], + ) diff --git a/dash_app/components/sidebar.py b/dash_app/components/sidebar.py new file mode 100644 index 0000000..1d4eed3 --- /dev/null +++ b/dash_app/components/sidebar.py @@ -0,0 +1,96 @@ +"""Left sidebar navigation component matching 01_nhs_classic.html design.""" +from urllib.parse import quote as url_quote + +from dash import html + + +def _svg_icon(svg_body): + """Wrap an SVG body string into an html.Img using a data URI. + + This avoids needing dash-svg or dangerouslySetInnerHTML. + The SVG icons are copied from 01_nhs_classic.html. + """ + svg = ( + f'{svg_body}' + ) + return html.Img( + src=f"data:image/svg+xml,{url_quote(svg)}", + className="sidebar__icon", + ) + + +# SVG icon bodies from 01_nhs_classic.html +_ICONS = { + "pathway": '', + "drug": '', + "trust": '', + "directory": '', + "indication": '', + "cost": '', + "export": '', +} + + +def make_sidebar(): + """Return the fixed left sidebar navigation.""" + return html.Nav( + className="sidebar", + **{"aria-label": "Main navigation"}, + children=[ + # Analysis section + html.Div( + className="sidebar__section", + children=[ + html.Div("Analysis", className="sidebar__label"), + _sidebar_item("Pathway Overview", "pathway", active=True), + _sidebar_item( + "Drug Selection", "drug", item_id="sidebar-drug-selection" + ), + _sidebar_item("Trust Selection", "trust"), + _sidebar_item("Directory Selection", "directory"), + _sidebar_item( + "Indications", "indication", item_id="sidebar-indications" + ), + ], + ), + # Reports section + html.Div( + className="sidebar__section", + children=[ + html.Div("Reports", className="sidebar__label"), + _sidebar_item("Cost Analysis", "cost"), + _sidebar_item("Export Data", "export"), + ], + ), + # Footer + html.Div( + className="sidebar__footer", + children=[ + "NHS Norfolk & Waveney ICB", + html.Br(), + "High Cost Drugs Programme", + ], + ), + ], + ) + + +def _sidebar_item(label, icon_key, active=False, item_id=None): + """Create a single sidebar navigation item.""" + class_name = "sidebar__item" + if active: + class_name += " sidebar__item--active" + + props = {"className": class_name} + if item_id: + props["id"] = item_id + props["n_clicks"] = 0 + + return html.A( + **props, + children=[ + _svg_icon(_ICONS[icon_key]), + label, + ], + )