@spark-web/design-system 5.0.99 → 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.
- package/CLAUDE.md +274 -0
- package/ai-context/layer-1-root.md +158 -0
- package/ai-context/layer-2-surface-pattern.md +236 -0
- package/ai-context/layer-3-component.md +271 -0
- package/dist/declarations/src/index.d.ts +8 -0
- package/dist/spark-web-design-system.cjs.dev.js +56 -0
- package/dist/spark-web-design-system.cjs.prod.js +56 -0
- package/dist/spark-web-design-system.esm.js +8 -0
- package/package.json +25 -14
- package/patterns/CLAUDE.md +67 -0
- package/patterns/internal-admin/CLAUDE.md +126 -0
- package/patterns/internal-admin/list-page.md +558 -0
|
@@ -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
|