@spark-web/design-system 5.0.100 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,413 @@
1
+ # Internal admin — list page pattern
2
+
3
+ ## Before using this pattern
4
+
5
+ Read node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md
6
+ fully before implementing this pattern. The interaction rules, badge tone
7
+ mapping, row clickability rules, and overflow menu rules defined there all apply
8
+ to this pattern.
9
+
10
+ ## What this pattern is
11
+
12
+ A full page layout for displaying a searchable, filterable, paginated list of
13
+ records in an internal admin interface. This is the most common page type in
14
+ admin surfaces.
15
+
16
+ ## When to use this pattern
17
+
18
+ Use this pattern when the PRD describes any of the following:
19
+
20
+ - A list of records that can be searched, filtered, or sorted
21
+ - A management interface where records can be viewed or acted upon
22
+ - The words "list", "manage", "view all", "records", or "results"
23
+
24
+ ---
25
+
26
+ ## Component docs to read
27
+
28
+ Read these before implementing — they own the component-level rules:
29
+
30
+ - `packages/page-header/CLAUDE.md` — PageHeader API, action type rules
31
+ - `packages/data-table/CLAUDE.md` — DataTable API, column defs, loading/empty
32
+ states, headerClassName tokens
33
+ - `packages/badge/CLAUDE.md` — status tone mapping
34
+ - `packages/meatball-menu/CLAUDE.md` — MeatballMenu API
35
+ - `packages/stack/CLAUDE.md` — vertical stacking and gap
36
+ - `packages/box/CLAUDE.md` — flex layout utilities
37
+ - `packages/columns/CLAUDE.md` — responsive multi-column layout
38
+ - `packages/field/CLAUDE.md` — Field API, labelVisibility
39
+ - `packages/text/CLAUDE.md` — Text API
40
+ - `packages/text-input/CLAUDE.md` — TextInput API
41
+
42
+ ---
43
+
44
+ ## Page structure
45
+
46
+ ```
47
+ Page
48
+ Outer wrapper Stack — full height, no padding
49
+ PageHeader — label={pageTitle}, optional statusBadge/action/controls
50
+ Alert (conditional) — page-level feedback only, omit if no page-level actions
51
+ Content area Stack — padding="large" gap="large", neutral background
52
+ Filter container — required if filtering or searching exists
53
+ Table scroll wrapper — flex scroll container (REQUIRED — never omit)
54
+ DataTable — required — the data table
55
+ Pagination — required if record count exceeds pageSize (custom, no Spark component)
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Section 1 — Page header
61
+
62
+ Always use `PageHeader` from `@spark-web/page-header`. Never replace with a
63
+ manual `Stack + Heading`.
64
+
65
+ ```tsx
66
+ <PageHeader label={pageTitle} />
67
+ ```
68
+
69
+ See `packages/page-header/CLAUDE.md` for action types (button, link, meatball),
70
+ statusBadge, and controls props.
71
+
72
+ Rules:
73
+
74
+ - `PageHeader` always renders an H1 — do not pass a different heading level
75
+ - Page-level actions (e.g. "Add new") belong in the `action` prop, not in the
76
+ content area
77
+ - This section always renders — never omit it
78
+
79
+ ---
80
+
81
+ ## Section 1b — Page-level feedback (conditional)
82
+
83
+ When a page-level action (e.g. CSV export, bulk mutation) can produce success or
84
+ error feedback, render an `<Alert>` between `<PageHeader>` and the content area
85
+ Stack. Only rendered when feedback state exists.
86
+
87
+ ```tsx
88
+ import { Alert } from '@spark-web/alert';
89
+
90
+ {
91
+ actionStatus && (
92
+ <Alert tone={actionStatus.isSuccessful ? 'positive' : 'critical'}>
93
+ <Text>{actionStatus.message}</Text>
94
+ </Alert>
95
+ );
96
+ }
97
+ ```
98
+
99
+ Rules:
100
+
101
+ - Never render Alert inside the neutral-background content Stack
102
+ - Never render Alert above PageHeader
103
+ - Feedback state is local component state, reset on filter change or page
104
+ navigation
105
+ - If the page has no page-level actions, omit this section entirely
106
+
107
+ ---
108
+
109
+ ## Section 2 — Outer wrapper
110
+
111
+ The outermost container fills the full viewport height.
112
+
113
+ ```tsx
114
+ <Stack height="full" className={css({ minHeight: '100vh' })}>
115
+ ```
116
+
117
+ Documented exception:
118
+
119
+ - `minHeight: '100vh'` — no Spark token equivalent; ensures page fills viewport
120
+ on short content
121
+
122
+ ---
123
+
124
+ ## Section 3 — Content area
125
+
126
+ Sits inside the outer wrapper. Owns all page padding and gap between sections.
127
+
128
+ ```tsx
129
+ <Stack
130
+ padding="large"
131
+ gap="large"
132
+ className={css({
133
+ backgroundColor: theme.color.background.neutral,
134
+ flex: 1,
135
+ display: 'flex',
136
+ flexDirection: 'column',
137
+ overflow: 'hidden',
138
+ })}
139
+ >
140
+ ```
141
+
142
+ Token mappings:
143
+
144
+ - `padding="large"` — all four sides, Spark token
145
+ - `gap="large"` — between all sections, Spark token
146
+ - `backgroundColor` — `theme.color.background.neutral` via `useTheme()`
147
+
148
+ Documented exceptions:
149
+
150
+ - `flex: 1` — makes content area fill remaining height, no Spark flex prop
151
+ - `display: 'flex'` + `flexDirection: 'column'` — required to activate flex: 1
152
+ - `overflow: 'hidden'` — scroll containment, no Spark overflow prop on Stack
153
+
154
+ ---
155
+
156
+ ## Section 4 — Filter container
157
+
158
+ Renders filter dropdowns and a search input in a horizontal row.
159
+
160
+ ```tsx
161
+ const FieldProps = { labelVisibility: 'hidden' as const };
162
+
163
+ <Columns gap="large" collapseBelow="desktop">
164
+ <TextInputField
165
+ control={control}
166
+ name="search"
167
+ label="Search"
168
+ placeholder="Search by..."
169
+ FieldProps={FieldProps}
170
+ >
171
+ <InputAdornment placement="start">
172
+ <SearchIcon size="xxsmall" tone="muted" />
173
+ </InputAdornment>
174
+ </TextInputField>
175
+ <MultiSelectField
176
+ control={control}
177
+ name="fieldName"
178
+ label="Label"
179
+ options={options}
180
+ placeholder="Filter by..."
181
+ fieldProps={FieldProps}
182
+ />
183
+ </Columns>;
184
+ ```
185
+
186
+ Rules:
187
+
188
+ - Use `Columns` from @spark-web/columns with `gap="large"`
189
+ - `collapseBelow="desktop"` — stacks vertically on mobile and tablet
190
+ - **Search input always appears first (leftmost)** in the filter row
191
+ - Filter dropdowns follow the search input, ordered by specificity (broadest
192
+ filter first, most specific last)
193
+ - Multi-select filter dropdowns use `MultiSelectField` from
194
+ `@brighte/ui-components` (portal-hub only — `@spark-web/multi-select` is not
195
+ available in portal-hub)
196
+ - Single-select filter dropdowns use `SelectField` from `@brighte/ui-components`
197
+ - Always pass `fieldProps={{ labelVisibility: 'hidden' as const }}` on both
198
+ `MultiSelectField` and `SelectField` — define it as a constant outside the
199
+ component to avoid re-renders
200
+ - If no filtering or searching is needed, omit this section entirely
201
+
202
+ ---
203
+
204
+ ## Section 5 — Table scroll wrapper
205
+
206
+ Wraps the DataTable to enable vertical scrolling without the page scrolling.
207
+
208
+ ```tsx
209
+ <Box display="flex" flexDirection="column">
210
+ <div
211
+ className={css({
212
+ position: 'relative',
213
+ flex: 1,
214
+ minHeight: 0,
215
+ overflowY: 'auto',
216
+ })}
217
+ >
218
+ <DataTable ... />
219
+ </div>
220
+ </Box>
221
+ ```
222
+
223
+ Documented exceptions — all required for flex scroll behaviour:
224
+
225
+ - `position: 'relative'` — scroll container positioning
226
+ - `flex: 1` — fills available height
227
+ - `minHeight: 0` — classic flex scroll fix, prevents overflow
228
+ - `overflowY: 'auto'` — enables vertical scrolling
229
+
230
+ ---
231
+
232
+ ## Section 6 — Table
233
+
234
+ Always uses `@spark-web/data-table`. See `packages/data-table/CLAUDE.md` for
235
+ column definition API, loading/empty states, sorting, and expansion.
236
+
237
+ Apply canonical list-page header styling via `headerClassName`. Use the named
238
+ import `import { css as reactCss } from '@emotion/react'` — `headerClassName`
239
+ accepts `SerializedStyles`, not a string class from `@emotion/css`.
240
+
241
+ ```tsx
242
+ <DataTable
243
+ className={reactCss({ width: '100%' })}
244
+ headerClassName={reactCss({
245
+ boxShadow: `inset 0 -2px 0 0 ${theme.color.background.primaryDark}`,
246
+ th: {
247
+ color: theme.color.background.primaryDark,
248
+ textTransform: 'capitalize',
249
+ svg: { stroke: theme.color.background.primaryDark },
250
+ },
251
+ })}
252
+ items={rows}
253
+ columns={columns}
254
+ isLoading={isLoading}
255
+ emptyState={
256
+ <Text tone="muted" size="small">
257
+ No records found. Try adjusting your filters.
258
+ </Text>
259
+ }
260
+ />
261
+ ```
262
+
263
+ Column rules:
264
+
265
+ - Default column width: equal flex distribution (`size` unset)
266
+ - Actions column: always last, `size: 80`, empty string `header`
267
+ - Status column: always `<Badge>` (dot + label) — never `<StatusBadge>`, never
268
+ plain text
269
+ - Actions column: always uses `<MeatballMenu>` when 2 or more row-level actions
270
+ exist
271
+
272
+ Row interaction rules — see
273
+ `packages/design-system/patterns/internal-admin/CLAUDE.md`:
274
+
275
+ - Pass `onRowClick` and `enableClickableRow` when the record has a detail view
276
+ - Hover state is applied automatically when `enableClickableRow` is true
277
+
278
+ Status tone mapping — authoritative rules are in
279
+ `packages/design-system/patterns/internal-admin/CLAUDE.md`. Do not duplicate
280
+ them here.
281
+
282
+ ---
283
+
284
+ ## Section 7 — Pagination
285
+
286
+ Render `TablePagination` outside and below DataTable — never nested inside it.
287
+
288
+ - **Only rendered when `total > pageSize`** — hide it when all records fit on
289
+ one page
290
+ - Default `pageSize` for full list pages: **20**
291
+ - Use the real total count from a dedicated count query — never derive total
292
+ from the length of the current page's result set
293
+ - No additional wrapper needed — content area `gap="large"` handles spacing
294
+
295
+ ---
296
+
297
+ ## Structural skeleton
298
+
299
+ Use this skeleton as the starting point for new builds and uplifts. Do not use
300
+ existing page implementations as a structural reference.
301
+
302
+ ```tsx
303
+ <Stack height="full" className={css({ minHeight: '100vh' })}>
304
+ <PageHeader label={pageTitle} />
305
+
306
+ <Stack
307
+ padding="large"
308
+ gap="large"
309
+ className={css({
310
+ backgroundColor: theme.color.background.neutral,
311
+ flex: 1,
312
+ display: 'flex',
313
+ flexDirection: 'column',
314
+ overflow: 'hidden',
315
+ })}
316
+ >
317
+ <Columns gap="large" collapseBelow="desktop">
318
+ {/* search first, then filters */}
319
+ </Columns>
320
+
321
+ <Box display="flex" flexDirection="column">
322
+ <div
323
+ className={css({
324
+ position: 'relative',
325
+ flex: 1,
326
+ minHeight: 0,
327
+ overflowY: 'auto',
328
+ })}
329
+ >
330
+ <DataTable
331
+ items={items}
332
+ columns={columns}
333
+ isLoading={isLoading}
334
+ emptyState={emptyState}
335
+ />
336
+ </div>
337
+ </Box>
338
+
339
+ {total > PAGE_SIZE && (
340
+ <TablePagination
341
+ total={total}
342
+ pageSize={PAGE_SIZE}
343
+ dataShowing={rows.length}
344
+ onChange={setPage}
345
+ current={page}
346
+ />
347
+ )}
348
+ </Stack>
349
+ </Stack>
350
+ ```
351
+
352
+ ---
353
+
354
+ ## Documented exceptions summary
355
+
356
+ These raw CSS values are required and have no Spark token equivalent. Use them
357
+ exactly as written. Do not substitute alternatives.
358
+
359
+ | Value | Property | Reason |
360
+ | --------------------------------------------- | ---------------------------- | --------------------------------------- |
361
+ | `minHeight: '100vh'` | Outer Stack | No Spark minHeight prop |
362
+ | `flex: 1` | Content area, scroll wrapper | No Spark flex prop |
363
+ | `display: 'flex'` + `flexDirection: 'column'` | Content area | Required for flex: 1 |
364
+ | `overflow: 'hidden'` | Content area | No Spark overflow on Stack |
365
+ | `position: 'relative'` | Scroll div | No Spark position prop |
366
+ | `minHeight: 0` | Scroll div | Flex scroll fix |
367
+ | `overflowY: 'auto'` | Scroll div | No Spark overflow prop |
368
+ | `backgroundColor: background.neutral` | Content area | Accessed via useTheme(), not a Box prop |
369
+
370
+ ---
371
+
372
+ ## Do NOTs
373
+
374
+ - NEVER use `<Container>` as the outer page wrapper — always use
375
+ `<Stack height="full">`. Container constrains width and removes full-height
376
+ layout and scroll containment behaviour
377
+ - NEVER place DataTable directly inside the content area Stack — always use the
378
+ `<Box display="flex" flexDirection="column"> <div className={...}>` scroll
379
+ wrapper from Section 5. Omitting it breaks page scroll containment
380
+ - NEVER put pagination inside DataTable
381
+ - NEVER render pagination when all records fit on one page — only show it when
382
+ total > pageSize
383
+ - NEVER derive the pagination total from `items.length` — always use a dedicated
384
+ count query
385
+ - NEVER place filter dropdowns before the search input — search always comes
386
+ first (leftmost)
387
+ - NEVER use plain text for status values — always use Badge with `children`
388
+ - NEVER import `MeatBall` from `@spark-web/meatball` — the component is
389
+ `MeatballMenu` from `@spark-web/meatball-menu`
390
+ - NEVER use `tone="pending"` on Badge — it does not exist; use `tone="info"` for
391
+ pending/awaiting states
392
+ - NEVER omit the page header — every list page has an H1 title via PageHeader
393
+ - NEVER add padding to the outer Stack wrapper
394
+ - NEVER omit the flex scroll wrapper around DataTable
395
+ - NEVER omit the empty state and loading state
396
+ - NEVER place filter controls inside DataTable
397
+ - NEVER hardcode column widths except for the actions column (80px)
398
+ - NEVER substitute the documented exception values with alternatives
399
+ - NEVER replace PageHeader with a manual Stack + Heading + Text breadcrumb
400
+ - NEVER apply detail page spacing (paddingX="xlarge" paddingY="xxlarge") to a
401
+ list page — those values belong in detail-page.md only
402
+ - NEVER render an external loading spinner/text outside DataTable — always use
403
+ the `isLoading` prop on DataTable to show loading state
404
+ - NEVER use `Stack + Text weight="semibold"` to label a MultiSelectField or
405
+ SelectField filter — always use
406
+ `fieldProps={{ labelVisibility: 'hidden' as const }}`
407
+ - NEVER omit `fieldProps={{ labelVisibility: 'hidden' as const }}` on
408
+ SelectField filter dropdowns — the same hidden label rule that applies to
409
+ MultiSelectField applies to SelectField equally
410
+ - NEVER use `@spark-web/multi-select` in portal-hub — it is not installed; use
411
+ `MultiSelectField` from `@brighte/ui-components`
412
+ - NEVER use `className` with a string from `@emotion/css` on DataTable — use
413
+ `SerializedStyles` from `@emotion/react`'s `css` tagged template