Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

Overview

An app's home page, with headline metrics, a chart, what needs attention and guidance for a new account.

An overview is an app's home page: headline numbers for the period, a chart behind one of them, the few things that need attention, and guidance while an account is new. Build it from MetricCard, StatTabs, the charts and SetupGuide; it adds no component of its own.

How it fits together

  • Headline numbers

    MetricCard shows one number with its change and a sparkline, and loads on its own.

  • A chart behind them

    StatTabs puts a few totals over one chart and switches the chart between them.

  • What needs attention

    RecommendationCard and Banner raise what needs a decision, each with an action and how long it takes, and are dismissed for good once closed.

  • Guidance while it's new

    SetupGuide floats at the bottom end until the account is set up, showing its progress and the next task.

Every block keeps its size while it loads, so the page doesn't shift as data arrives.

Here is a whole overview: a banner, two metric cards for the week, a chart of daily totals under StatTabs, a tip, and a setup guide. The numbers are written into the file to keep it short; an app's would come from its data.

src/pages/OverviewPage.tsx

import { useState } from "react";
import type { ReactElement } from "react";
import { Grid } from "@butteryui/core";
import {
  AppPage,
  Banner,
  BarChart,
  MetricCard,
  PageHeader,
  RecommendationCard,
  SetupGuide,
  StatTabs,
} from "@butteryui/react";
import { Truck } from "lucide-react";

const WEEK = ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"];

const dollars = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
});

const DAILY = [
  { value: "orders", label: "Orders", values: [38, 42, 40, 47, 51, 63, 58] },
  { value: "bags", label: "Bags", values: [61, 70, 66, 78, 84, 102, 95] },
  { value: "refunds", label: "Refunds", values: [1, 0, 2, 0, 1, 1, 0] },
] as const;

function total(values: readonly number[]): number {
  return values.reduce((sum, value) => sum + value, 0);
}

export function OverviewPage(): ReactElement {
  const [metric, setMetric] = useState("orders");
  const shown = DAILY.find((entry) => entry.value === metric) ?? DAILY[0];
  return (
    <AppPage
      header={
        <PageHeader
          title="Overview"
          description="How the roastery did this week, and what needs you."
        />
      }
    >
      <Banner
        icon={Truck}
        action={{ label: "Turn on pickup", href: "/settings/pickup" }}
        estimate="2 min"
        storageKey="overview-pickup"
      >
        Customers nearby can collect their coffee at the roastery.
      </Banner>
      <AppPage.Section title="This week">
        <Grid columns={{ base: 1, md: 2 }} gap={4}>
          <MetricCard
            title="Sales"
            value={dollars.format(8420)}
            delta={0.12}
            labels={WEEK}
            series={[
              {
                key: "now",
                label: "This week",
                values: [980, 1040, 1010, 1190, 1260, 1540, 1400],
              },
              {
                key: "before",
                label: "Last week",
                values: [900, 950, 980, 1050, 1120, 1380, 1140],
                comparison: true,
              },
            ]}
            formatValue={(value) => dollars.format(value)}
            updated="Updated just now"
            href="/payouts"
          />
          <MetricCard
            title="New subscribers"
            value="24"
            delta={-0.04}
            labels={WEEK}
            series={[
              {
                key: "now",
                label: "This week",
                values: [2, 4, 3, 5, 3, 4, 3],
              },
              {
                key: "before",
                label: "Last week",
                values: [3, 4, 4, 5, 4, 3, 2],
                comparison: true,
              },
            ]}
            updated="Updated just now"
            href="/subscriptions"
          />
        </Grid>
      </AppPage.Section>
      <AppPage.Section title="Day by day">
        <StatTabs
          label="Daily numbers"
          tabs={DAILY.map((entry) => ({
            value: entry.value,
            label: entry.label,
            count: total(entry.values),
          }))}
          value={metric}
          onValueChange={setMetric}
        />
        <BarChart
          label={`${shown.label} each day`}
          labels={WEEK}
          labelsHeading="Day"
          series={[
            { key: shown.value, label: shown.label, values: shown.values },
          ]}
        />
      </AppPage.Section>
      <AppPage.Section title="Worth a look">
        <RecommendationCard
          title="Get an alert when green coffee runs low"
          action={{ label: "Turn on alerts", href: "/settings/notifications" }}
          estimate="2 min"
          storageKey="overview-low-stock"
        >
          Nyeri is down to 180 kg, about nine days of roasting.
        </RecommendationCard>
      </AppPage.Section>
      <SetupGuide
        title="Get your shop ready"
        tasks={[
          { id: "coffee", title: "Add your first coffee", done: true },
          { id: "shipping", title: "Set shipping rates", done: true },
          {
            id: "bank",
            title: "Connect your bank",
            action: { label: "Connect", href: "/settings/payouts" },
          },
        ]}
        storageKey="overview-setup"
      />
    </AppPage>
  );
}

Three places show it working:

See MetricCard, StatTabs and SetupGuide for each part.

Guidance of your own

Two parts behind the guidance components are exported for guidance you build yourself.

TimeEstimate

TimeEstimate says how long a task takes, with a 14px clock and the time in caption, read as "Takes about 1 min". SetupGuide and RecommendationCard show it beside their tasks.

useDismissible

useDismissible is the state behind a part people can dismiss for good, as Banner and RecommendationCard are. When you call dismiss():

  • The part fades where it is over dur-fast, then dismissed turns true and you render nothing.
  • Inside an AppPage, what's below then slides up into its place.
  • Focus in the part moves on to the next tab stop as it goes.
  • With a storageKey, the part stays gone on later visits.

Spread its motionProps on the part's root, a buttery.div:

src/RoastDayTip.tsx

import { X } from "lucide-react";
import type { ReactElement } from "react";
import { Inline, Text } from "@butteryui/core";
import { buttery, IconButton, useDismissible } from "@butteryui/react";

export function RoastDayTip(): ReactElement | null {
  const { dismissed, dismiss, motionProps } = useDismissible({
    kind: "tip",
    storageKey: "roast-day",
  });
  if (dismissed) return null;
  return (
    <buttery.div {...motionProps}>
      <Inline gap={2} align="center">
        <Text variant="body">Roast days move to Thursday from next week.</Text>
        <IconButton icon={X} label="Dismiss" onClick={dismiss} />
      </Inline>
    </buttery.div>
  );
}
  • MetricCard

    One metric on a card: its title, value and change against the previous period, a small chart of both periods, and when it was updated.

  • StatTabs

    Equal tabs over a list that each show a view's count, the chosen one on a sliding raised panel, a select on a phone.

  • SetupGuide

    A floating checklist in the bottom-end lane: progress, the next task, and every task when unfolded, remembered across visits.

  • RecommendationCard

    A tip on a card: what to try, why, a link to do it and how long it takes, with optional media, dismissed for good once closed.