Blocks / Application

Dashboard

An app home screen: shell with side navigation, page header, key figures, charts and a feed of today's activity.

Width
Skip to content
LarderMarrow, Hackney

This week at Marrow

Monday 9 to Friday 13 November. Counts are live from the till and this morning's deliveries.

  • 412 covers so far
  • Counted 17:40 by Ines
Food cost28.4%Down 2.8from 31.2Of food sales, target 29%
Values, oldest first: 33.1, 32.4, 31.8, 32.6, 30.9, 31.2, 29.8, 31.2, 28.4
Sales£18,240Up 7.9%from £16,910Food only
Values, oldest first: 14,200, 15,100, 14,800, 16,300, 15,900, 16,200, 17,400, 16,910, 18,240
Binned£412Down 31.1%from £5982.3% of purchases
Values, oldest first: 780, 702, 655, 690, 610, 588, 540, 598, 412
Below par7items3 out before Saturday

Food cost against target

Share of food sales spent on ingredients, by week

  1. 14 Sep
    • Food cost33.1%
    • Target29%
  2. 21 Sep
    • Food cost32.4%
    • Target29%
  3. 28 Sep
    • Food cost31.8%
    • Target29%
  4. 5 Oct
    • Food cost32.6%
    • Target29%
  5. 12 Oct
    • Food cost30.9%
    • Target29%
  6. 19 Oct
    • Food cost31.2%
    • Target29%
  7. 26 Oct
    • Food cost29.8%
    • Target29%
  8. 2 Nov
    • Food cost31.2%
    • Target29%
  9. 9 Nov
    • Food cost28.4%
    • Target29%

Where the waste went

This week, by reason

  • Over-prepped£164
  • Past date£118
  • Spoiled in delivery£72
  • Staff meals£58

Spend by supplier

Last 30 days

£21.6k
  • Flock & Herd32%, £6,840
  • Natoora24%, £5,210
  • Neal's Yard Dairy18%, £3,980
  • Shellseekers16%, £3,460
  • Wildfarmed6%, £1,270
  • Other4%, £840

Today in the kitchen

    1. Sam Whitlock checked in Neal's Yard Dairy, 9 lines

      Butter 2 kg short. Credit note requested.
    2. Kasia Nowak logged waste on sourdough, 6 loaves

    3. Ines Duarte changed the par for hake fillet

      From 3 kg to 4 kg while the hake special runs.
    4. Tom Okafor sent order #2042 to Natoora

Menu

Install

Writes the block to components/blocks/dashboard.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:dashboard

Built from 14 parts

Notes

  • The shell is the page's main landmark and its own scroll area. Set --blk-dashboard-height, or --eq-app-shell-height on it, to fit your layout; 100dvh fills the window.
  • The side navigation moves into a drawer behind a menu button when the shell is narrower than 48rem, measured by the shell, not the window.
  • Each chart names itself and adds its range or largest value to that name, so the figures are readable without seeing them.

Source

components/blocks/dashboard.tsx208 lines
import { ActivityFeed, ActivityFeedGroup, ActivityFeedItem } from "@/components/equip/activity-feed";
import { AppShell } from "@/components/equip/app-shell";
import { Avatar } from "@/components/equip/avatar";
import { BarChart } from "@/components/equip/bar-chart";
import { Button } from "@/components/equip/button";
import { Card, CardBody, CardHeader } from "@/components/equip/card";
import { Donut } from "@/components/equip/donut";
import { Icon } from "@/components/equip/icon";
import { LineChart } from "@/components/equip/line-chart";
import { PageHeader } from "@/components/equip/page-header";
import { Sidebar, SidebarFooter, SidebarItem, SidebarSection } from "@/components/equip/sidebar";
import { Sparkline } from "@/components/equip/sparkline";
import { Stat } from "@/components/equip/stat";
import { StatGroup } from "@/components/equip/stat-group";
import "./dashboard.css";

/*
 * The screen a kitchen opens before service: the week in four figures, food cost
 * against target, where money went in the bin, who it was spent with, and what the
 * team did today. The shell folds its nav into a drawer by its own width.
 */

const gbp = { style: "currency", currency: "GBP", maximumFractionDigits: 0 } as const;
const WEEKS = ["14 Sep", "21 Sep", "28 Sep", "5 Oct", "12 Oct", "19 Oct", "26 Oct", "2 Nov", "9 Nov"];

const nav = (
  <Sidebar label="Main">
    <SidebarSection>
      <SidebarItem href="#today" icon={<Icon name="home" />} current>
        Today
      </SidebarItem>
      <SidebarItem href="#stock" icon={<Icon name="package" />} badge={7}>
        Stock
      </SidebarItem>
      <SidebarItem href="#orders" icon={<Icon name="truck" />} badge={2}>
        Orders
      </SidebarItem>
      <SidebarItem href="#recipes" icon={<Icon name="book-open" />}>
        Recipes
      </SidebarItem>
      <SidebarItem href="#waste" icon={<Icon name="recycle" />}>
        Waste log
      </SidebarItem>
      <SidebarItem href="#reports" icon={<Icon name="bar-chart" />}>
        Reports
      </SidebarItem>
    </SidebarSection>
    <SidebarSection label="Sites">
      <SidebarItem href="#marrow" icon={<Icon name="utensils" />}>
        Marrow
      </SidebarItem>
      <SidebarItem href="#bakery" icon={<Icon name="store" />}>
        Marrow Bakery
      </SidebarItem>
    </SidebarSection>
    <SidebarFooter>
      <SidebarSection>
        <SidebarItem href="#settings" icon={<Icon name="settings" />}>
          Settings
        </SidebarItem>
      </SidebarSection>
    </SidebarFooter>
  </Sidebar>
);

function Brand() {
  return (
    <span className="blk-dashboard-brand">
      <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
        <path d="M7 4h10" />
        <path d="M8 4v2.5a3 3 0 0 1-1.5 2.6A3 3 0 0 0 5 11.7V18a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6.3a3 3 0 0 0-1.5-2.6A3 3 0 0 1 16 6.5V4" />
        <path d="M5 13h14" />
      </svg>
      Larder
    </span>
  );
}

export default function Dashboard() {
  return (
    <AppShell
      className="blk-dashboard"
      navLabel="Menu"
      header={
        <div className="blk-dashboard-header">
          <Brand />
          <span className="blk-dashboard-site">Marrow, Hackney</span>
          <div className="blk-dashboard-header-end">
            <Button variant="ghost" size="s" aria-label="Notifications, 3 new" lead={<Icon name="bell" />} />
            <Avatar name="Ines Duarte" size="s" />
          </div>
        </div>
      }
      nav={nav}
    >
      <div className="blk-dashboard-page">
        <PageHeader
          title="This week at Marrow"
          description="Monday 9 to Friday 13 November. Counts are live from the till and this morning's deliveries."
          meta={[<span key="covers">412 covers so far</span>, <span key="count">Counted 17:40 by Ines</span>]}
          secondaryActions={
            <Button variant="outline" lead={<Icon name="download" />}>
              Export
            </Button>
          }
          primaryAction={
            <Button variant="accent" lead={<Icon name="truck" />}>
              Build tomorrow&apos;s order
            </Button>
          }
        />

        <StatGroup variant="card" label="The week in figures">
          <Stat label="Food cost" value={28.4} unit="%" previous={31.2} change="absolute" goodWhen="down" hint="Of food sales, target 29%" locale="en-GB">
            <Sparkline values={[33.1, 32.4, 31.8, 32.6, 30.9, 31.2, 29.8, 31.2, 28.4]} label="Food cost by week" />
          </Stat>
          <Stat label="Sales" value={18240} format={gbp} previous={16910} goodWhen="up" hint="Food only" locale="en-GB">
            <Sparkline values={[14200, 15100, 14800, 16300, 15900, 16200, 17400, 16910, 18240]} label="Sales by week" />
          </Stat>
          <Stat label="Binned" value={412} format={gbp} previous={598} goodWhen="down" hint="2.3% of purchases" locale="en-GB">
            <Sparkline values={[780, 702, 655, 690, 610, 588, 540, 598, 412]} label="Waste by week" />
          </Stat>
          <Stat label="Below par" value={7} unit="items" hint="3 out before Saturday" locale="en-GB" />
        </StatGroup>

        <div className="blk-dashboard-grid">
          <Card className="blk-dashboard-wide">
            <CardHeader title="Food cost against target" description="Share of food sales spent on ingredients, by week" />
            <CardBody>
              <LineChart
                label="Food cost by week"
                labels={WEEKS}
                series={[
                  { label: "Food cost", values: [33.1, 32.4, 31.8, 32.6, 30.9, 31.2, 29.8, 31.2, 28.4] },
                  { label: "Target", values: [29, 29, 29, 29, 29, 29, 29, 29, 29] },
                ]}
                zero={false}
                min={26}
                max={34}
                height={180}
                format={{ style: "unit", unit: "percent", maximumFractionDigits: 1 }}
                smooth
                locale="en-GB"
              />
            </CardBody>
          </Card>

          <Card>
            <CardHeader title="Where the waste went" description="This week, by reason" />
            <CardBody>
              <BarChart
                label="Waste this week by reason"
                orientation="horizontal"
                format={gbp}
                locale="en-GB"
                data={[
                  { label: "Over-prepped", value: 164, tone: "warn" },
                  { label: "Past date", value: 118, tone: "danger" },
                  { label: "Spoiled in delivery", value: 72 },
                  { label: "Staff meals", value: 58 },
                ]}
              />
            </CardBody>
          </Card>

          <Card>
            <CardHeader title="Spend by supplier" description="Last 30 days" />
            <CardBody>
              <Donut
                label="Spend by supplier"
                size={112}
                format={gbp}
                locale="en-GB"
                center={<span className="blk-dashboard-centre">£21.6k</span>}
                data={[
                  { label: "Flock & Herd", value: 6840 },
                  { label: "Natoora", value: 5210 },
                  { label: "Neal's Yard Dairy", value: 3980 },
                  { label: "Shellseekers", value: 3460 },
                  { label: "Wildfarmed", value: 1270 },
                  { label: "Belazu", value: 840 },
                ]}
              />
            </CardBody>
          </Card>

          <Card className="blk-dashboard-feed">
            <CardHeader title="Today in the kitchen" />
            <CardBody>
              <ActivityFeed label="Kitchen activity today">
                <ActivityFeedGroup label="Today">
                  <ActivityFeedItem actor="Sam Whitlock" verb="checked in" object="Neal's Yard Dairy, 9 lines" href="#delivery-2041" time="16:05" dateTime="2026-11-13T16:05">
                    Butter 2 kg short. Credit note requested.
                  </ActivityFeedItem>
                  <ActivityFeedItem actor="Kasia Nowak" verb="logged waste on" object="sourdough, 6 loaves" href="#waste-881" time="14:30" dateTime="2026-11-13T14:30" />
                  <ActivityFeedItem actor="Ines Duarte" verb="changed the par for" object="hake fillet" href="#sku-1173" time="11:12" dateTime="2026-11-13T11:12">
                    From 3 kg to 4 kg while the hake special runs.
                  </ActivityFeedItem>
                  <ActivityFeedItem actor="Tom Okafor" verb="sent" object="order #2042 to Natoora" href="#order-2042" time="09:47" dateTime="2026-11-13T09:47" />
                </ActivityFeedGroup>
              </ActivityFeed>
            </CardBody>
          </Card>
        </div>
      </div>
    </AppShell>
  );
}
components/blocks/dashboard.css89 lines
@layer blocks {
  /* The shell is the container here; set --blk-dashboard-height to fit your page. */
  .blk-dashboard {
    --eq-app-shell-height: var(--blk-dashboard-height, 52rem);
  }

  .blk-dashboard-header {
    display: flex;
    align-items: center;
    gap: var(--eq-space-3);
    min-width: 0;
    flex: 1;
  }

  .blk-dashboard-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--eq-space-2);
    font-weight: var(--eq-weight-strong);
  }

  .blk-dashboard-site {
    overflow: hidden;
    padding-inline-start: var(--eq-space-3);
    border-inline-start: 1px solid var(--eq-line);
    color: var(--eq-ink-2);
    font-size: var(--eq-text-s);
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .blk-dashboard-header-end {
    display: flex;
    align-items: center;
    gap: var(--eq-space-2);
    margin-inline-start: auto;
  }

  .blk-dashboard-page {
    container: blk-dashboard / inline-size;
    display: grid;
    gap: var(--eq-space-5);
    padding: var(--eq-space-4) var(--eq-space-3) var(--eq-space-6);
  }

  .blk-dashboard-grid {
    display: grid;
    gap: var(--eq-space-4);
  }

  .blk-dashboard-grid > * {
    min-width: 0;
  }

  .blk-dashboard-centre {
    font-weight: var(--eq-weight-strong);
    font-variant-numeric: tabular-nums;
  }

  @container blk-dashboard (width >= 40rem) {
    .blk-dashboard-page {
      padding: var(--eq-space-5);
    }

    .blk-dashboard-grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .blk-dashboard-wide,
    .blk-dashboard-feed {
      grid-column: 1 / -1;
    }
  }

  @container blk-dashboard (width >= 64rem) {
    .blk-dashboard-grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .blk-dashboard-wide {
      grid-column: span 2;
    }

    .blk-dashboard-feed {
      grid-column: 3;
      grid-row: 1 / span 2;
    }
  }
}

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