Skip to content

A custom record card

A list can show its records as cards beside its table: a title, a subtitle, a badge and a few small fields. Rijnland’s sales team wants order cards that lead with the total and the customer, the way they talk about orders. That is a card component: the list page still loads the orders, filters, sorts and links each card to its order; the component only draws what is inside a card. This guide adds one to the ERP example (examples/erp).

A card component gets row, one record of the list:

components/order-card.tsx
import type { RecordCardProps } from '@protobase/ui'
/** An order as a card: its number and total, the customer, and its status. */
export const OrderCard = ({ row }: RecordCardProps) => (
<span className="flex flex-col gap-1">
<span className="flex items-baseline justify-between gap-2">
<span className="font-mono text-xs text-muted-foreground">{String(row.record.number)}</span>
<span className="font-semibold tabular-nums">€{Number(row.record.total).toFixed(2)}</span>
</span>
<span className="truncate font-medium">{row.labels.company_id ?? String(row.record.company_id)}</span>
<span className="mt-1 self-start rounded-md bg-primary-soft px-1.5 py-0.5 text-xs text-primary-text">{String(row.record.status)}</span>
</span>
)

row.record is the order as the list API gives it, and row.labels the name of the record each relation points at: company_id is the company’s id, labels.company_id its name. row.key is the order’s key.

The whole card is a link to the order, so the component draws no links or buttons of its own; the app draws the card’s border, padding and hover, in the app’s theme.

protobase.ui.tsx
export { OrderCard } from './components/order-card'

Declare the component in the view and give it to a cards display. The fields the display names are the fields the cards get, so name every field the card reads:

config/orders/ui.ts
import { component } from '@protobase/layout'
export const OrderCard = component('OrderCard')
// ...
.list((r, d) => ({
columns: [r.number, r.company_id, r.status, r.total, r.created_at],
sort: [[r.created_at, 'desc']],
displays: [d.cards({ title: r.number, subtitle: r.company_id, badge: r.status, meta: [r.total] }, { card: OrderCard })],
}))

Run bun run protobase dev, open the orders and switch to Cards: each order is drawn by OrderCard. The display keeps everything else the list has: its search, filters, sort menu, the count of orders and loading more as you scroll.

protobase build fails when the config declares OrderCard and protobase.ui.tsx does not export it. A card from a plugin is declared with the plugin’s name, component('OrderCard', { plugin: 'sales-tools' }).

The fields in d.cards do double duty: they are also the built-in card, title over subtitle with badge and the meta fields. Each card falls back to it:

  • when the component throws while drawing, that card alone;
  • when no component is registered under the name, for instance because its plugin is off, every card.

So a broken card still shows each order’s number, customer and status, and still opens it.