Blocks / Marketing
Feature grid
Six features in a bento grid, two of them with a small product picture built from parts.
What it does
The clipboard jobs, done while you cook.
Six things that used to happen on the back of a delivery note at midnight. Larder does them as the day goes, and keeps the sums.
Counts from sales
Every ticket that closes on the till takes its recipe off the shelf, gram by gram. The count is right before anyone opens the walk-in.
Taken off the shelf tonight£61264 covers since 17:30Values, oldest first: 2, 4, 3, 6, 9, 12, 10, 8, 6, 4- Shellseekers hake
- Cultured butter
Supplier orders, drafted
Larder writes tomorrow's order from par levels and the bookings. You check it, change a line, and send.
Natoora, for Thursday
Drafted at 22:10 from par
- Banana shallotsLow
- Cavolo nero
- Blood orangesDearer
- Puntarelle
4 lines£186.40Delivery check-in by scanning
Scan each case as it comes off the van. Shorts and swaps are caught at the back door, not at the invoice.
A waste log that adds up
Two taps at the bin: what, how much, why. By Monday you know which dish is paying for the compost.
Par levels that learn
Set a par once. Larder nudges it with the season and the bookings, and tells you before service when something will run short.
Transfers between sites
Send six kilos of flour to the bakery and it leaves one count and lands in the other, with the cost moving with it.
MarrowWilton WayMarrow BakeryBroadway Market
Install
Writes the block to components/blocks/feature-grid.tsx and its .css, and every part it uses to components/equip/, with the parts those are built from.
npx https://equip.studio311.app/equip.tgz add block:feature-gridBuilt from 8 parts
Notes
- Each product picture is one role=img with a sentence for a label, and the parts inside it are inert, so nothing in it takes focus.
- Tiles are placed with named grid areas: one column, then two at 40rem, then a three-column bento at 60rem.
- Reading order follows the markup, so rearrange tiles by editing grid-template-areas rather than the list.
Source
components/blocks/feature-grid.tsx200 lines
import { Badge } from "@/components/equip/badge";
import { Card, CardBody, CardHeader } from "@/components/equip/card";
import { Heading } from "@/components/equip/heading";
import { Icon } from "@/components/equip/icon";
import { List, ListItem } from "@/components/equip/list";
import { Sparkline } from "@/components/equip/sparkline";
import { Stat } from "@/components/equip/stat";
import { Text } from "@/components/equip/text";
import "./feature-grid.css";
/*
* Six things Larder does, laid out as a bento: two tiles carry a small picture of the product
* made from its own parts, the rest are a numbered icon, a name and two lines.
*/
export default function FeatureGrid() {
return (
<section className="blk-feature-grid" aria-labelledby="feature-grid-title">
<div className="blk-feature-grid-inner">
<header className="blk-feature-grid-head">
<Text size="s" weight="strong" tone="quiet">
What it does
</Text>
<Heading level={2} id="feature-grid-title" className="blk-feature-grid-title">
The clipboard jobs, done while you cook.
</Heading>
<Text size="l" tone="quiet" className="blk-feature-grid-lede">
Six things that used to happen on the back of a delivery note at midnight. Larder does them as the day
goes, and keeps the sums.
</Text>
</header>
<ul className="blk-feature-grid-cells">
<li className="blk-feature-grid-cell" data-area="sales" data-picture="">
<div className="blk-feature-grid-copy">
<div className="blk-feature-grid-mark">
<span className="blk-feature-grid-icon">
<Icon name="receipt" />
</span>
<span className="blk-feature-grid-index" aria-hidden="true">01</span>
</div>
<Heading level={3} size="s">Counts from sales</Heading>
<Text size="s" tone="quiet">
Every ticket that closes on the till takes its recipe off the shelf, gram by gram. The count is right
before anyone opens the walk-in.
</Text>
</div>
<div
className="blk-feature-grid-picture"
role="img"
aria-label="Friday dinner so far: 64 covers sold, and £612 of stock taken off the shelf by the till, with hake and butter the biggest movers"
>
<div className="blk-feature-grid-stage" inert>
<Card tone="raised" padding="s" className="blk-feature-grid-panel">
<Stat
label="Taken off the shelf tonight"
value={612}
format={{ style: "currency", currency: "GBP", maximumFractionDigits: 0 }}
hint="64 covers since 17:30"
locale="en-GB"
>
<Sparkline
className="blk-feature-grid-spark"
label="Covers per quarter hour"
values={[2, 4, 3, 6, 9, 12, 10, 8, 6, 4]}
width={88}
height={32}
area
showLast
locale="en-GB"
/>
</Stat>
<List density="compact" dividers className="blk-feature-grid-moves">
<ListItem title="Shellseekers hake" meta="2.5 kg" />
<ListItem title="Cultured butter" meta="1.1 kg" />
</List>
</Card>
</div>
</div>
</li>
<li className="blk-feature-grid-cell" data-area="order" data-picture="">
<div className="blk-feature-grid-copy">
<div className="blk-feature-grid-mark">
<span className="blk-feature-grid-icon">
<Icon name="clipboard-check" />
</span>
<span className="blk-feature-grid-index" aria-hidden="true">02</span>
</div>
<Heading level={3} size="s">Supplier orders, drafted</Heading>
<Text size="s" tone="quiet">
Larder writes tomorrow's order from par levels and the bookings. You check it, change a line,
and send.
</Text>
</div>
<div
className="blk-feature-grid-picture"
role="img"
aria-label="A draft Natoora order for Thursday: four lines, banana shallots below par, blood oranges dearer this week, £186.40 in all"
>
<div className="blk-feature-grid-stage" inert>
<Card tone="raised" padding="s" className="blk-feature-grid-panel">
<CardHeader title="Natoora, for Thursday" description="Drafted at 22:10 from par" />
<CardBody>
<List density="compact" dividers className="blk-feature-grid-order">
<ListItem title="Banana shallots" meta="5 kg" trail={<Badge tone="warn">Low</Badge>} />
<ListItem title="Cavolo nero" meta="3 kg" />
<ListItem title="Blood oranges" meta="2 cases" trail={<Badge>Dearer</Badge>} />
<ListItem title="Puntarelle" meta="4 heads" />
</List>
<div className="blk-feature-grid-total">
<Text as="span" size="s" tone="quiet">4 lines</Text>
<Text as="span" size="s" weight="strong">£186.40</Text>
</div>
</CardBody>
</Card>
</div>
</div>
</li>
<li className="blk-feature-grid-cell" data-area="scan">
<div className="blk-feature-grid-copy">
<div className="blk-feature-grid-mark">
<span className="blk-feature-grid-icon">
<Icon name="barcode" />
</span>
<span className="blk-feature-grid-index" aria-hidden="true">03</span>
</div>
<Heading level={3} size="s">Delivery check-in by scanning</Heading>
<Text size="s" tone="quiet">
Scan each case as it comes off the van. Shorts and swaps are caught at the back door, not at the
invoice.
</Text>
</div>
</li>
<li className="blk-feature-grid-cell" data-area="waste">
<div className="blk-feature-grid-copy">
<div className="blk-feature-grid-mark">
<span className="blk-feature-grid-icon">
<Icon name="trash" />
</span>
<span className="blk-feature-grid-index" aria-hidden="true">04</span>
</div>
<Heading level={3} size="s">A waste log that adds up</Heading>
<Text size="s" tone="quiet">
Two taps at the bin: what, how much, why. By Monday you know which dish is paying for the compost.
</Text>
</div>
</li>
<li className="blk-feature-grid-cell" data-area="par">
<div className="blk-feature-grid-copy">
<div className="blk-feature-grid-mark">
<span className="blk-feature-grid-icon">
<Icon name="gauge" />
</span>
<span className="blk-feature-grid-index" aria-hidden="true">05</span>
</div>
<Heading level={3} size="s">Par levels that learn</Heading>
<Text size="s" tone="quiet">
Set a par once. Larder nudges it with the season and the bookings, and tells you before service when
something will run short.
</Text>
</div>
</li>
<li className="blk-feature-grid-cell" data-area="transfer">
<div className="blk-feature-grid-copy">
<div className="blk-feature-grid-mark">
<span className="blk-feature-grid-icon">
<Icon name="truck" />
</span>
<span className="blk-feature-grid-index" aria-hidden="true">06</span>
</div>
<Heading level={3} size="s">Transfers between sites</Heading>
<Text size="s" tone="quiet">
Send six kilos of flour to the bakery and it leaves one count and lands in the other, with the cost
moving with it.
</Text>
</div>
<div
className="blk-feature-grid-route"
role="img"
aria-label="A transfer of 6 kg Wildfarmed flour from Marrow on Wilton Way to Marrow Bakery on Broadway Market"
>
<span className="blk-feature-grid-stop">
<span className="blk-feature-grid-dot" aria-hidden="true" />
<Text as="span" size="s" weight="strong">Marrow</Text>
<Text as="span" size="s" tone="faint">Wilton Way</Text>
</span>
<span className="blk-feature-grid-line" aria-hidden="true">
<span className="blk-feature-grid-load">6 kg flour</span>
</span>
<span className="blk-feature-grid-stop">
<span className="blk-feature-grid-dot" aria-hidden="true" />
<Text as="span" size="s" weight="strong">Marrow Bakery</Text>
<Text as="span" size="s" tone="faint">Broadway Market</Text>
</span>
</div>
</li>
</ul>
</div>
</section>
);
}components/blocks/feature-grid.css326 lines
@layer blocks {
.blk-feature-grid {
container: blk-feature-grid / inline-size;
background: var(--eq-bg);
color: var(--eq-ink);
}
.blk-feature-grid-inner {
display: grid;
gap: calc(var(--eq-space-6) * 1.25);
max-width: 76rem;
margin-inline: auto;
padding: calc(var(--eq-space-6) * 1.5) var(--eq-space-4);
}
.blk-feature-grid-head {
display: grid;
justify-items: start;
gap: var(--eq-space-3);
max-width: 40rem;
}
.blk-feature-grid-head > * {
margin: 0;
}
.blk-feature-grid-title {
font-size: clamp(1.75rem, 5.5cqi, 3rem);
line-height: 1.06;
letter-spacing: -0.025em;
text-wrap: balance;
}
.blk-feature-grid-lede {
max-width: 34rem;
text-wrap: pretty;
}
/* ---- The grid ---- */
.blk-feature-grid-cells {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--eq-space-3);
margin: 0;
padding: 0;
list-style: none;
}
.blk-feature-grid-cell {
display: grid;
align-content: start;
gap: var(--eq-space-5);
min-width: 0;
margin: 0;
padding: var(--eq-space-5) var(--eq-space-4);
border: 1px solid var(--eq-line);
border-radius: var(--eq-radius-l);
background: var(--eq-surface);
}
.blk-feature-grid-copy {
display: grid;
justify-items: start;
align-content: start;
gap: var(--eq-space-2);
min-width: 0;
}
.blk-feature-grid-copy > * {
margin: 0;
}
.blk-feature-grid-copy > p {
max-width: 30rem;
text-wrap: pretty;
}
.blk-feature-grid-icon {
display: grid;
place-items: center;
width: var(--eq-h-m);
height: var(--eq-h-m);
border-radius: var(--eq-radius-m);
background: var(--eq-tint-strong);
color: var(--eq-ink);
}
/* The icon and a quiet running number, like the tab on a prep list. */
.blk-feature-grid-mark {
display: flex;
align-items: start;
justify-content: space-between;
justify-self: stretch;
gap: var(--eq-space-3);
margin-bottom: var(--eq-space-2);
}
.blk-feature-grid-index {
color: var(--eq-ink-3);
font: var(--eq-weight) var(--eq-text-xs) / 1 var(--eq-font-mono);
font-variation-settings: var(--eq-font-mono-settings);
font-variant-numeric: tabular-nums;
letter-spacing: 0.04em;
}
/* ---- The two product pictures ---- */
.blk-feature-grid-picture {
display: grid;
align-items: center;
min-width: 0;
padding: var(--eq-space-4) var(--eq-space-3);
border: 1px solid var(--eq-line);
border-radius: var(--eq-radius-m);
background:
radial-gradient(circle at 1px 1px, var(--eq-line-strong) 1px, transparent 0) 0 0 / 18px 18px,
color-mix(in oklab, var(--eq-surface) 60%, var(--eq-bg));
}
.blk-feature-grid-stage {
min-width: 0;
}
.blk-feature-grid-panel {
width: 100%;
max-width: 22rem;
margin-inline: auto;
}
.blk-feature-grid-spark {
color: var(--eq-chart-1);
}
.blk-feature-grid-moves,
.blk-feature-grid-order {
margin-inline: calc(var(--eq-space-2) * -1);
}
.blk-feature-grid-moves {
padding-top: var(--eq-space-2);
border-top: 1px solid var(--eq-line);
}
.blk-feature-grid-total {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--eq-space-2);
margin-top: var(--eq-space-2);
padding-top: var(--eq-space-2);
border-top: 1px solid var(--eq-line);
color: var(--eq-ink);
font-variant-numeric: tabular-nums;
}
/* ---- The transfer route: two stops on a dashed line, stacked until the tile is wide ---- */
.blk-feature-grid-route {
--_dot: 10px;
display: grid;
gap: var(--eq-space-2);
min-width: 0;
padding: var(--eq-space-4);
border: 1px dashed var(--eq-line-strong);
border-radius: var(--eq-radius-m);
}
.blk-feature-grid-stop {
display: grid;
grid-template-columns: var(--_dot) minmax(0, 1fr);
align-items: center;
column-gap: var(--eq-space-2);
min-width: 0;
}
.blk-feature-grid-stop > :not(.blk-feature-grid-dot) {
grid-column: 2;
margin: 0;
}
.blk-feature-grid-dot {
grid-row: 1;
width: var(--_dot);
height: var(--_dot);
border-radius: var(--eq-radius-full);
border: 1.5px solid var(--eq-ink);
background: var(--eq-bg);
}
.blk-feature-grid-stop:last-child .blk-feature-grid-dot {
background: var(--eq-chart-2);
border-color: var(--eq-chart-2);
}
.blk-feature-grid-line {
display: flex;
align-items: center;
min-height: var(--eq-h-m);
margin-inline-start: calc(var(--_dot) / 2 - 0.5px);
padding-inline-start: calc(var(--eq-space-2) + var(--_dot) / 2);
border-inline-start: 1px dashed var(--eq-line-strong);
}
.blk-feature-grid-load {
display: inline-flex;
align-items: center;
height: 20px;
padding-inline: var(--eq-space-2);
border-radius: var(--eq-radius-s);
background: var(--eq-tint-strong);
color: var(--eq-ink);
font: var(--eq-weight-strong) var(--eq-text-xs) / 1 var(--eq-font-mono);
font-variation-settings: var(--eq-font-mono-settings);
white-space: nowrap;
}
/* ---- Two columns ---- */
@container blk-feature-grid (width >= 40rem) {
.blk-feature-grid-inner {
padding-inline: var(--eq-space-6);
}
.blk-feature-grid-cells {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-areas:
"sales sales"
"order order"
"scan waste"
"par transfer";
}
.blk-feature-grid-cell {
padding: var(--eq-space-5);
}
.blk-feature-grid-cell[data-area="sales"] { grid-area: sales; }
.blk-feature-grid-cell[data-area="scan"] { grid-area: scan; }
.blk-feature-grid-cell[data-area="order"] { grid-area: order; }
.blk-feature-grid-cell[data-area="waste"] { grid-area: waste; }
.blk-feature-grid-cell[data-area="par"] { grid-area: par; }
.blk-feature-grid-cell[data-area="transfer"] { grid-area: transfer; }
/* A wide tile with a picture sets its words beside the picture. */
.blk-feature-grid-cell[data-picture] {
grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
align-items: center;
gap: var(--eq-space-6);
}
.blk-feature-grid-picture {
align-self: stretch;
padding: var(--eq-space-5) var(--eq-space-4);
}
}
/* ---- Bento ---- */
@container blk-feature-grid (width >= 60rem) {
.blk-feature-grid-inner {
gap: calc(var(--eq-space-6) * 1.75);
padding-block: calc(var(--eq-space-6) * 2.5);
}
.blk-feature-grid-cells {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-areas:
"sales sales order"
"scan waste order"
"par transfer transfer";
gap: var(--eq-space-4);
}
/* The order tile is tall and narrow here: words on top, the draft order below. */
.blk-feature-grid-cell[data-area="order"] {
grid-template-columns: minmax(0, 1fr);
align-items: stretch;
align-content: stretch;
grid-template-rows: auto 1fr;
gap: var(--eq-space-5);
}
.blk-feature-grid-cell[data-area="order"] .blk-feature-grid-picture {
padding: var(--eq-space-4) var(--eq-space-2);
}
.blk-feature-grid-cell[data-area="transfer"] {
grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
align-items: center;
gap: var(--eq-space-6);
}
.blk-feature-grid-route {
grid-template-columns: auto minmax(min-content, 1fr) auto;
align-items: start;
gap: 0;
}
.blk-feature-grid-stop {
grid-template-columns: minmax(0, 1fr);
justify-items: start;
row-gap: var(--eq-space-1);
}
.blk-feature-grid-stop > :not(.blk-feature-grid-dot) {
grid-column: 1;
}
.blk-feature-grid-stop .blk-feature-grid-dot {
grid-row: auto;
margin-bottom: var(--eq-space-1);
}
.blk-feature-grid-line {
flex-direction: column;
align-items: center;
min-height: 0;
margin: calc(var(--_dot) / 2 - 0.5px) var(--eq-space-2) 0;
padding: var(--eq-space-2) 0 0;
border-inline-start: 0;
border-top: 1px dashed var(--eq-line-strong);
}
}
}