logicspike/docs

Seller Dashboard

Seller dashboard: the page header standard

Decided 2026-10-09 by the founder: "drop slogans, title labels, every noise, and set a standard rule; take very minimum space." Component: apps/seller-dashboard/src/components/page-header.tsx (PageHeader, PageAction, HeaderCount).

Why

The header above every page already says where you are: the service button plus the highlighted page tab. An audit of about 70 page headers found that each page then repeated that:

  • No shared component. Every page hand-built its own header.
  • Seven title sizes (text-xl up to text-5xl), in five hand-built styles.
  • Heights from about 60px to 300px. Content started far down the screen.
  • Repetition: an eyebrow restating the nav ("FORMS · INBOX"). The Forms page said "Forms" three times.
  • Slogans as titles ("Pulse of your library.", "Deliver / ability.").
  • Toolbars repeating nav tabs as buttons.
  • Copies: several hand-made copies of the stat strip and the live dot.

The rule

A page header is one row, 44px. It contains only:

[← Parent /]  Page name  [count or status]                [secondary] [secondary] [Primary]
tabs (only when the page has its own sections)
  1. Title. On a page reached from a nav tab, the title is exactly that tab's label (from service-registry.ts). On a record page it is the record's name, after a ← Parent back link. Anywhere else, a short plain name.
  2. Meta. At most one small item next to the title: a HeaderCount or a status chip.
  3. Actions. Right-aligned: at most two PageAction secondaries, then one PageAction primary. Everything uses the same size and the dashboard's editorial button style.
  4. Tabs. Only a page's own sections go in the tabs slot: a form's Build/Share/Settings, a collection's tabs, Billing, Team. The header then becomes a third header row, built like row 2: full-bleed, the page name on the left, a divider, then the tabs, actions on the right. Use HeaderTabsList / HeaderTabsTrigger (exported from page-header.tsx): plain underline tabs, the same look as row 2. Never the grey pill TabsList, never icons in these tabs. On phones the tabs drop to their own line under the name.

Section headings inside a page are <SectionHead title="…" /> (16px, semibold). The old numbered eyebrow ("01 — Identity") is no longer rendered anywhere.

Never in a page header:

  • an eyebrow, kicker or "Service · Page" label
  • a slogan or tagline
  • a description or subtitle
  • a live dot or decorative status
  • a stat strip (stats are the page's first row, directly under the header)
  • filters, search, range toggles or steppers (they live in the page, directly under the header)
  • a button that links to a nav tab of the same service
  • a divider (unless there are tabs)

No page header on Home either: the coral "⌂ Home" button names it, and it starts straight with "Needs you" (a visually hidden <h1>Home</h1> keeps the outline right).

No page header at all on full-screen consoles and editors. They keep their own toolbars:

Kind Screens
Consoles Mailbox inbox, Chatbot Sandbox, AI Copilot, Contacts Playground (chat)
Editors blog editor, newsletter composer and template editor, content composer

Spacing. PageHeader brings its own pt-2 mb-4, so the header-to-content gap is 16px everywhere. Don't wrap it in extra padding.

Full width. Every page uses the full content width, like the service pages. Never cap a page wrapper (max-w-3xl, max-w-5xl…) and never centre it with mx-auto: a capped page either hugs the left with an empty right half or floats away from the tabs above it. Only text blocks (a paragraph, an empty state) may cap their own line length.

No back link to a tab. back is for record pages (a key, a form, a campaign). A page that already has a tab in row 2 never gets "← Parent": the tab row is the way back.

Status

Rolled out 2026-10-09 to every page in the dashboard shell: Forms, Engagement, Collections, Flows, Chatbot, AI Brain, Contacts, Blog, Newsletter, Mailbox, Content Engine, Media, Dashboard, Integrations and Settings. The shared StatStrip now sizes to its cells.

The account-settings area (/settings, profile, security, notifications, appearance, account) followed the same day. Every page with a header in the app now uses PageHeader. The exceptions are by design: consoles, editors, sign-in/invite, and the legal pages.

Adding a page

<PageHeader
    title="Campaigns"
    meta={<HeaderCount value={campaigns.length} label="campaigns" />}
    actions={
        <>
            <PageAction>Import</PageAction>
            <PageAction primary asChild>
                <Link href="/dashboard/newsletter/campaigns/new">New campaign</Link>
            </PageAction>
        </>
    }
/>
<StatStrip>…</StatStrip>   {/* stats, if any: first row of the page */}

Record page with its own sections:

<Tabs value={active}>
    <PageHeader
        back={{ href: "/dashboard/forms", label: "Forms" }}
        title={form.name}
        meta={<StatusChip status={form.status} />}
        actions={<PageAction>Open hosted page</PageAction>}
        tabs={
            <HeaderTabsList aria-label="Form sections">
                <HeaderTabsTrigger value="build" asChild><Link href="…/build">Build</Link></HeaderTabsTrigger>
                …
            </HeaderTabsList>
        }
    />
</Tabs>
Seller Dashboard