Detail Page Standard

The deep-dive Gera commissioned 2026-08-25: inventory every detail surface, root-cause the double skeleton, propose the one boilerplate everything fills into. Captures: detail-pages-inventory.

"We want to have a standard boilerplate detailed structure. And then everything kind of fills up on that... we never want to see double skeleton... I really like the work order structure — but the work order one is kind of noisy. I like the checkpoints where it kind of shows the company colors."

1 · The good news: the standard half-exists

DetailPage (src/components/feature/detail/DetailPage.tsx, ADR-0095, docs/architecture/detail-page.md) is already the codified host: 8 of 12 PM-facing detail surfaces render through it, a permanent drift test pins adoption and forbids hand-rolled back arrows, and its core design is exactly right — loading / error / loaded render from the same props, so a route skeleton structurally cannot drift from the loaded page, and the back contract makes a dead back-button a compile error.

So the effort is not "invent a standard." It is: fix the skeleton class-bug, close ten gaps in the host, adopt the last four surfaces, and unify three vocabularies (banners, progress rails, back labels) that the host doesn't govern yet.

2 · The inventory — 12 surfaces, one table

SurfaceHostWidthBackBannerProgress idiomSkeletons seenLOC
ProspectDetailPagewideBack to Prospectsbrand-run activity rail ★2 (list's + own)1,739
RenewalDetailPage (no rail)wideBack to Renewalsgradient hero + amber banner + 2 stat cardsStepper (semantic) + phase grid2 (board's + own)2,623
Work orderDetailPagewideBack to Work Ordersorange strip, hardcoded hexStepper (semantic)1929
TurnoverDetailPagewideBack to Unit Turnoversgradient hero (TurnoverHero)Stepper (semantic)2 (list's + own)934
TenantDetailPagewideBack to Tenantspill-node lifecycle rail1502
UnitDetailPagewideBack to propertysemantic status strip ✓severity-dot signals1207
VendorDetailPagewideBack to Vendors2 (list's + own)350
Agent sessionDetailPagewideBack to Agents1
Propertyhand-rolledwideBack to Propertiesaccent upload notice2 (list's + own)1,854
Conversationhand-rollednarrowbare "Back" (history)checkpoint dividers1396
Collections demandhand-rollednarrow"Collections" (no prefix)2 (collections list's!)1,303
Mass sendhand-rolledwideBack to Building communicationsqueue-drain bar1772

★ = the owner's anchor idiom (logo colors on progress). The captures artifact shows every row the same day.

3 · The double skeleton — root-caused, and it's a class, not a bug

Why it happens: Next.js wraps a segment's page and every child segment in the Suspense boundary its loading.tsx defines. The prospects list and the prospect detail share the prospects path segment — so the list's table-skeleton is an ancestor fallback of the detail route. Next then guarantees both windows on every list→detail click, twice over: the child slot is keyed by segment (a fresh boundary shows the parent fallback immediately), and prefetch deliberately stops at the first loading.tsx walking down — the list's — so the detail shell can never be cached at click time. Result: list skeleton → detail skeleton → content. The ADR-0095 work already made detail-skeleton → content seamless; the list-level boundary sits outside its reach.

Six routes have the class: prospects, renewals, properties, vendors, turnovers — and collections/demand, where the first skeleton is the collections list's, a different page entirely.

The fix (Wave A, URL-preserving): move each list page + its loading.tsx into a route group — prospects/(list)/ — leaving [id]/ outside it. URLs don't change; the list keeps its own entry skeleton; the first boundary on the detail path becomes the detail's own ghost, which prefetch can now cache — so a click paints the correct shell instantly, then content. This is exactly the shape maintenance/ and tenants/ already have (their lists live at /list), which is why they never had the bug. Plus a drift test pinning the invariant: no segment may own both a loading.tsx and a child [param]/loading.tsx — so the class can't come back.

4 · The boilerplate: DetailPage v2, grown in place

Replacing the host would re-fight a consolidation that already worked. Every gap is additive and slot-shaped; every current consumer keeps compiling:

  1. Banner slot with a policy: semantic status strips (the unit page's left-border alert — the good species) get a first-class banner position between back and header; today renewal/unit smuggle banners into header and property hand-rolls one outside the host.
  2. Structured header option{title, subtitle, actions} — so the ghost can mirror the real header instead of a generic title+pill shape, and the action bar standardizes.
  3. Width passthroughvariant="narrow" unlocks conversation + collections demand (the app's forms-get-narrow rule).
  4. Hub affordances — optional tabs row + full-width sections + rail options (sticky, ratio) bring property detail inside the fence its own docs currently exile it from.
  5. Error contract — kinds (not-found / denied / unavailable) + optional action + an error-branch test id.
  6. Loading-twin explicitness — a columns prop instead of the subtle bare-sidebar attr; kill the default ghost's hardcoded "Activity Log" heading.
  7. Footer/action-bar slot — brings the form-shaped surfaces (mass send, demand builder) inside.

Adoption roadmap = the gap list: each gap unblocks a named hand-rolled surface. The drift test's ADOPTED list grows as each lands.

5 · Three unifications the host can't do alone (owner calls)

a. Banners: status yes, heroes no

Two gradient heroes exist (renewal, turnover — violet/cyan radial glows, animated pill, AuroraText). Proposal: retire the hero species; the renewal header calms to the standard header + ONE status element, its stats become ordinary cards. The unit page's semantic strip becomes the sanctioned banner. Decision: keep any hero at all?

b. One progress vocabulary

Four today: brand-run activity rail (prospect — your stated favorite), semantic Stepper (WO/renewal/turnover), pill-node lifecycle rail (tenant collections), checkpoint dividers (conversations). Proposal: journey rails ride the brand run (Stepper gains the spectrum treatment the activity rail has — "checkpoints in company colors"), while pass/fail state stays semantic (a failed step is red, not sky). Decision: convert Stepper's done-state from emerald to the run?

c. Back labels

"Back to X" everywhere: conversations' bare history-"Back" gets a contextual label, demand's "Collections" gets its prefix. The BackLink contract already encodes this; two call sites drift from it.

6 · Waves

WaveWhatRisk
ADouble-skeleton sweep: 6 route-group moves + the boundary drift test. No pixel changes, URLs unchanged.low — mechanical
BDetailPage v2 slots (banner, header, variant, error, twins, footer). No consumer breaks.low — additive
CAdopt property / conversation / demand / mass-send; back-label fixes.medium — per-page diffs
DCalm passes: renewal hero, WO details-card sectioning + tokenized strip, progress-rail unification.medium — needs your taste sign-off per surface

Wave A is ready to build the moment you nod — it's the "boom, you see the right one" fix, and it needs none of the taste decisions.

PropFlow Docs