Resolved: light

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.

NameTeamZIPStatus
Maria RiveraBay Area94601Active
Daniel OkaforCentral Valley93720Onboarding
Grace ChenNorth Coast95521Active
Elena NovakBay Area94110Active
Theo BanksSacramento95814Paused
Carmen FloresCentral Valley93291Active
Priya NairNorth Coast95437Onboarding
Sam OrtegaSacramento95818Active

248 rows · 6 columns

<SpreadsheetPage> — TitleBar + status badge · search · "View" Select toolbar · full-bleed editable grid (static stand-in for the caller-passed VirtualDataGrid in the `grid` slot) · count footer
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

Step 2 of 4
  1. Upload
  2. Map
  3. Preview
  4. Done

volunteers-nvda-export.csv · 248 rows · showing first 5

Full NameEmail AddressCityZIP / Postal
Maria Riveramaria.r@example.orgOakland94601
Daniel Okaford.okafor@example.orgFresno93720
Grace Chengrace.chen@example.orgArcata— missing —
Elena Novakelena.n@example.orgSan Francisco94110
Theo Bankstheo.banks@example.orgSacramento95814

245 valid · 3 skipped

<ImportPage> — TitleBar · ProgressSteps rail (Upload → Map → Preview → Done) · parsed source preview + column-mapping aside · commit footer. The ds/files FileUpload + papaparse/xlsx parse live in the `viewer` slot (static stand-in here).
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

248 records
Format
Columns
Joined between
to

Generated from the live roster · 2026-06-28 · excludes archived records.

<ExportPage> — TitleBar · centered config form (dataset · format · column checklist · date range) + summary aside (≈248 rows · ~32 KB · CSV · UTF-8) · Download footer. The file-build / blob-download work lives in the `footer` action.
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

Active phonebankersMine

Team = Phonebank · Status = Active

Bay Area onboardingShared

Region = Bay Area · Status = Onboarding

Lapsed volunteersMine

Status = Paused · Last activity > 90d

Sacramento rosterShared

Region = Sacramento · sorted by Name

New this monthMine

Joined ≥ 2026-06-01 · Status = Active

All adoptersShared

Role = Adopter · sorted by Joined

6 views

<SavedViewsPage> — TitleBar (+ New view) · scope toolbar (search + owner Select) · saved-view list (name · Mine/Shared badge · filter summary · last-used · ⋮ overflow) · count footer
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.

tsx
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}`} />}
/>

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.

tsx
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} />}
/>

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

tsx
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"
/>