Blocks / Marketing

Feature grid

Six features in a bento grid, two of them with a small product picture built from parts.

Width

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.

  • Supplier orders, drafted

    Larder writes tomorrow's order from par levels and the bookings. You check it, change a line, and send.

  • Delivery 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.

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-grid

Built 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&apos;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);
    }
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index