CRUD
The resource lifecycle. Full templates for taking data in and out — SpreadsheetPage, ImportPage, ExportPage and SavedViewsPage — plus the configured presets of the keystone ResourceIndexPage / ResourceEditPage recipes: DataGridPage, SearchResultsPage, AuditLogPage, GalleryPage and ResourceCreatePage.
The full ResourceIndexPage / ResourceDetailPage / ResourceEditPage keystones live on the Page recipes overview; the templates and presets below specialise them for the list-and-edit surfaces an admin lives in.
SpreadsheetPage
A bulk-edit grid page: a toolbar band over a full-width spreadsheet/data-grid, with an optional columns rail.
Roster sheet
| Name | Team | ZIP | Status |
|---|---|---|---|
| Maria Rivera | Bay Area | 94601 | Active |
| Daniel Okafor | Central Valley | 93720 | Onboarding |
| Grace Chen | North Coast | 95521 | Active |
| Elena Novak | Bay Area | 94110 | Active |
| Theo Banks | Sacramento | 95814 | Paused |
| Carmen Flores | Central Valley | 93291 | Active |
| Priya Nair | North Coast | 95437 | Onboarding |
| Sam Ortega | Sacramento | 95818 | Active |
248 rows · 6 columns
- grid
- A caller-passed spreadsheet/data-grid (ds/grid VirtualDataGrid/DataGrid, a Univer sheet, or a passed table); the heavy editing lib lives in this slot. → TablePage table. Omit → STUB placeholder.
- toolbar / facets
- toolbar is the search / view-switch / bulk-actions band; facets is an optional columns rail that splits the body (facetWidth default 240px).
- footer
- Row/column counts, selection summary. Server-safe shell; the editing island rides the grid slot.
ImportPage
The 'bring data in' page: an upload/preview canvas with a column-mapping aside and an optional step strip.
Import volunteers
- Upload
- Map
- Preview
- Done
volunteers-nvda-export.csv · 248 rows · showing first 5
| Full Name | Email Address | City | ZIP / Postal |
|---|---|---|---|
| Maria Rivera | maria.r@example.org | Oakland | 94601 |
| Daniel Okafor | d.okafor@example.org | Fresno | 93720 |
| Grace Chen | grace.chen@example.org | Arcata | — missing — |
| Elena Novak | elena.n@example.org | San Francisco | 94110 |
| Theo Banks | theo.banks@example.org | Sacramento | 95814 |
245 valid · 3 skipped
- viewer (the canvas)
- Upload dropzone / parsed-source preview (caller-passed; ds/files FileUpload + the papaparse/xlsx parse lib live HERE). → DocumentLayout viewer. Omit → STUB placeholder.
- steps / aside
- steps is an optional ProgressSteps strip (Upload → Map → Preview → Done); aside is the column-mapping / validation rail (source-col → field selects, error counts; default 320px).
- footer
- The Import action + provenance. Server-safe shell; the heavy parse island rides the viewer slot.
ExportPage
The 'take data out' page: a centered export-config form with a file-summary aside and a Download footer.
Export roster
Generated from the live roster · 2026-06-28 · excludes archived records.
- content
- The export-config form — dataset / format / field-selection / filters (caller-passed). → RecordPage main. Omit → STUB placeholder.
- aside / footer
- aside is the summary/preview (row count, size estimate, format note); footer is the Export/Download action + provenance.
- maxWidth
- Centers to a reading column (default true). Server-safe — the blob-build / download work lives in the footer action.
SavedViewsPage
A management list for saved filters/views: a toolbar band over saved-view rows, with an optional facet rail.
Saved views
Team = Phonebank · Status = Active
Region = Bay Area · Status = Onboarding
Status = Paused · Last activity > 90d
Region = Sacramento · sorted by Name
Joined ≥ 2026-06-01 · Status = Active
Role = Adopter · sorted by Joined
6 views
- table (the list)
- Saved-view rows or cards (caller-passed table/list; ds/grid SavedView data builds the rows). → TablePage table. Omit → STUB placeholder.
- toolbar / facets
- toolbar is the search + 'New view' band; facets is an optional rail (Mine / Shared / by resource type) at facetWidth (default 240px).
- footer
- Count / pagination. Server-safe shell.
DataGridPage
This is ResourceIndexPage configured as a data grid — the table slot holds a full ds/grid DataGrid (selectable, sortable, sticky-header) instead of a passed table.
import { ResourceIndexPage } from "@/components/ds/recipes";
import { DataGrid, FilterBar } from "@/components/ds/grid";
import { Pagination } from "@savethemarshalldogs/design-system";
<ResourceIndexPage
title="Rescuers"
primaryAction={{ key: "new", label: "New rescuer", href: "/admin/rescuers/new" }}
toolbar={<FilterBar fields={fields} value={filter} onChange={setFilter} search={search} />}
table={<DataGrid<Rescuer> rows={rows} columns={columns} getRowId={(r) => r.id} selectable density="compact" stickyHeader />}
footer={<Pagination page={page} pageCount={pageCount} hrefFor={(p) => `?page=${p}`} />}
/>SearchResultsPage
This is ResourceIndexPage configured as search results — toolbar is the query CommandBar, facets refine, and the table slot holds ranked result rows instead of a grid.
import { ResourceIndexPage } from "@/components/ds/recipes";
import { CommandBar } from "@/components/ds/surface";
import { FacetRail } from "@/components/ds/grid";
import { Pagination } from "@savethemarshalldogs/design-system";
<ResourceIndexPage
title={`Results for "${q}"`}
toolbar={<CommandBar search={<Input type="search" defaultValue={q} />} controls={scopeChips} />}
facets={<FacetRail>{facetGroups}</FacetRail>}
table={<SearchResultList items={results} />}
footer={<Pagination page={page} pageCount={pageCount} hrefFor={hrefFor} />}
/>AuditLogPage
This is ResourceIndexPage configured as an immutable audit log — no 'New' action; the table slot is a stack of @phauna/ds AuditEntry rows rather than a DataGrid.
import { ResourceIndexPage } from "@/components/ds/recipes";
import { FilterBar } from "@/components/ds/grid";
import { AuditEntry, Pagination } from "@savethemarshalldogs/design-system";
<ResourceIndexPage
title="Audit log"
toolbar={<FilterBar fields={auditFields} value={filter} onChange={setFilter} />}
table={
<ol className="space-y-2">
{entries.map((e) => (
<li key={e.id}>
<AuditEntry actor={e.actor} action={e.action} time={e.time} />
</li>
))}
</ol>
}
footer={<Pagination page={page} pageCount={pageCount} hrefFor={hrefFor} />}
/>GalleryPage
This is ResourceIndexPage configured as a media gallery — the table slot holds a ds/surface Gallery (or MasonryGrid) of thumbnails instead of a tabular grid.
import { ResourceIndexPage } from "@/components/ds/recipes";
import { Gallery, CommandBar } from "@/components/ds/surface";
import { Pagination } from "@savethemarshalldogs/design-system";
<ResourceIndexPage
title="Foster spaces"
toolbar={<CommandBar search={searchInput} controls={tagChips} />}
table={<Gallery items={photos} minColumnWidth="14rem" ratio="4 / 3" />}
footer={<Pagination page={page} pageCount={pageCount} hrefFor={hrefFor} />}
/>ResourceCreatePage
This is ResourceEditPage configured for creation — the same RecordPage body + dirty-aware SaveBar + browser leave-guard, but a blank form and Save labelled 'Create'.
import { ResourceEditPage } from "@/components/ds/recipes";
<ResourceEditPage
title="New rescuer"
form={<RescuerForm value={draft} onChange={setDraft} />}
aside={<CreateHelp />}
dirty={dirty}
saving={saving}
onSave={create}
onDiscard={() => router.back()}
saveLabel="Create"
discardLabel="Cancel"
/>