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.jsonOr 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
<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
<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.