UNPKG

@spark-web/design-system

Version:

--- title: Design System ---

422 lines (338 loc) 14 kB
# Internal admin — detail page pattern ## Before using this pattern Surface rules: read `node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md` in full first — its rules all apply here. ## What this pattern is A full page layout for displaying the detail view of a single record in an internal admin interface. The page shows structured data and contextual sub-tables in a two-column layout, with record-level actions surfaced through a header dropdown. ## When to use this pattern Any single-record detail/profile viewthe registry (`node_modules/@spark-web/design-system/patterns/CLAUDE.md`) owns surface and feature-type classification. --- ## Component docs to read Read these before implementingthey own the component-level rules: - `node_modules/@spark-web/action-dropdown/CLAUDE.md` — dropdown construction, ordering, hide vs. disable - `node_modules/@spark-web/modal-dialog/CLAUDE.md` — ContentDialog API, admin modal sizing, destructive modal anatomy - `node_modules/@spark-web/data-table/CLAUDE.md` — DataTable API, loading/empty states, expandable rows - `node_modules/@spark-web/tabs/CLAUDE.md` — Tabs API, internal-admin background override, null guard - `node_modules/@spark-web/section-card/CLAUDE.md` — SectionCard API - `node_modules/@spark-web/section-header/CLAUDE.md` — SectionHeader API, for SectionCard headers - `node_modules/@spark-web/badge/CLAUDE.md` — status tone mapping - `node_modules/@spark-web/columns/CLAUDE.md` — responsive two-column layout - `node_modules/@spark-web/box/CLAUDE.md` — flex layout utilities - `node_modules/@spark-web/stack/CLAUDE.md` — vertical stacking and gap - `node_modules/@spark-web/heading/CLAUDE.md` — detail-page H1 - `node_modules/@spark-web/alert/CLAUDE.md` — page-level feedback Alert --- ## Page structure ``` Outer wrapper Stack paddingX="xlarge" paddingY="xxlarge" gap="xlarge" Header Box spaceBetween row: [Heading level="1" + Badge] | [ActionDropdown] Page-level feedback Alert conditionalonly for inline (non-modal) action feedback Modals all ContentDialog modals declared here, controlled by openModal state Content Columns template=[1,1] gap="xlarge" collapseBelow="desktop" Left column Stack gap="xlarge" — primary record fields + primary sub-tables Right column Stack gap="xlarge" — secondary/contextual sections SectionCard (per section) DataTable PAGE_SIZE=5 items; see node_modules/@spark-web/data-table/CLAUDE.md TablePagination consumer-supplied — only when total > PAGE_SIZE ``` --- ## Section 1 — Outer wrapper ```tsx <Stack height="full" paddingX="xlarge" paddingY="xxlarge" gap="xlarge"> ``` All spacing uses Spark tokens. This is distinct from list-page spacing (`padding="large"` on a neutral-background Stack) — do not mix the two. --- ## Section 2 — Page header ```tsx <Box display="flex" flexDirection={{ mobile: 'column', tablet: 'row' }} justifyContent={{ tablet: 'spaceBetween' }} gap="medium" > <Box display="flex" flexDirection={{ mobile: 'columnReverse', tablet: 'row' }} alignItems={{ mobile: 'start', tablet: 'center' }} gap={{ mobile: 'medium', tablet: 'small' }} > <Heading level="1">{recordTitle}</Heading> <Badge tone={statusTone}>{statusLabel}</Badge> </Box> {actions.length > 0 && ( <Box css={{ minWidth: 130 }}> <ActionDropdown label="Actions" actions={actions} /> </Box> )} </Box> ``` Rules: - Status badge follows the heading — never precedes it - Only render `ActionDropdown` when `actions.length > 0` - No action buttons directly in the header — always use `ActionDropdown` --- ## Section 3 — Actions dropdown See `node_modules/@spark-web/action-dropdown/CLAUDE.md` for full API and ordering rules. Page-level decisions: - **Order**: non-destructive actions first, restore-type actions second, `tone: 'critical'` destructive actions always last - **Hide vs. disable**: hide actions permanently unavailable for the current record state (conditional spread); disable actions temporarily unavailable (e.g. mutation in-flight) - **Direct vs. modal**: direct actions (password reset, restore, activate) call the mutation inline and surface feedback via page-level `actionStatus` Alert; destructive actions (delete, suspend) always open a `ContentDialog` modal first --- ## Section 4 — Page-level feedback ```tsx { actionStatus && ( <Alert tone={actionStatus.isSuccessful ? 'positive' : 'critical'}> <Text>{actionStatus.message}</Text> </Alert> ); } ``` State shape: `{ isSuccessful: boolean; message: string } | undefined` Rendered between the header and content columns. Only used for direct inline mutations. Modal confirmations own their own error Alert inside the `ContentDialog` — see `node_modules/@spark-web/modal-dialog/CLAUDE.md`. --- ## Section 5 — Confirmation modals See `node_modules/@spark-web/modal-dialog/CLAUDE.md` for ContentDialog API, sizing, form-in-modal anatomy, and the full destructive modal pattern. Declare all modals in the component JSX, controlled by a single `openModal` state union: ```ts const [openModal, setOpenModal] = useState<'delete' | 'suspend' | null>(null); ``` ```tsx { openModal === 'delete' && recordId && ( <DeleteModal isOpen onToggle={() => setOpenModal(null)} recordId={recordId} onSuccess={() => { refetch(); setOpenModal(null); }} /> ); } ``` --- ## Section 6 — Content columns ```tsx <Columns gap="xlarge" template={[1, 1]} collapseBelow="desktop"> <Stack gap="xlarge">{/* left column */}</Stack> <Stack gap="xlarge">{/* right column */}</Stack> </Columns> ``` Always equal columns (`template={[1, 1]}`), always collapse below desktop. --- ## Section 7 — Section cards Each content section is wrapped in a `SectionCard` from `@spark-web/section-card`, composed with `SectionHeader` from `@spark-web/section-header` for the card header: ```tsx import { SectionCard } from '@spark-web/section-card'; import { SectionHeader } from '@spark-web/section-header'; <SectionCard header={<SectionHeader label="Section Title" />}> {/* section content */} </SectionCard>; ``` | Prop | Type | Notes | | ---------- | ----------- | ------------------------------------------------------- | | `children` | `ReactNode` | Required — main content area | | `header` | `ReactNode` | Optional — renders above the content; use SectionHeader | | `footer` | `ReactNode` | Optional — rendered below a Divider | The header text (`label`), optional `action`, and optional `controls` belong to `SectionHeader` — see `node_modules/@spark-web/section-header/CLAUDE.md`. Return `null` for sections conditionally hidden — never render an empty card. Working in portal-hub? Apply the substitutions in `node_modules/@spark-web/design-system/patterns/internal-admin/portal-hub.md`. --- ## Section 8 — Section data tables See `node_modules/@spark-web/data-table/CLAUDE.md` for DataTable API, column definitions, and loading/empty state props. Detail-page-specific rules (differ from list pages): - **`PAGE_SIZE = 5`** — always 5 items per section table (not 20 like list pages) - **Pagination threshold**: render `TablePagination` only when `total > PAGE_SIZE`. `TablePagination` is NOT a spark-web component (COMPONENT GAP) — build a placeholder from `@spark-web/box` + `@spark-web/button` (prev/next + "Show N of M") and flag it for product design review before production. Props: `total`, `pageSize`, `current`, `dataShowing`, `onChange(page)` - **Reset page**: reset to 1 when the record context changes (e.g. `userId`) ```tsx const PAGE_SIZE = 5; const [page, setPage] = useState(1); useEffect(() => { setPage(1); }, [recordId]); // Client-side pagination — fetch all, slice const allItems = data?.items ?? []; const total = allItems.length; const pageItems = allItems.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); // Server-side pagination — skip/take API const { data } = useQuery({ skip: (page - 1) * PAGE_SIZE, take: PAGE_SIZE }); const total = countData?.count ?? 0; const pageItems = data?.items ?? []; ``` ```tsx <Stack gap="large"> <DataTable items={pageItems} columns={columns} isLoading={isLoading} emptyState={ <Text tone="muted" size="small" align="center"> No items. </Text> } /> {/* COMPONENT GAP: TablePagination — see the pagination rules above */} {total > PAGE_SIZE && ( <TablePagination total={total} pageSize={PAGE_SIZE} dataShowing={pageItems.length} onChange={setPage} current={page} /> )} </Stack> ``` --- ## Section 9 — Tabbed sections See `node_modules/@spark-web/tabs/CLAUDE.md` for the Tabs API, dynamic tab construction, the required internal-admin background override, and the null guard pattern. Use tabs when a section has multiple sub-views (e.g. Email / SMS history). Each tab panel follows the same PAGE_SIZE=5 and pagination rules as Section 8. --- ## Structural skeleton ```tsx <Stack height="full" paddingX="xlarge" paddingY="xxlarge" gap="xlarge"> {/* Header */} <Box display="flex" flexDirection={{ mobile: 'column', tablet: 'row' }} justifyContent={{ tablet: 'spaceBetween' }} gap="medium" > <Box display="flex" flexDirection={{ mobile: 'columnReverse', tablet: 'row' }} alignItems={{ mobile: 'start', tablet: 'center' }} gap={{ mobile: 'medium', tablet: 'small' }} > <Heading level="1">{recordTitle}</Heading> <Badge tone={statusTone}>{statusLabel}</Badge> </Box> {actions.length > 0 && ( <Box css={{ minWidth: 130 }}> <ActionDropdown label="Actions" actions={actions} /> </Box> )} </Box> {/* Page-level feedback — direct actions only */} {actionStatus && ( <Alert tone={actionStatus.isSuccessful ? 'positive' : 'critical'}> <Text>{actionStatus.message}</Text> </Alert> )} {/* Modals */} {openModal === 'delete' && recordId && ( <DeleteModal isOpen onToggle={() => setOpenModal(null)} recordId={recordId} onSuccess={() => { refetch(); setOpenModal(null); }} /> )} {/* Content */} <Columns gap="xlarge" template={[1, 1]} collapseBelow="desktop"> <Stack gap="xlarge"> <SectionCard header={<SectionHeader label="Details" />}> {/* fields */} </SectionCard> </Stack> <Stack gap="xlarge"> <SectionCard header={<SectionHeader label="History" />}> {/* table + pagination */} </SectionCard> </Stack> </Columns> </Stack> ``` --- ## Documented exceptions summary These raw CSS values are required and have no Spark token equivalent. Use them exactly as written. | Value | Property | Reason | | --------------- | ---------------------------- | -------------------------------------------------------------------- | | `minWidth: 130` | ActionDropdown container Box | Prevents dropdown collapse on narrow content; no Spark minWidth prop | --- ## Do NOTs - NEVER apply list-page spacing to a detail page — outer wrapper uses `paddingX="xlarge" paddingY="xxlarge"`, not `padding="large"` - NEVER place a destructive action before non-destructive actions in the dropdown - NEVER call a destructive mutation directly from a dropdown item — open a modal - NEVER surface modal errors as page-level Alerts — see `node_modules/@spark-web/modal-dialog/CLAUDE.md` - NEVER use `PAGE_SIZE = 20` on section tables — always 5 on detail pages - NEVER render `TablePagination` when `total <= PAGE_SIZE` - NEVER render an empty `SectionCard` for hidden sections — return `null` - NEVER render `ActionDropdown` when `actions` is empty — gate with `actions.length > 0` - NEVER render `Tabs` inside `SectionCard` without the background override — see `node_modules/@spark-web/tabs/CLAUDE.md` - NEVER use `Container` as the outer page wrapper --- ## Validation checklist Run before marking any detail-page task complete and fix every violation; the uplift protocol in `node_modules/@spark-web/design-system/CLAUDE.md` runs this list PASS/FAIL against existing pages. 1. Outer wrapper is `Stack height="full" paddingX="xlarge" paddingY="xxlarge" gap="xlarge"` — not list-page spacing, not Container. 2. Header: `Heading level="1"` first, `Badge` after it; ActionDropdown only when `actions.length > 0`; no inline action buttons in the header. 3. Dropdown action order: non-destructive → restore-type → critical destructive last; permanently unavailable actions hidden, temporarily unavailable actions disabled. 4. Destructive actions open a ContentDialog modal; direct actions surface feedback via the page-level Alert between header and content — modal errors render inside the modal, never as page-level Alerts. 5. All modals declared in JSX, controlled by a single `openModal` union state. 6. Content is `Columns template={[1, 1]} gap="xlarge" collapseBelow="desktop"`. 7. Every content section is wrapped in a SectionCard (or the documented consumer-overlay equivalent); conditionally hidden sections return `null`. 8. Section tables use `PAGE_SIZE = 5`; pagination only when `total > PAGE_SIZE`; page resets when the record context changes. 9. Tabs inside SectionCard use the internal-admin background override per `node_modules/@spark-web/tabs/CLAUDE.md`. 10. No raw CSS beyond the Documented exceptions table; every component is `@spark-web/*` or an explicit consumer-overlay substitute; gaps flagged with `// COMPONENT GAP:`.