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''
+ )
+ 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,
+ ],
+ )