UI reuse and consistency
This document captures repeatable layout patterns in the Next.js App Router frontend, when to extract shared components, and what we intentionally leave local. It complements .claude/skills/ahd-design-system/SKILL.md (tokens and visual rules).
Investigation summary (2026-03)#
The codebase uses a small set of dashboard-style surfaces: hero image + horizontal stats row, bordered cards with uppercase micro-labels, and admin widgets with grid + dividers. Duplication is highest in the hero stats strip wrapper (same flex + divide + border classes across many routes). Secondary duplication appears in stat cell markup (label + value columns), but those vary enough in padding and content that a single abstraction would fight the routes.
Findings (with references)#
Repeated structures#
| Pattern | Where it appears | Notes |
|---|---|---|
| Hero + stats strip | , , , , , , , src/app/central-bank/[code]/CentralBankClient.tsx, , , , src/app/national/page.tsx, , UK executive clients, etc. |
Same outer flex/divide/border classes; inner cells differ. |
| Political meters (influence / favorability) | (StatMeter, HeatMeter from ProfileMeters.tsx), |
Actions hero uses slimmer inline bars aligned with the design-system progress pattern; profile uses segmented BarMeter. |
| Admin task widget row | grid + divide-x, different layout goal than hero strip; do not force into HeroStatsStrip. |
Inconsistencies worth knowing#
- Stats strip chrome: Some routes use
divide-card-border, othersdivide-card-border/50withbg-card/50 backdrop-blur-sm(Campaign Operations).HeroStatsStripencodes this asvariant="default" | "overlay". - Hero image height: Design standard is
h-[175px] w-full sm:h-[220px](see design-system skill). Most heroes match; always check when adding a new page header.
When to generalize vs keep local#
Extract a shared primitive when:
- The same class string (or trivial variation) appears in three or more places and wrapping
childrenis enough, no prop explosion. - The wrapper is layout-only (no domain branching). Example:
HeroStatsStrip.
Keep markup local when:
- Cells differ in min-width, links, tooltips, loading states, or country-specific copy, a “universal stat cell” tends to accumulate optional props and become harder to read than three copies.
- The surface is admin-only or one-off (e.g. a grid dashboard) and a new abstraction would not reduce line count meaningfully.
Prefer composition over configuration: A thin wrapper + JSX children beats a component with twelve optional props.
Shared primitives#
HeroStatsStrip#
- File:
- Export:
@/components/ui(HeroStatsStrip,HeroStatsStripProps,HeroStatsStripVariant) - Use for: The horizontal row directly under a hero image with vertical dividers between cells.
- Variants:
default(solid borders);overlay(muted strip, Campaign Operations hero).
Adopted in:
- (
variant="overlay")
Deferred: Migrating every remaining hero page to HeroStatsStrip is a mechanical follow-up; touch files only when those areas change for other reasons, or in a dedicated cleanup PR.
Leader / person cells#
- Pattern:
LeaderStatItem-style cells (see design-system skill) live inCountryOverviewClient.tsxas reference. Reuse that pattern when a strip shows a linked character or “Vacant”, not a raw number.
Profile meters#
StatMeter/HeatMeter/BarMeter: , best for standalone panels with tooltips. Hero strips may use simplerh-2bars to save vertical space (as on Actions).
Admin and dashboard surfaces#
- Tasks / metrics widgets often use CSS grid +
divide-xordivide-y(TasksWidget, state metric drilldowns). Treat these separately from hero strips; grid column counts are page-specific. - Do not route admin-only layout through player-facing
HeroStatsStripunless the visual goal truly matches (horizontal scroll row under a header).
Validation checklist for UI changes#
- Token classes only, no raw Tailwind palette colors (see design-system skill).
- Hero image dimensions unchanged unless the task explicitly allows it.
- New shared component: used in at least two call sites or justified as the single supported API for new work.
- Run
npm run verifybefore merge.
Remaining risks / deferred issues#
- Broad migration of all hero pages to
HeroStatsStrip, low risk but high diff noise; deferred. - Unifying
ActionsHerometers withStatMeter, would add imports and segmented bar styling inconsistent with the compact hero; deferred unless product asks for parity. - PartiesHeader and similar strips use the same idea but sometimes omit
overflow-x-auto; align when editing those files.