ADR-0095: One DetailPage host owns the detail-page skeleton
Status: Accepted (2026-07-16) · Owner: Gera
Plan: docs/planning/detail-page-host.md · Recipe: docs/architecture/detail-page.md
Context
Every entity detail page (work order, renewal, tenant, vendor, unit, prospect,
turnover) shares one skeleton: a PageBody, a BackLink above the header, the
standard 2/3 + 1/3 grid, and loading/error twins. Before Wave 2, each page
hand-rolled that skeleton (~5,200 lines across six clients), so the loading
twin could — and did — drift from the loaded view (stale labels, destination
flashes, an aria live region wrapping the back link). Wave 1
(back-button-body-standard.md) standardized the affordance; the skeleton
itself remained N implementations.
Decision
src/components/feature/detail/DetailPage.tsx is THE host every detail page
branches from ("a work-order detail page is a branch of DetailPage" — Gera).
It owns: the back affordance (full BackLink contract, label rule enforced by
the primitive), the header slot, the optional-sidebar grid, and the
loading/error branches rendered from the SAME props as the loaded view — so
loading-twin parity is structural, not a convention a guard has to chase.
Per-page ghost customization goes through ghost slots, never a fork.
DetailPageShell was renamed/reshaped into it (no shim, no parallel path).
Pages whose layout is genuinely not a detail skeleton (the property HUB page)
do not adopt — forced uniformity is drift in the other direction.
Alternatives considered
- Status quo (per-page skeletons): proven to drift; Wave-1's guard could only pin affordance presence, not structure.
- Convention + lint/drift rules alone: unenforceable for structure (grid shape, ghost parity, aria scoping) without rendering; the host makes the right thing the only thing.
Consequences
- New detail pages start as
<DetailPage back={…} header={…} sidebar={…}>. BackLinkinternal hrefs soft-nav through Next<Link>(D2), app-wide.- Drift-guarded by
src/__tests__/back-affordance-drift.test.ts(coverage + anti-fork + loading parity + Wave-2 pins: shell nonexistence, adoption set).