@spark-web/design-system 5.0.100 → 5.1.0

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,558 @@
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
+ ## Required imports
27
+
28
+ ```tsx
29
+ import { Stack } from '@spark-web/stack';
30
+ import { Box } from '@spark-web/box';
31
+ import { Columns } from '@spark-web/columns';
32
+ import { Field } from '@spark-web/field';
33
+ import { PageHeader } from '@spark-web/page-header';
34
+ import { Text } from '@spark-web/text';
35
+ import { TextInput } from '@spark-web/text-input';
36
+ import { createColumnHelper, DataTable } from '@spark-web/data-table';
37
+ import { Badge } from '@spark-web/badge';
38
+ import { MeatballMenu } from '@spark-web/meatball-menu';
39
+ import {
40
+ MultiSelectField,
41
+ SelectField,
42
+ TextInputField,
43
+ } from '@brighte/ui-components'; // portal-hub only — @spark-web/multi-select not available
44
+ import { css } from '@emotion/css';
45
+ import { css as reactCss } from '@emotion/react';
46
+ import { useTheme } from '@spark-web/theme';
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Component docs to read
52
+
53
+ Read these files at Step 4 — no others:
54
+
55
+ - packages/page-header/CLAUDE.md
56
+ - packages/data-table/CLAUDE.md
57
+ - packages/badge/CLAUDE.md
58
+ - packages/meatball-menu/CLAUDE.md
59
+ - packages/stack/CLAUDE.md
60
+ - packages/box/CLAUDE.md
61
+ - packages/columns/CLAUDE.md
62
+ - packages/field/CLAUDE.md
63
+ - packages/text/CLAUDE.md
64
+ - packages/text-input/CLAUDE.md
65
+
66
+ ---
67
+
68
+ ## Page structure
69
+
70
+ The page is composed in this exact order, top to bottom. Do not change the order
71
+ or omit any required section.
72
+
73
+ ```
74
+ Page
75
+ Outer wrapper Stack — full height, no padding
76
+ PageHeader — label={pageTitle}, optional statusBadge/action/controls
77
+ Alert (conditional) — page-level feedback only, omit if no page-level actions
78
+ Content area Stack — padding="large" gap="large", neutral background
79
+ Filter container — required if filtering or searching exists
80
+ Table scroll wrapper — flex scroll container (REQUIRED — never omit)
81
+ DataTable — required — the data table
82
+ Pagination — required if record count exceeds pageSize (custom, no Spark component)
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Section 1 — Page header
88
+
89
+ Renders the page title as an H1 using `PageHeader` from
90
+ `@spark-web/page-header`.
91
+
92
+ ```tsx
93
+ <PageHeader label={pageTitle} />
94
+ ```
95
+
96
+ Optional variants — see `@spark-web/page-header` CLAUDE.md for full prop docs:
97
+
98
+ ```tsx
99
+ {/* With status badge */}
100
+ <PageHeader label={pageTitle} statusBadge={{ children: 'Active', tone: 'positive' }} />
101
+
102
+ {/* With a single action button */}
103
+ <PageHeader label={pageTitle} action={{ type: 'button', label: 'Add', onClick: handleAdd }} />
104
+
105
+ {/* With overflow menu (2+ actions) */}
106
+ <PageHeader label={pageTitle} action={{ type: 'meatball', items: [...] }} />
107
+ ```
108
+
109
+ Rules:
110
+
111
+ - Always use `PageHeader` — never replace with a manual `Stack + Heading`
112
+ - `PageHeader` always renders an H1 — do not pass a different heading level
113
+ - Page-level actions (e.g. "Add new") belong in the `action` prop, not in the
114
+ content area
115
+ - This section always renders — never omit it
116
+
117
+ ---
118
+
119
+ ## Section 1b — Page-level feedback (conditional)
120
+
121
+ When a page-level action (e.g. CSV export, bulk mutation) can produce success or
122
+ error feedback, render an `<Alert>` between `<PageHeader>` and the content area
123
+ Stack. Only rendered when feedback state exists.
124
+
125
+ ```tsx
126
+ import { Alert } from '@spark-web/alert';
127
+
128
+ {
129
+ actionStatus && (
130
+ <Alert tone={actionStatus.isSuccessful ? 'positive' : 'critical'}>
131
+ <Text>{actionStatus.message}</Text>
132
+ </Alert>
133
+ );
134
+ }
135
+ ```
136
+
137
+ Rules:
138
+
139
+ - Never render Alert inside the neutral-background content Stack
140
+ - Never render Alert above PageHeader
141
+ - Feedback state is local component state, reset on filter change or page
142
+ navigation
143
+ - If the page has no page-level actions, omit this section entirely
144
+
145
+ ---
146
+
147
+ ## Section 2 — Outer wrapper
148
+
149
+ The outermost container fills the full viewport height.
150
+
151
+ ```tsx
152
+ <Stack
153
+ height="full"
154
+ className={css({ minHeight: '100vh' })}
155
+ >
156
+ ```
157
+
158
+ Documented exceptions — no Spark token equivalent:
159
+
160
+ - `minHeight: '100vh'` — ensures page fills viewport on short content
161
+
162
+ ---
163
+
164
+ ## Section 3 — Content area
165
+
166
+ Sits inside the outer wrapper. Owns all page padding and gap between sections.
167
+
168
+ ```tsx
169
+ <Stack
170
+ padding="large"
171
+ gap="large"
172
+ className={css({
173
+ backgroundColor: theme.color.background.neutral,
174
+ flex: 1,
175
+ display: 'flex',
176
+ flexDirection: 'column',
177
+ overflow: 'hidden',
178
+ })}
179
+ >
180
+ ```
181
+
182
+ Token mappings:
183
+
184
+ - `padding="large"` — all four sides, Spark token
185
+ - `gap="large"` — between all sections, Spark token
186
+ - `backgroundColor` — `theme.color.background.neutral` via `useTheme()`
187
+
188
+ Documented exceptions:
189
+
190
+ - `flex: 1` — makes content area fill remaining height, no Spark flex prop
191
+ - `display: 'flex'` + `flexDirection: 'column'` — required to activate flex: 1
192
+ - `overflow: 'hidden'` — scroll containment, no Spark overflow prop on Stack
193
+
194
+ ---
195
+
196
+ ## Section 4 — Filter container
197
+
198
+ Renders filter dropdowns and a search input in a horizontal row.
199
+
200
+ ```tsx
201
+ const FieldProps = { labelVisibility: 'hidden' as const };
202
+
203
+ <Columns gap="large" collapseBelow="desktop">
204
+ <MultiSelectField
205
+ control={control}
206
+ name="fieldName"
207
+ label="Label"
208
+ options={options}
209
+ placeholder="Filter by..."
210
+ fieldProps={FieldProps}
211
+ />
212
+ <Field label="Search">
213
+ <TextInput
214
+ type="search"
215
+ value={search}
216
+ onChange={e => setSearch(e.target.value)}
217
+ placeholder="Search by..."
218
+ />
219
+ </Field>
220
+ </Columns>;
221
+ ```
222
+
223
+ Rules:
224
+
225
+ - Use `Columns` from @spark-web/columns with `gap="large"`
226
+ - `collapseBelow="desktop"` — stacks vertically on mobile and tablet
227
+ - Multi-select filter dropdowns use `MultiSelectField` from
228
+ `@brighte/ui-components` (portal-hub only — `@spark-web/multi-select` is not
229
+ available in portal-hub).
230
+ - Single-select filter dropdowns use `SelectField` from
231
+ `@brighte/ui-components`.
232
+ - Always pass `fieldProps={{ labelVisibility: 'hidden' as const }}` on both
233
+ `MultiSelectField` and `SelectField` — define it as a constant outside the
234
+ component to avoid re-renders. Never wrap in a `Stack` + `Text` label — the
235
+ label is hidden, not replaced. This rule applies to both components equally.
236
+
237
+ ```tsx
238
+ const FieldProps = { labelVisibility: 'hidden' as const };
239
+
240
+ <SelectField
241
+ control={control}
242
+ name="vendor"
243
+ label="Vendor"
244
+ options={vendorOptions}
245
+ placeholder="Filter by vendor..."
246
+ fieldProps={FieldProps}
247
+ />;
248
+ ```
249
+
250
+ - Search input uses `TextInputField` from `@brighte/ui-components` with
251
+ `FieldProps` (same hidden label constant), with `SearchIcon` adornment on
252
+ start and a clear button (`XIcon`) on end
253
+ - Filter controls always appear left of the search input
254
+ - Maximum 2 filter dropdowns before the search input
255
+ - Search input always appears on the far right
256
+ - If no filtering or searching is needed, omit this section entirely
257
+
258
+ ---
259
+
260
+ ## Section 5 — Table scroll wrapper
261
+
262
+ Wraps the Table to enable vertical scrolling without the page scrolling.
263
+
264
+ ```tsx
265
+ <Box display="flex" flexDirection="column">
266
+ <div
267
+ className={css({
268
+ position: 'relative',
269
+ flex: 1,
270
+ minHeight: 0,
271
+ overflowY: 'auto',
272
+ })}
273
+ >
274
+ <Table>...</Table>
275
+ </div>
276
+ </Box>
277
+ ```
278
+
279
+ Documented exceptions — all required for flex scroll behaviour:
280
+
281
+ - `position: 'relative'` — scroll container positioning
282
+ - `flex: 1` — fills available height
283
+ - `minHeight: 0` — classic flex scroll fix, prevents overflow
284
+ - `overflowY: 'auto'` — enables vertical scrolling
285
+
286
+ ---
287
+
288
+ ## Section 6 — Table
289
+
290
+ The core data display. Always uses `@spark-web/data-table`.
291
+
292
+ Define columns outside the component using `createColumnHelper`. Pass `items`
293
+ and `columns` to `<DataTable>`.
294
+
295
+ Always apply `className` and `headerClassName` for canonical list-page styling:
296
+
297
+ ```tsx
298
+ <DataTable
299
+ className={reactCss({ width: '100%' })}
300
+ headerClassName={reactCss({
301
+ boxShadow: `inset 0 -2px 0 0 ${theme.color.background.primaryDark}`,
302
+ th: {
303
+ color: theme.color.background.primaryDark,
304
+ textTransform: 'capitalize',
305
+ svg: { stroke: theme.color.background.primaryDark },
306
+ },
307
+ })}
308
+ ...
309
+ />
310
+ ```
311
+
312
+ Note: `className` and `headerClassName` accept a `SerializedStyles` object from
313
+ `@emotion/react`'s `css` — not a string class from `@emotion/css`. Use the named
314
+ import `import { css as reactCss } from '@emotion/react'` to distinguish them.
315
+
316
+ Column rules:
317
+
318
+ - Default column width: equal flex distribution (`size` unset)
319
+ - Actions column: always last, `size: 80`, empty string `header`
320
+ - Status column: always use `<Badge>` (dot + label) — never `<StatusBadge>`,
321
+ never plain text
322
+ - Actions column: always uses `<MeatballMenu>` when 2 or more row-level actions
323
+ exist
324
+
325
+ Row interaction rules — see
326
+ node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md:
327
+
328
+ - Pass `onRowClick` and `enableClickableRow` when the record has a detail view
329
+ - Hover state is applied automatically when `enableClickableRow` is true
330
+
331
+ Status tone mapping — authoritative rules are in
332
+ node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md. Do not
333
+ duplicate them here.
334
+
335
+ Table states to always implement:
336
+
337
+ - Default: `items={rows}` with data
338
+ - Loading: `isLoading` prop — renders spinner overlay, replaces data rows
339
+ - Empty: `emptyState={<Text tone="muted" size="small">…</Text>}` — shown when
340
+ `items` is an empty array
341
+
342
+ ---
343
+
344
+ ## Section 7 — Pagination
345
+
346
+ No dedicated Spark pagination component exists. Implement using Spark primitives
347
+ or flag as a component gap.
348
+
349
+ - Pagination is rendered outside and below DataTable — never nested inside it
350
+ - Pagination is always rendered regardless of loading or empty state — never
351
+ conditionally hidden.
352
+
353
+ Rules:
354
+
355
+ - Always implement when the data source is an API or database
356
+ - Default pageSize: 20
357
+ - No additional wrapper needed — content area `gap="large"` handles spacing
358
+
359
+ ---
360
+
361
+ ## Complete assembly
362
+
363
+ ```tsx
364
+ const col = createColumnHelper<Row>();
365
+
366
+ const columns = [
367
+ col.accessor('column1', {
368
+ header: 'Column 1',
369
+ cell: info => <Text size="small">{String(info.getValue())}</Text>,
370
+ }),
371
+ col.accessor('status', {
372
+ header: 'Status',
373
+ cell: info => (
374
+ <Badge tone={deriveTone(info.getValue())}>{info.getValue()}</Badge>
375
+ ),
376
+ }),
377
+ col.display({
378
+ id: 'actions',
379
+ header: '',
380
+ size: 80,
381
+ cell: ({ row }) => <MeatballMenu items={rowActions(row.original)} />,
382
+ }),
383
+ ];
384
+
385
+ // ---
386
+
387
+ const theme = useTheme()
388
+
389
+ <Stack height="full" className={css({ minHeight: '100vh' })}>
390
+ {/* Section 1: Page header */}
391
+ <PageHeader label={pageTitle} />
392
+
393
+ {/* Sections 3–7: Content area — neutral background */}
394
+ <Stack
395
+ padding="large"
396
+ gap="large"
397
+ className={css({
398
+ backgroundColor: theme.color.background.neutral,
399
+ flex: 1,
400
+ display: 'flex',
401
+ flexDirection: 'column',
402
+ overflow: 'hidden',
403
+ })}
404
+ >
405
+ {/* Section 4: Filters */}
406
+ <Columns gap="large" collapseBelow="desktop">
407
+ <MultiSelectField
408
+ control={control}
409
+ name="status"
410
+ label="Status"
411
+ options={statusOptions}
412
+ placeholder="Filter by status"
413
+ fieldProps={FieldProps}
414
+ />
415
+ <Field label="Search">
416
+ <TextInput type="search" value={search} onChange={e => setSearch(e.target.value)} placeholder="Search by..." />
417
+ </Field>
418
+ </Columns>
419
+
420
+ {/* Section 5 + 6: Table */}
421
+ <Box display="flex" flexDirection="column">
422
+ <div className={css({
423
+ position: 'relative',
424
+ flex: 1,
425
+ minHeight: 0,
426
+ overflowY: 'auto',
427
+ })}>
428
+ <DataTable
429
+ className={reactCss({ width: '100%' })}
430
+ headerClassName={reactCss({
431
+ boxShadow: `inset 0 -2px 0 0 ${theme.color.background.primaryDark}`,
432
+ th: {
433
+ color: theme.color.background.primaryDark,
434
+ textTransform: 'capitalize',
435
+ svg: { stroke: theme.color.background.primaryDark },
436
+ },
437
+ })}
438
+ items={rows}
439
+ columns={columns}
440
+ isLoading={isLoading}
441
+ emptyState={
442
+ <Text tone="muted" size="small">No records found. Try adjusting your filters.</Text>
443
+ }
444
+ />
445
+ </div>
446
+
447
+ {/* Section 7: Pagination — implement with Spark primitives */}
448
+ </Box>
449
+ </Stack>
450
+ </Stack>
451
+ ```
452
+
453
+ ---
454
+
455
+ ## Structural skeleton
456
+
457
+ Use this skeleton as the starting point for new builds and uplifts. Do not use
458
+ existing page implementations as a structural reference.
459
+
460
+ ```tsx
461
+ <Stack height="full" className={css({ minHeight: '100vh' })}>
462
+ <PageHeader label={pageTitle} />
463
+
464
+ <Stack
465
+ padding="large"
466
+ gap="large"
467
+ className={css({
468
+ backgroundColor: theme.color.background.neutral,
469
+ flex: 1,
470
+ display: 'flex',
471
+ flexDirection: 'column',
472
+ overflow: 'hidden',
473
+ })}
474
+ >
475
+ <Columns gap="large" collapseBelow="desktop">
476
+ {/* filters here */}
477
+ {/* search here */}
478
+ </Columns>
479
+
480
+ <Box display="flex" flexDirection="column">
481
+ <div
482
+ className={css({
483
+ position: 'relative',
484
+ flex: 1,
485
+ minHeight: 0,
486
+ overflowY: 'auto',
487
+ })}
488
+ >
489
+ <DataTable
490
+ items={items}
491
+ columns={columns}
492
+ isLoading={isLoading}
493
+ emptyState={emptyState}
494
+ />
495
+ </div>
496
+ </Box>
497
+
498
+ {/* pagination here */}
499
+ </Stack>
500
+ </Stack>
501
+ ```
502
+
503
+ ---
504
+
505
+ ## Documented exceptions summary
506
+
507
+ These raw CSS values are required and have no Spark token equivalent. Use them
508
+ exactly as written. Do not substitute alternatives.
509
+
510
+ | Value | Property | Reason |
511
+ | --------------------------------------------- | ---------------------------- | --------------------------------------- |
512
+ | `minHeight: '100vh'` | Outer Stack | No Spark minHeight prop |
513
+ | `flex: 1` | Content area, scroll wrapper | No Spark flex prop |
514
+ | `display: 'flex'` + `flexDirection: 'column'` | Content area | Required for flex: 1 |
515
+ | `overflow: 'hidden'` | Content area | No Spark overflow on Stack |
516
+ | `position: 'relative'` | Scroll div | No Spark position prop |
517
+ | `minHeight: 0` | Scroll div | Flex scroll fix |
518
+ | `overflowY: 'auto'` | Scroll div | No Spark overflow prop |
519
+ | `backgroundColor: background.neutral` | Content area | Accessed via useTheme(), not a Box prop |
520
+
521
+ ---
522
+
523
+ ## Do NOTs
524
+
525
+ - NEVER use `<Container>` as the outer page wrapper — always use
526
+ `<Stack height="full">`. Container constrains width and removes full-height
527
+ layout and scroll containment behaviour
528
+ - NEVER place DataTable directly inside the content area Stack — always use the
529
+ `<Box display="flex" flexDirection="column"> <div className={...}>` scroll
530
+ wrapper from Section 5. Omitting it breaks page scroll containment
531
+ - NEVER put pagination inside DataTable
532
+ - NEVER use plain text for status values — always use Badge with `children`
533
+ - NEVER import `MeatBall` from `@spark-web/meatball` — the component is
534
+ `MeatballMenu` from `@spark-web/meatball-menu`
535
+ - NEVER use `tone="pending"` on Badge — it does not exist; use `tone="info"` for
536
+ pending/awaiting states
537
+ - NEVER omit the page header — every list page has an H1 title via PageHeader
538
+ - NEVER add padding to the outer Stack wrapper
539
+ - NEVER omit the flex scroll wrapper around DataTable
540
+ - NEVER omit the empty state and loading state
541
+ - NEVER place filter controls inside DataTable
542
+ - NEVER hardcode column widths except for the actions column (80px)
543
+ - NEVER substitute the documented exception values with alternatives
544
+ - NEVER replace PageHeader with a manual Stack + Heading + Text breadcrumb
545
+ - NEVER apply detail page spacing (paddingX="xlarge" paddingY="xxlarge") to a
546
+ list page — those values belong in detail-page.md only
547
+ - NEVER render an external loading spinner/text outside DataTable — always use
548
+ the `isLoading` prop on DataTable to show loading state
549
+ - NEVER use `Stack + Text weight="semibold"` to label a MultiSelectField or
550
+ SelectField filter — always use
551
+ `fieldProps={{ labelVisibility: 'hidden' as const }}`
552
+ - NEVER omit `fieldProps={{ labelVisibility: 'hidden' as const }}` on
553
+ SelectField filter dropdowns — the same hidden label rule that applies to
554
+ MultiSelectField applies to SelectField equally
555
+ - NEVER use `@spark-web/multi-select` in portal-hub — it is not installed; use
556
+ `MultiSelectField` from `@brighte/ui-components`
557
+ - NEVER use `className` with a string from `@emotion/css` on DataTable — use
558
+ `SerializedStyles` from `@emotion/react`'s `css` tagged template