What this solves
Gestalt hierarchy: panes or rows that share one card read as one comparison, not as unrelated numbers the reader has to mentally join. A hairline or a gap does the grouping work that a full card border would otherwise force onto separate cards.
Use it when
- Two related breakdowns of the same metric, side by sidewalmart-mvp Admin.tsx: the two-pane usage grid, each pane its own section label and table
- A repeating list where every other row needs a visual seamwalmart-mvp Home.tsx: the fine-grain catalog snapshot, odd cells take sm:border-l
- Three or more facets of one dataset, with a card-level actionthe header-action variant below, an Export CSV action riding the card toolbar
Rendered
Fixture data; check both themes.Two panes, coarse split plus fine-grain hairline
External usage
By organization
- Acme Distribution128
- Karlin Auto61
- Northside Parts17
Days with usage
- Aug 1142
- Aug 1238
- Aug 1351
Catalog snapshot
Titles written9,204
Errors auto-resolved312
Errors manually resolved88
Rows ingested40,119
Three panes, with a card-level toolbar action
External usage
Last 7 days, three waysBy organization
- Acme Distribution128
- Karlin Auto61
- Northside Parts17
By day
- Aug 1142
- Aug 1238
- Aug 1351
By module
- Part Type84
- Scrape55
- Normalize40
- Content27
patterns/split-cards/example.tsxCopy this into a fresh route and it renders as above.
import { Card, RenderIcon } from "@versable-git/ui";// Two grains of one idea: a card split into two independently-headed panes,// and a card whose repeating rows take a hairline divider between columns.const orgs = [ { name: "Acme Distribution", runs: 128 }, { name: "Karlin Auto", runs: 61 }, { name: "Northside Parts", runs: 17 },];const days = [ { day: "Aug 11", runs: 42 }, { day: "Aug 12", runs: 38 }, { day: "Aug 13", runs: 51 },];const snapshot = [ { Icon: "Success", label: "Titles written", value: "9,204" }, { Icon: "Refresh", label: "Errors auto-resolved", value: "312" }, { Icon: "Edit", label: "Errors manually resolved", value: "88" }, { Icon: "Database", label: "Rows ingested", value: "40,119" },] as const;export function SplitCards() { return ( <div className="flex flex-col gap-6"> <Card title="External usage" noAnimate> <div className="grid gap-8 md:grid-cols-[minmax(0,1.2fr)_minmax(200px,0.8fr)]"> <div> <div className="text-base-content/45 mb-2 text-xs font-medium tracking-wide uppercase">By organization</div> <ul className="flex flex-col gap-1"> {orgs.map((o) => ( <li key={o.name} className="flex justify-between text-sm"> <span>{o.name}</span> <span className="text-base-content/65 tabular-nums">{o.runs}</span> </li> ))} </ul> </div> <div> <div className="text-base-content/45 mb-2 text-xs font-medium tracking-wide uppercase">Days with usage</div> <ul className="flex flex-col gap-1"> {days.map((d) => ( <li key={d.day} className="flex justify-between text-sm"> <span>{d.day}</span> <span className="text-base-content/65 tabular-nums">{d.runs}</span> </li> ))} </ul> </div> </div> </Card> <Card title="Catalog snapshot" noAnimate> <div className="grid grid-cols-1 sm:grid-cols-2"> {snapshot.map((s, i) => ( <div key={s.label} className={`border-base-300/70 flex items-center gap-3 border-t px-4 py-3 first:border-t-0 sm:nth-[-n+2]:border-t-0 ${i % 2 === 1 ? "sm:border-l" : ""}`} > <span className="bg-base-200 text-base-content/50 grid size-7 shrink-0 place-items-center rounded-md"> <RenderIcon Icon={s.Icon} size={15} /> </span> <span className="text-base-content/75 flex-1 text-sm">{s.label}</span> <span className="text-sm font-medium tabular-nums">{s.value}</span> </div> ))} </div> </Card> </div> );}Where it ships
walmart-mvp/frontend/src/pages/Admin.tsx:674-730the two-pane grid: gap-based split, each pane with its own section label and tablewalmart-mvp/frontend/src/pages/Home.tsx:95-113the fine grain: repeating rows in a 2-column grid where odd cells take sm:border-l
App-specific: The coarse split is gap-based rather than a hairline, and pane widths pin the secondary pane with minmax. Real cards feed both panes from one loader payload; the fine-grain rows swap the value for a Skeleton while loading.