UNPKG

@spark-web/design-system

Version:

--- title: Design System ---

414 lines (330 loc) 13.9 kB
# Internal admin — list page pattern ## Before using this pattern Read node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md fully before implementing this pattern. The interaction rules, badge tone mapping, row clickability rules, and overflow menu rules defined there all apply to this pattern. ## What this pattern is A full page layout for displaying a searchable, filterable, paginated list of records in an internal admin interface. This is the most common page type in admin surfaces. ## When to use this pattern Use this pattern when the PRD describes any of the following: - A list of records that can be searched, filtered, or sorted - A management interface where records can be viewed or acted upon - The words "list", "manage", "view all", "records", or "results" --- ## Component docs to read Read these before implementing — they own the component-level rules: - `packages/page-header/CLAUDE.md` — PageHeader API, action type rules - `packages/data-table/CLAUDE.md` — DataTable API, column defs, loading/empty states, headerClassName tokens - `packages/badge/CLAUDE.md` — status tone mapping - `packages/meatball-menu/CLAUDE.md` — MeatballMenu API - `packages/stack/CLAUDE.md` — vertical stacking and gap - `packages/box/CLAUDE.md` — flex layout utilities - `packages/columns/CLAUDE.md` — responsive multi-column layout - `packages/field/CLAUDE.md` — Field API, labelVisibility - `packages/text/CLAUDE.md` — Text API - `packages/text-input/CLAUDE.md` — TextInput API --- ## Page structure ``` Page Outer wrapper Stack — full height, no padding PageHeader — label={pageTitle}, optional statusBadge/action/controls Alert (conditional) — page-level feedback only, omit if no page-level actions Content area Stack — padding="large" gap="large", neutral background Filter container — required if filtering or searching exists Table scroll wrapper — flex scroll container (REQUIRED — never omit) DataTable — required — the data table Pagination — required if record count exceeds pageSize (custom, no Spark component) ``` --- ## Section 1 — Page header Always use `PageHeader` from `@spark-web/page-header`. Never replace with a manual `Stack + Heading`. ```tsx <PageHeader label={pageTitle} /> ``` See `packages/page-header/CLAUDE.md` for action types (button, link, meatball), statusBadge, and controls props. Rules: - `PageHeader` always renders an H1 — do not pass a different heading level - Page-level actions (e.g. "Add new") belong in the `action` prop, not in the content area - This section always renders — never omit it --- ## Section 1b — Page-level feedback (conditional) When a page-level action (e.g. CSV export, bulk mutation) can produce success or error feedback, render an `<Alert>` between `<PageHeader>` and the content area Stack. Only rendered when feedback state exists. ```tsx import { Alert } from '@spark-web/alert'; { actionStatus && ( <Alert tone={actionStatus.isSuccessful ? 'positive' : 'critical'}> <Text>{actionStatus.message}</Text> </Alert> ); } ``` Rules: - Never render Alert inside the neutral-background content Stack - Never render Alert above PageHeader - Feedback state is local component state, reset on filter change or page navigation - If the page has no page-level actions, omit this section entirely --- ## Section 2 — Outer wrapper The outermost container fills the full viewport height. ```tsx <Stack height="full" className={css({ minHeight: '100vh' })}> ``` Documented exception: - `minHeight: '100vh'`no Spark token equivalent; ensures page fills viewport on short content --- ## Section 3 — Content area Sits inside the outer wrapper. Owns all page padding and gap between sections. ```tsx <Stack padding="large" gap="large" className={css({ backgroundColor: theme.color.background.neutral, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', })} > ``` Token mappings: - `padding="large"` — all four sides, Spark token - `gap="large"` — between all sections, Spark token - `backgroundColor``theme.color.background.neutral` via `useTheme()` Documented exceptions: - `flex: 1` — makes content area fill remaining height, no Spark flex prop - `display: 'flex'` + `flexDirection: 'column'` — required to activate flex: 1 - `overflow: 'hidden'` — scroll containment, no Spark overflow prop on Stack --- ## Section 4 — Filter container Renders filter dropdowns and a search input in a horizontal row. ```tsx const FieldProps = { labelVisibility: 'hidden' as const }; <Columns gap="large" collapseBelow="desktop"> <TextInputField control={control} name="search" label="Search" placeholder="Search by..." FieldProps={FieldProps} > <InputAdornment placement="start"> <SearchIcon size="xxsmall" tone="muted" /> </InputAdornment> </TextInputField> <MultiSelectField control={control} name="fieldName" label="Label" options={options} placeholder="Filter by..." fieldProps={FieldProps} /> </Columns>; ``` Rules: - Use `Columns` from @spark-web/columns with `gap="large"` - `collapseBelow="desktop"` — stacks vertically on mobile and tablet - **Search input always appears first (leftmost)** in the filter row - Filter dropdowns follow the search input, ordered by specificity (broadest filter first, most specific last) - Multi-select filter dropdowns use `MultiSelectField` from `@brighte/ui-components` (portal-hub only — `@spark-web/multi-select` is not available in portal-hub) - Single-select filter dropdowns use `SelectField` from `@brighte/ui-components` - Always pass `fieldProps={{ labelVisibility: 'hidden' as const }}` on both `MultiSelectField` and `SelectField` — define it as a constant outside the component to avoid re-renders - If no filtering or searching is needed, omit this section entirely --- ## Section 5 — Table scroll wrapper Wraps the DataTable to enable vertical scrolling without the page scrolling. ```tsx <Box display="flex" flexDirection="column"> <div className={css({ position: 'relative', flex: 1, minHeight: 0, overflowY: 'auto', })} > <DataTable ... /> </div> </Box> ``` Documented exceptions — all required for flex scroll behaviour: - `position: 'relative'` — scroll container positioning - `flex: 1` — fills available height - `minHeight: 0` — classic flex scroll fix, prevents overflow - `overflowY: 'auto'` — enables vertical scrolling --- ## Section 6 — Table Always uses `@spark-web/data-table`. See `packages/data-table/CLAUDE.md` for column definition API, loading/empty states, sorting, and expansion. Apply canonical list-page header styling via `headerClassName`. Use the named import `import { css as reactCss } from '@emotion/react'``headerClassName` accepts `SerializedStyles`, not a string class from `@emotion/css`. ```tsx <DataTable className={reactCss({ width: '100%' })} headerClassName={reactCss({ boxShadow: `inset 0 -2px 0 0 ${theme.color.background.primaryDark}`, th: { color: theme.color.background.primaryDark, textTransform: 'capitalize', svg: { stroke: theme.color.background.primaryDark }, }, })} items={rows} columns={columns} isLoading={isLoading} emptyState={ <Text tone="muted" size="small"> No records found. Try adjusting your filters. </Text> } /> ``` Column rules: - Default column width: equal flex distribution (`size` unset) - Actions column: always last, `size: 80`, empty string `header` - Status column: always `<Badge>` (dot + label) — never `<StatusBadge>`, never plain text - Actions column: always uses `<MeatballMenu>` when 2 or more row-level actions exist Row interaction rules — see `packages/design-system/patterns/internal-admin/CLAUDE.md`: - Pass `onRowClick` and `enableClickableRow` when the record has a detail view - Hover state is applied automatically when `enableClickableRow` is true Status tone mapping — authoritative rules are in `packages/design-system/patterns/internal-admin/CLAUDE.md`. Do not duplicate them here. --- ## Section 7 — Pagination Render `TablePagination` outside and below DataTable — never nested inside it. - **Only rendered when `total > pageSize`** — hide it when all records fit on one page - Default `pageSize` for full list pages: **20** - Use the real total count from a dedicated count query — never derive total from the length of the current page's result set - No additional wrapper needed — content area `gap="large"` handles spacing --- ## Structural skeleton Use this skeleton as the starting point for new builds and uplifts. Do not use existing page implementations as a structural reference. ```tsx <Stack height="full" className={css({ minHeight: '100vh' })}> <PageHeader label={pageTitle} /> <Stack padding="large" gap="large" className={css({ backgroundColor: theme.color.background.neutral, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', })} > <Columns gap="large" collapseBelow="desktop"> {/* search first, then filters */} </Columns> <Box display="flex" flexDirection="column"> <div className={css({ position: 'relative', flex: 1, minHeight: 0, overflowY: 'auto', })} > <DataTable items={items} columns={columns} isLoading={isLoading} emptyState={emptyState} /> </div> </Box> {total > PAGE_SIZE && ( <TablePagination total={total} pageSize={PAGE_SIZE} dataShowing={rows.length} onChange={setPage} current={page} /> )} </Stack> </Stack> ``` --- ## Documented exceptions summary These raw CSS values are required and have no Spark token equivalent. Use them exactly as written. Do not substitute alternatives. | Value | Property | Reason | | --------------------------------------------- | ---------------------------- | --------------------------------------- | | `minHeight: '100vh'` | Outer Stack | No Spark minHeight prop | | `flex: 1` | Content area, scroll wrapper | No Spark flex prop | | `display: 'flex'` + `flexDirection: 'column'` | Content area | Required for flex: 1 | | `overflow: 'hidden'` | Content area | No Spark overflow on Stack | | `position: 'relative'` | Scroll div | No Spark position prop | | `minHeight: 0` | Scroll div | Flex scroll fix | | `overflowY: 'auto'` | Scroll div | No Spark overflow prop | | `backgroundColor: background.neutral` | Content area | Accessed via useTheme(), not a Box prop | --- ## Do NOTs - NEVER use `<Container>` as the outer page wrapper — always use `<Stack height="full">`. Container constrains width and removes full-height layout and scroll containment behaviour - NEVER place DataTable directly inside the content area Stack — always use the `<Box display="flex" flexDirection="column"> <div className={...}>` scroll wrapper from Section 5. Omitting it breaks page scroll containment - NEVER put pagination inside DataTable - NEVER render pagination when all records fit on one page — only show it when total > pageSize - NEVER derive the pagination total from `items.length` — always use a dedicated count query - NEVER place filter dropdowns before the search input — search always comes first (leftmost) - NEVER use plain text for status values — always use Badge with `children` - NEVER import `MeatBall` from `@spark-web/meatball` — the component is `MeatballMenu` from `@spark-web/meatball-menu` - NEVER use `tone="pending"` on Badge — it does not exist; use `tone="info"` for pending/awaiting states - NEVER omit the page header — every list page has an H1 title via PageHeader - NEVER add padding to the outer Stack wrapper - NEVER omit the flex scroll wrapper around DataTable - NEVER omit the empty state and loading state - NEVER place filter controls inside DataTable - NEVER hardcode column widths except for the actions column (80px) - NEVER substitute the documented exception values with alternatives - NEVER replace PageHeader with a manual Stack + Heading + Text breadcrumb - NEVER apply detail page spacing (paddingX="xlarge" paddingY="xxlarge") to a list page — those values belong in detail-page.md only - NEVER render an external loading spinner/text outside DataTable — always use the `isLoading` prop on DataTable to show loading state - NEVER use `Stack + Text weight="semibold"` to label a MultiSelectField or SelectField filter — always use `fieldProps={{ labelVisibility: 'hidden' as const }}` - NEVER omit `fieldProps={{ labelVisibility: 'hidden' as const }}` on SelectField filter dropdowns — the same hidden label rule that applies to MultiSelectField applies to SelectField equally - NEVER use `@spark-web/multi-select` in portal-hub — it is not installed; use `MultiSelectField` from `@brighte/ui-components` - NEVER use `className` with a string from `@emotion/css` on DataTable — use `SerializedStyles` from `@emotion/react`'s `css` tagged template