designComponents

Search the design system…

Search the design system…

layouts

DSListPage

**Use this for every /list page. Always.** Composite layout that bundles NotebookTabs (optional, top) + title row + headerActions + scroll-owned body + footer in the canonical order. Enforces the table-page rules in code, not in docs — agents cannot accidentally split the page, drop the footer, mismatch control sizes, or put tabs below the title.

DSPageShell is the lower-level building block; DSListPage is the locked composition you should reach for first. The headerActions slot auto-sizes its direct interactive children (button / input / select-trigger) to 32px so search inputs and filter buttons always line up vertically — the most common DS regression in product list pages. With `tabs`, NotebookTabs render above and per-tab pages live inside, in the canonical order (tabs → title → actions → table → footer).

Install

Pull this component (and its dependencies) straight into your app via the shadcn CLI:

npx shadcn@latest add https://design.oapps.io/r/ds-list-page.json

Or import from the workspace package:

import { DSListPage, DSListPageProps, DSListPageTab } from "@8maverik8/design";

Examples

Single-page mode (no tabs)

The canonical /list page. Filter + search + primary action in headerActions; table as the body; mono-caps stats footer.

Audiences

NameStatusResources
PegasusActive23
HydraPaused8
CentaurActive17

3 audiences · last updated 2m ago

<DSListPage
  title="Audiences"
  headerActions={
    <>
      <Button variant="outline" size="default">All lifecycle stages</Button>
      <Input placeholder="Search audiences…" className="w-56" />
      <Button><Plus />New audience</Button>
    </>
  }
  footer="3 audiences · last updated 2m ago"
>
  <DSTable columns={columns} data={data} rowKey={(r) => r.id} />
</DSListPage>

Tabbed mode — NotebookTabs above title

When the page partitions into sub-pages (Findings / Context / Settings). NotebookTabs render at the top; each tab has its own title, headerActions, footer and body. The composition is locked — agents physically cannot put tabs below the title.

Findings

NameStatusResources
PegasusActive23
HydraPaused8
CentaurActive17

1,248 findings · 4 critical

<DSListPage
  tabs={[
    {
      value: "findings", label: "Findings", icon: Layers,
      title: "Findings",
      headerActions: <>
        <Button variant="outline">All severities</Button>
        <Input placeholder="Search…" />
        <Button><Plus />New finding</Button>
      </>,
      footer: "1,248 findings · 4 critical",
      body: <DSTable .../>,
    },
    {
      value: "context", label: "Context", icon: Settings,
      title: "Context",
      headerActions: <Button variant="outline">Edit context</Button>,
      footer: "Last edited by jane@oapps.io · 3h ago",
      body: <ContextEditor />,
    },
  ]}
/>

Anatomy

  • titlePage heading. String for static; pass `<EditableTitle>` for inline-rename pages.
  • headerLeftMetaOptional content next to the title (refresh spinner, status badge).
  • headerActionsRight-aligned slot for filters / search / primary action. ALL direct interactive children (button, input, select-trigger) are auto-sized to 32px regardless of what `size` they declare. Order convention: filters → search → primary.
  • footerMono-caps stats line at the bottom. Pass `null` to explicitly hide. The canonical place for counts, last-updated, sync status.
  • tabsWhen set, render NotebookTabs at the top with per-tab `{ title, headerActions, footer, body }`. Without `tabs`, falls back to single-page mode using the base props.
  • defaultTabInitial active tab when `tabs` is set. Defaults to the first tab.
  • childrenBody (table). Used only in single-page mode — when `tabs` is set, each tab carries its own `body`.

Guidelines

  • **Reach for DSListPage first.** Don't compose NotebookTabs + DSPageShell + a div for actions by hand. Composing manually is what regularly drifts; the composite locks the canonical structure.
  • Pass interactive controls directly into `headerActions` — `<Button>`, `<Input>`, `<MultiSelectFilter>`. The wrapper auto-sizes them; you don't have to specify `size`.
  • Put `<NotebookTabs>` BELOW the title row.Tabs partition the page into sub-pages — they belong above. DSListPage with the `tabs` prop physically prevents this; just use it.
  • Put descriptive text, KPI cards, charts, status banners, or chips between the header and the table.A list page is title row → table. If the page genuinely needs metrics + a list, it's two pages.
  • Skip the footer.Footer carries the stats line ("147 audiences · 4m ago"). Without it the page lacks the canonical bottom rail. Pass `footer={null}` to explicitly hide it (rare).
  • Add a description / subtitle under the title on a list page.The table itself is the description. Subtitles push the table down and add noise. If users need help, put it in an Empty state or a column-header tooltip.