feat(dashboard): adopt <Card> + <Cluster> in AlertsPanel [Phase 1e]#624
Conversation
Migrates the dashboard AlertsPanel from a hand-rolled `s.card` rule
to the design-system <Card> primitive. The horizontal title/badge
header is rebuilt with <Cluster as="header" justify="between">.
The alert-specific affordances (row grid, severity tints, count
badges, "show all" footer button) remain in the local CSS module —
they are not generic enough to belong in the primitive layer yet.
Why not CardHeader/CardContent/CardFooter?
- CardHeader is a vertical Stack of title+description; this panel
has a horizontal eyebrow + count-badge layout, so <Cluster as=
"header"> is a closer fit.
- The content slot is a single <ul>; wrapping it in CardContent
(a Stack) would add an empty flex container with no value.
- The "show all" button is a full-width hairline-bordered control,
not a justify-end actions row, so CardFooter does not apply.
These subcomponents are kept available for panels whose anatomy
matches them (PageHeader-style sections, multi-block content cards,
right-aligned action footers).
Accessibility improvement: the panel title is now a real <h2>
inside the <section aria-label>, instead of a styled <span>.
CSS deltas:
- Removed .card (background, border, radius, padding, overflow)
- Removed .card::before (decorative gradient hairline)
- Removed .header (replaced by <Cluster justify="between">)
Net: -22 lines of duplicated container styling.
Tests (frontend/src/components/dashboard/__tests__/AlertsPanel.test.tsx):
- empty state returns null when all counts are 0
- one row per non-zero count, sorted by severity (critical → info)
- panel renders as <section aria-label> with <h2> title
- total count badge equals number of rows
- each row has a severity-specific class for the tint
- clicking a row calls navigate(route)
- 5-row boundary case does not render the "show all" footer
- Card adoption smoke-test (data-variant="subtle", radius=lg)
Verification (local):
- frontend tsc -b: clean
- frontend lint: 0 errors (warning count unchanged)
- vitest design-system + dashboard subset: 67/67 passing
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6cfc4b4f71
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <header className={s.header}> | ||
| <span className={s.title}>{dash.needsAttention}</span> | ||
| <Card as="section" radius="lg" aria-label={dash.needsAttention}> | ||
| <Cluster as="header" justify="between" align="center" gap="2"> |
There was a problem hiding this comment.
Keep alerts header on a single row
Switching the header to Cluster changed its flex behavior from non-wrapping to wrapping by default, so on narrow layouts or longer localized needsAttention strings the count badge can drop to a second line. The previous header styling kept title and badge in one horizontal row, and this panel’s anatomy depends on that for consistent scanability. Set nowrap on this Cluster (or otherwise disable wrapping) to preserve the prior behavior.
Useful? React with 👍 / 👎.
…[Phase 1f] (#625) Finishes the DashboardV2 row-card migration. Both remaining sibling panels (OperationsTimeline in row 4, UpcomingPanel in row 5) now use the design-system <Card> primitive instead of the local `s.card` wrapper. Combined with Phase 1d (WarehouseSnapshot) and Phase 1e (AlertsPanel), every dashboard surface in this row is now driven by the same primitive. Wrapper migration (DashboardV2.tsx) ----------------------------------- - <div className={s.card}><OperationsTimeline …/></div> → <Card>…</Card> - <div className={s.card}><UpcomingPanel …/></div> → <Card>…</Card> Both sites are pure wrapper swaps. <Card> defaults (variant=subtle, radius=xl=16px, padding=5=20px, bordered=true) are an exact drop-in for the legacy `.card` rule, so the visual contract (background, border, radius, padding) is preserved. The internal list/item content of each panel is untouched. CSS dead-code removal (DashboardV2.module.css) ---------------------------------------------- With both consumers gone, these rules become unreferenced and are deleted (≈39 lines): - .card (background, border, radius, padding, transition) - .card:hover (subtle background lift) - .card::before (decorative top hairline) - .cardHeader (flex justify-between row, never used in DashboardV2.tsx) - .cardLink, .cardLink:hover (eyebrow link button, never used in DashboardV2.tsx) A short tombstone comment is left in place so future readers see why the section is empty and where to look (Card primitive path). Out of scope ------------ - No route changes - No API changes - No business-logic changes - No refactor of inner panel markup - No new dependencies - No Card highlight prop introduction - No new tests (spec: only update if coverage exists; none did) Verification (local) -------------------- - frontend tsc -b: clean - frontend lint: 0 errors (warning count unchanged) - vitest design-system + dashboard subset: 67/67 passing - Full test + build run delegated to CI. Phase 1 status after this PR ---------------------------- - ✅ 1a — TS tokens (#616) - ✅ 1b — Layout + Typography primitives (#620) - ✅ 1c — PageHeader migration (#621) - ✅ 1d — Surface + Card primitives + first dashboard adoption (#622) - ✅ 1e — AlertsPanel adoption (#624) - ✅ 1f — OperationsTimeline + UpcomingPanel adoption (this PR) Co-authored-by: replit-agent <agent@replit.local>
Phase 1e — Adopt
<Card>+<Cluster>in AlertsPanelContinues the dashboard's design-system migration started in Phase 1d. The dashboard
AlertsPanelpanel now uses the<Card>primitive for its container and<Cluster as="header">for its title row.What changed
<section className={s.card}>→<Card as="section" radius="lg" aria-label={…}>. All container concerns (background, border, radius, padding, overflow) now come from the primitive.<header className={s.header}>→<Cluster as="header" justify="between" align="center">. The title is now a real<h2>, restoring proper landmark semantics for the panel..card,.card::before, and.header(–22 lines of duplicated container styling). Kept everything alert-specific: row grid, severity tints, count badges,.showAllbutton.Why not
CardHeader/CardContent/CardFooter?The new subcomponents from #622 are intentionally opinionated:
CardHeaderStackof title + descriptionCardContentStackslot with default gap<ul>— wrapper would be emptyCardFooterCluster justify="end"actions rowForcing them here would either alter the visual hierarchy or add empty wrapper divs. They remain available for panels whose anatomy matches them (multi-block content cards, right-aligned action footers).
Tests
frontend/src/components/dashboard/__tests__/AlertsPanel.test.tsx— 9 tests:null<section aria-label>with<h2>titlenavigate(route)with the right pathdata-variant="subtle",var(--radius-lg))Local verification
tsc -b: cleaneslint: 0 errors (warning count unchanged)vitest run src/design-system src/components/dashboard: 67/67 passing (9 new + 25 DS + others)Phase 1 progress
PageHeadermigration (refactor(page-header): migrate to design-system primitives [Phase 1c] #621)Surface+Cardprimitives + first dashboard adoption (feat(design-system): add Surface and Card primitives + first dashboard adoption [Phase 1d] #622)AlertsPaneladoption (this PR)MachineryStatusBoard, dashboard cards inOperations.tsx,Warehouse.tsxsummary tiles.Branch:
replit/phase-1e-migrate-alerts-paneloffmain@8ece50b.