@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.
- 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 +26 -15
- package/patterns/CLAUDE.md +67 -0
- package/patterns/internal-admin/CLAUDE.md +126 -0
- package/patterns/internal-admin/details-page.md +358 -0
- package/patterns/internal-admin/list-page.md +413 -0
|
@@ -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
|