{% extends "brickwork/shell/app.html" %} {% comment %} An analysis dashboard: one question answered at the top, the shape of the answer over time beneath it, then the breakdowns that explain it. COPY THIS FILE into your project and edit it. It is not on the template loader path, so you cannot extend it (ADR-056). What your view must supply: headline_tiles the weighted scorecard grid (see the shape note below) revenue_mount a {% templatetag openblock %} bw_chart_mount {% templatetag closeblock %} result, marked safe channel_rows ranked rows: label / amount / value dicts region_rows ranked rows: label / amount / value dicts nav_items / nav_active as in app/list.html Every heading, caption and judgement below is typed into the template, which is where you change them. WHY THIS PAGE IS SHAPED THIS WAY, since it is the part worth copying: A dashboard is not a list of numbers. It is an argument, and an argument has an order. This page reads top to bottom as one: the headline says what happened, the chart shows the shape of it, the breakdowns say where it came from, and the gauge says whether we are on track. A reader who stops after the first band has still learned the answer. That ordering, not the choice of components, is what makes a dashboard readable. So the first band is NOT four equal tiles. Equal tiles say every number matters equally, which is never true, and a reader scanning them has to work out the ranking themselves. The scorecard's span= carries that ranking instead: the metric this dashboard exists to report gets span=2 and is read first, and the supporting metrics take one column each. Change which tile is wide and you have changed what the dashboard is about, which is the right amount of effort for that decision. THE SCORECARD'S SHAPE, and why it needs your view: _scorecard.html takes items as a list of dicts, each {content, span}, where content is already-rendered, already-safe markup. A Django template cannot build a list of dicts and cannot capture an {% templatetag openblock %} include {% templatetag closeblock %} into a variable, so this one grid genuinely has to come from Python. Build it in your view: from django.template.loader import render_to_string from django.utils.safestring import mark_safe headline_tiles = [ {"content": mark_safe(render_to_string( "brickwork/components/_stat_comparison.html", {"label": "Net revenue", "current": "GBP 1.24m", "previous": "GBP 1.08m", "period_label": "same quarter last year", "trend": "up", "trend_label": "15% higher", "size": "lg"})), "span": 2}, ... ] The grid is 1 column on a phone, 2 at sm and 4 at lg, so a span=2 tile is full width on a phone, full width at sm, and half the row at lg. Spans read as designed at lg and clamp gracefully below it; there is nothing to write for the narrow case. ON TREND, and this is the trap worth reading twice: trend states the DIRECTION THE NUMBER MOVED, never whether that is good news. _trend_indicator.html renders trend="down" as a down arrow plus the hidden word "decreased", and you cannot override that word. A cost that fell moved DOWN, so it takes trend="down" even though falling is what you wanted; the judgement goes in trend_label, in words ("18% lower, which is the target"). Get this backwards and a screen reader announces the opposite of what a sighted reader sees. No gate in this package catches it: not axe, not the render tests, not a visual check. Only reading it does. States: the scorecard has no states of its own, each tile carries its own (see _stat.html and _stat_comparison.html's own headers); the chart card's loading/error/empty/populated states, which are mutually exclusive and resolve in that order (see _chart_card.html's own header); each ranked list's own loading/empty states; the gauge's threshold bands. Accessibility: inherits shell/app.html's skip link, sidebar/drawer nav and page-header region. Every number is visible text: the chart is a mount whose accessible name is supplied at the call site, the sparkline and the ranked bars are aria-hidden decoration reinforcing figures stated in words beside them, and the gauge always renders its own numeric reading. No reading on this page rides on colour or on a shape alone. Covered by the archetype harness's full gate sweep (render, axe WCAG 2.2 AA, no horizontal overflow, light/dark distinctness, skip-link first-tab-stop with JS disabled) at every W0.1 breakpoint, both themes. Responsive: no breakpoint switch of its own; inherits shell/app.html's sidebar-to-drawer collapse at --bw-breakpoint-md (48rem). The scorecard steps 1 to 2 to 4 columns at its own sm and lg breakpoints and clamps wide spans automatically; the ranked lists and chart card reflow inside whatever column they land in. {% endcomment %} {% load brickwork_components brickwork_nav %} {% block page_title %}Revenue analysis - Northwind{% endblock %} {% block sidebar %}{% bw_nav nav_items nav_active %}{% endblock %} {% block sidebar_mobile %}{% bw_nav nav_items nav_active %}{% endblock %} {% block brand_wordmark %}Northwind{% endblock %} {% block page_header %} {% include "brickwork/components/_page_header.html" with title="Revenue analysis" description="Q3 2026 against the same quarter last year. Figures settle 48 hours after period close." %} {% endblock %} {% block page_actions %} {% bw_button "Export" variant="secondary" icon="download" href="/analysis/export/" %} {% endblock %} {% block content %}