@spark-web/design-system 5.1.4 → 5.1.6
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 +38 -192
- package/package.json +20 -21
- package/patterns/CLAUDE.md +75 -35
- package/patterns/internal-admin/CLAUDE.md +7 -19
- package/patterns/internal-admin/{details-page.md → detail-page.md} +114 -51
- package/patterns/internal-admin/list-page.md +199 -137
- package/patterns/internal-admin/portal-hub.md +165 -0
- package/patterns/vendor-admin/CLAUDE.md +216 -0
- package/patterns/vendor-admin/dashboard.md +681 -0
- package/patterns/vendor-admin/form-page.md +504 -0
- package/patterns/vendor-admin/list-page.md +709 -0
- package/patterns/vendor-admin/vendor-portal.md +309 -0
- package/ai-context/layer-1-root.md +0 -158
- package/ai-context/layer-2-surface-pattern.md +0 -236
- package/ai-context/layer-3-component.md +0 -271
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Before using this pattern
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
to this pattern.
|
|
5
|
+
Surface rules: read
|
|
6
|
+
`node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md` in
|
|
7
|
+
full first — its rules all apply here.
|
|
9
8
|
|
|
10
9
|
## What this pattern is
|
|
11
10
|
|
|
@@ -15,11 +14,9 @@ admin surfaces.
|
|
|
15
14
|
|
|
16
15
|
## When to use this pattern
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
- A management interface where records can be viewed or acted upon
|
|
22
|
-
- The words "list", "manage", "view all", "records", or "results"
|
|
17
|
+
Any list/management view of records — the registry
|
|
18
|
+
(`node_modules/@spark-web/design-system/patterns/CLAUDE.md`) owns surface and
|
|
19
|
+
feature-type classification.
|
|
23
20
|
|
|
24
21
|
---
|
|
25
22
|
|
|
@@ -27,17 +24,24 @@ Use this pattern when the PRD describes any of the following:
|
|
|
27
24
|
|
|
28
25
|
Read these before implementing — they own the component-level rules:
|
|
29
26
|
|
|
30
|
-
- `
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
36
|
-
- `
|
|
37
|
-
- `
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
40
|
-
- `
|
|
27
|
+
- `node_modules/@spark-web/page-header/CLAUDE.md` — PageHeader API, action type
|
|
28
|
+
rules
|
|
29
|
+
- `node_modules/@spark-web/data-table/CLAUDE.md` — DataTable API, column defs,
|
|
30
|
+
loading/empty states, headerClassName tokens
|
|
31
|
+
- `node_modules/@spark-web/badge/CLAUDE.md` — status tone mapping
|
|
32
|
+
- `node_modules/@spark-web/meatball-menu/CLAUDE.md` — MeatballMenu API
|
|
33
|
+
- `node_modules/@spark-web/stack/CLAUDE.md` — vertical stacking and gap
|
|
34
|
+
- `node_modules/@spark-web/box/CLAUDE.md` — flex layout utilities
|
|
35
|
+
- `node_modules/@spark-web/columns/CLAUDE.md` — responsive multi-column layout
|
|
36
|
+
- `node_modules/@spark-web/field/CLAUDE.md` — Field API, labelVisibility
|
|
37
|
+
- `node_modules/@spark-web/select/CLAUDE.md` — Select API, filter dropdown
|
|
38
|
+
pattern
|
|
39
|
+
- `node_modules/@spark-web/multi-select/CLAUDE.md` — MultiSelect filter
|
|
40
|
+
dropdowns
|
|
41
|
+
- `node_modules/@spark-web/text/CLAUDE.md` — Text API
|
|
42
|
+
- `node_modules/@spark-web/text-input/CLAUDE.md` — TextInput API
|
|
43
|
+
- `node_modules/@spark-web/alert/CLAUDE.md` — page-level feedback Alert
|
|
44
|
+
- `node_modules/@spark-web/icon/CLAUDE.md` — icon sizing and tones (SearchIcon)
|
|
41
45
|
|
|
42
46
|
---
|
|
43
47
|
|
|
@@ -66,8 +70,8 @@ manual `Stack + Heading`.
|
|
|
66
70
|
<PageHeader label={pageTitle} />
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
See `
|
|
70
|
-
statusBadge, and controls props.
|
|
73
|
+
See `node_modules/@spark-web/page-header/CLAUDE.md` for action types (button,
|
|
74
|
+
link, meatball), statusBadge, and controls props.
|
|
71
75
|
|
|
72
76
|
Rules:
|
|
73
77
|
|
|
@@ -111,7 +115,7 @@ Rules:
|
|
|
111
115
|
The outermost container fills the full viewport height.
|
|
112
116
|
|
|
113
117
|
```tsx
|
|
114
|
-
<Stack height="full"
|
|
118
|
+
<Stack height="full" css={{ minHeight: '100vh' }}>
|
|
115
119
|
```
|
|
116
120
|
|
|
117
121
|
Documented exception:
|
|
@@ -129,13 +133,11 @@ Sits inside the outer wrapper. Owns all page padding and gap between sections.
|
|
|
129
133
|
<Stack
|
|
130
134
|
padding="large"
|
|
131
135
|
gap="large"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
overflow: 'hidden',
|
|
138
|
-
})}
|
|
136
|
+
flex={1}
|
|
137
|
+
display="flex"
|
|
138
|
+
flexDirection="column"
|
|
139
|
+
overflow="hidden"
|
|
140
|
+
css={{ backgroundColor: theme.color.background.neutral }}
|
|
139
141
|
>
|
|
140
142
|
```
|
|
141
143
|
|
|
@@ -145,11 +147,17 @@ Token mappings:
|
|
|
145
147
|
- `gap="large"` — between all sections, Spark token
|
|
146
148
|
- `backgroundColor` — `theme.color.background.neutral` via `useTheme()`
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
Spark props (not exceptions):
|
|
151
|
+
|
|
152
|
+
- `flex={1}` — makes content area fill remaining height (Box `flex` prop)
|
|
153
|
+
- `display="flex"` + `flexDirection="column"` — required to activate `flex={1}`
|
|
154
|
+
(Box props)
|
|
155
|
+
- `overflow="hidden"` — scroll containment (Box `overflow` prop)
|
|
156
|
+
|
|
157
|
+
Documented exception:
|
|
149
158
|
|
|
150
|
-
- `
|
|
151
|
-
|
|
152
|
-
- `overflow: 'hidden'` — scroll containment, no Spark overflow prop on Stack
|
|
159
|
+
- `backgroundColor: theme.color.background.neutral` — accessed via `useTheme()`,
|
|
160
|
+
not a Box prop, so it goes through the `css` prop
|
|
153
161
|
|
|
154
162
|
---
|
|
155
163
|
|
|
@@ -158,29 +166,43 @@ Documented exceptions:
|
|
|
158
166
|
Renders filter dropdowns and a search input in a horizontal row.
|
|
159
167
|
|
|
160
168
|
```tsx
|
|
161
|
-
const FieldProps = { labelVisibility: 'hidden' as const };
|
|
162
|
-
|
|
163
169
|
<Columns gap="large" collapseBelow="desktop">
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
{/* Search — always first */}
|
|
171
|
+
<Field label="Search" labelVisibility="hidden">
|
|
172
|
+
<TextInput
|
|
173
|
+
placeholder="Search by..."
|
|
174
|
+
value={search}
|
|
175
|
+
onChange={e => setSearch(e.target.value)}
|
|
176
|
+
>
|
|
177
|
+
<InputAdornment placement="start">
|
|
178
|
+
<SearchIcon size="xxsmall" tone="muted" />
|
|
179
|
+
</InputAdornment>
|
|
180
|
+
</TextInput>
|
|
181
|
+
</Field>
|
|
182
|
+
|
|
183
|
+
{/* Multi-select filter */}
|
|
184
|
+
<Box>
|
|
185
|
+
<VisuallyHidden as="span" id="status-filter-label">
|
|
186
|
+
Status
|
|
187
|
+
</VisuallyHidden>
|
|
188
|
+
<MultiSelect
|
|
189
|
+
aria-labelledby="status-filter-label"
|
|
190
|
+
options={[{ label: '', options: statusOptions }]}
|
|
191
|
+
placeholder="Filter by status..."
|
|
192
|
+
onChange={setStatusFilter}
|
|
193
|
+
/>
|
|
194
|
+
</Box>
|
|
195
|
+
|
|
196
|
+
{/* Single-select filter */}
|
|
197
|
+
<Field label="Role" labelVisibility="hidden">
|
|
198
|
+
<Select
|
|
199
|
+
placeholder="Filter by role..."
|
|
200
|
+
options={roleOptions}
|
|
201
|
+
value={roleFilter}
|
|
202
|
+
onChange={e => setRoleFilter(e.target.value)}
|
|
203
|
+
/>
|
|
204
|
+
</Field>
|
|
205
|
+
</Columns>
|
|
184
206
|
```
|
|
185
207
|
|
|
186
208
|
Rules:
|
|
@@ -190,13 +212,18 @@ Rules:
|
|
|
190
212
|
- **Search input always appears first (leftmost)** in the filter row
|
|
191
213
|
- Filter dropdowns follow the search input, ordered by specificity (broadest
|
|
192
214
|
filter first, most specific last)
|
|
193
|
-
- Multi-select filter dropdowns use `
|
|
194
|
-
`@
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
215
|
+
- Multi-select filter dropdowns use `MultiSelect` from
|
|
216
|
+
`@spark-web/multi-select`. Its `options` are grouped — pass
|
|
217
|
+
`[{ label: '', options }]` for a flat list. It renders no visible label, so
|
|
218
|
+
label it for assistive technology via `aria-labelledby` pointing at a
|
|
219
|
+
`VisuallyHidden` element from `@spark-web/a11y`
|
|
220
|
+
- Single-select filter dropdowns use `Select` from `@spark-web/select`, always
|
|
221
|
+
wrapped in a `Field`
|
|
222
|
+
- Filter labels are never visible — wrap `TextInput` and `Select` in a `Field`
|
|
223
|
+
with `labelVisibility="hidden"`; the `label` is still required for
|
|
224
|
+
accessibility
|
|
225
|
+
- Working in portal-hub? Apply the substitutions in
|
|
226
|
+
`node_modules/@spark-web/design-system/patterns/internal-admin/portal-hub.md`.
|
|
200
227
|
- If no filtering or searching is needed, omit this section entirely
|
|
201
228
|
|
|
202
229
|
---
|
|
@@ -207,32 +234,33 @@ Wraps the DataTable to enable vertical scrolling without the page scrolling.
|
|
|
207
234
|
|
|
208
235
|
```tsx
|
|
209
236
|
<Box display="flex" flexDirection="column">
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
minHeight: 0,
|
|
215
|
-
overflowY: 'auto',
|
|
216
|
-
})}
|
|
237
|
+
<Box
|
|
238
|
+
position="relative"
|
|
239
|
+
flex={1}
|
|
240
|
+
css={{ minHeight: 0, overflowY: 'auto' }}
|
|
217
241
|
>
|
|
218
242
|
<DataTable ... />
|
|
219
|
-
</
|
|
243
|
+
</Box>
|
|
220
244
|
</Box>
|
|
221
245
|
```
|
|
222
246
|
|
|
223
|
-
|
|
247
|
+
Spark props and documented exceptions — all required for flex scroll behaviour:
|
|
224
248
|
|
|
225
|
-
- `position
|
|
226
|
-
- `flex
|
|
227
|
-
- `minHeight: 0` — classic flex scroll fix, prevents overflow
|
|
228
|
-
|
|
249
|
+
- `position="relative"` — scroll container positioning (Box `position` prop)
|
|
250
|
+
- `flex={1}` — fills available height (Box `flex` prop)
|
|
251
|
+
- `minHeight: 0` — classic flex scroll fix, prevents overflow (documented
|
|
252
|
+
exception — Box `minHeight` only accepts `0` via the prop, so this goes
|
|
253
|
+
through `css`)
|
|
254
|
+
- `overflowY: 'auto'` — enables vertical scrolling (documented exception — no
|
|
255
|
+
axis-specific overflow prop, so this goes through `css`)
|
|
229
256
|
|
|
230
257
|
---
|
|
231
258
|
|
|
232
259
|
## Section 6 — Table
|
|
233
260
|
|
|
234
|
-
Always uses `@spark-web/data-table`. See
|
|
235
|
-
column definition API,
|
|
261
|
+
Always uses `@spark-web/data-table`. See
|
|
262
|
+
`node_modules/@spark-web/data-table/CLAUDE.md` for column definition API,
|
|
263
|
+
loading/empty states, sorting, and expansion.
|
|
236
264
|
|
|
237
265
|
Apply canonical list-page header styling via `headerClassName`. Use the named
|
|
238
266
|
import `import { css as reactCss } from '@emotion/react'` — `headerClassName`
|
|
@@ -270,14 +298,14 @@ Column rules:
|
|
|
270
298
|
exist
|
|
271
299
|
|
|
272
300
|
Row interaction rules — see
|
|
273
|
-
`
|
|
301
|
+
`node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md`:
|
|
274
302
|
|
|
275
303
|
- Pass `onRowClick` and `enableClickableRow` when the record has a detail view
|
|
276
304
|
- Hover state is applied automatically when `enableClickableRow` is true
|
|
277
305
|
|
|
278
306
|
Status tone mapping — authoritative rules are in
|
|
279
|
-
`
|
|
280
|
-
them here.
|
|
307
|
+
`node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md`. Do
|
|
308
|
+
not duplicate them here.
|
|
281
309
|
|
|
282
310
|
---
|
|
283
311
|
|
|
@@ -285,12 +313,30 @@ them here.
|
|
|
285
313
|
|
|
286
314
|
Render `TablePagination` outside and below DataTable — never nested inside it.
|
|
287
315
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
316
|
+
```tsx
|
|
317
|
+
{
|
|
318
|
+
/* COMPONENT GAP: TablePagination is NOT a spark-web component — build a
|
|
319
|
+
placeholder from @spark-web/box + @spark-web/button (prev/next + "Show N of
|
|
320
|
+
M") and flag it for product design review before production. Props:
|
|
321
|
+
total, pageSize, current, dataShowing, onChange(page). */
|
|
322
|
+
}
|
|
323
|
+
{
|
|
324
|
+
total > PAGE_SIZE && (
|
|
325
|
+
<TablePagination
|
|
326
|
+
total={total}
|
|
327
|
+
pageSize={PAGE_SIZE}
|
|
328
|
+
dataShowing={rows.length}
|
|
329
|
+
onChange={setPage}
|
|
330
|
+
current={page}
|
|
331
|
+
/>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Render-only-when `total > pageSize`, default `pageSize` **20**, and
|
|
337
|
+
total-from-a-dedicated-count-query are enforced by the Do NOTs and checklist
|
|
338
|
+
below. No additional wrapper needed — content area `gap="large"` handles
|
|
339
|
+
spacing.
|
|
294
340
|
|
|
295
341
|
---
|
|
296
342
|
|
|
@@ -300,42 +346,23 @@ Use this skeleton as the starting point for new builds and uplifts. Do not use
|
|
|
300
346
|
existing page implementations as a structural reference.
|
|
301
347
|
|
|
302
348
|
```tsx
|
|
303
|
-
<Stack height="full"
|
|
349
|
+
<Stack height="full" css={{ minHeight: '100vh' }}>
|
|
304
350
|
<PageHeader label={pageTitle} />
|
|
305
351
|
|
|
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
|
-
>
|
|
352
|
+
<Stack /* Section 3 content-area props — copy verbatim from Section 3 */>
|
|
317
353
|
<Columns gap="large" collapseBelow="desktop">
|
|
318
|
-
{/* search first, then filters */}
|
|
354
|
+
{/* search first, then filters — Section 4 */}
|
|
319
355
|
</Columns>
|
|
320
356
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
})}
|
|
329
|
-
>
|
|
330
|
-
<DataTable
|
|
331
|
-
items={items}
|
|
332
|
-
columns={columns}
|
|
333
|
-
isLoading={isLoading}
|
|
334
|
-
emptyState={emptyState}
|
|
335
|
-
/>
|
|
336
|
-
</div>
|
|
337
|
-
</Box>
|
|
357
|
+
{/* Section 5 scroll wrapper — copy verbatim from Section 5, wrapping: */}
|
|
358
|
+
<DataTable
|
|
359
|
+
items={items}
|
|
360
|
+
columns={columns}
|
|
361
|
+
isLoading={isLoading}
|
|
362
|
+
emptyState={emptyState}
|
|
363
|
+
/>
|
|
338
364
|
|
|
365
|
+
{/* COMPONENT GAP: TablePagination — see Section 7 */}
|
|
339
366
|
{total > PAGE_SIZE && (
|
|
340
367
|
<TablePagination
|
|
341
368
|
total={total}
|
|
@@ -356,16 +383,15 @@ existing page implementations as a structural reference.
|
|
|
356
383
|
These raw CSS values are required and have no Spark token equivalent. Use them
|
|
357
384
|
exactly as written. Do not substitute alternatives.
|
|
358
385
|
|
|
359
|
-
| Value
|
|
360
|
-
|
|
|
361
|
-
| `minHeight: '100vh'`
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
368
|
-
| `backgroundColor: background.neutral` | Content area | Accessed via useTheme(), not a Box prop |
|
|
386
|
+
| Value | Property | Reason |
|
|
387
|
+
| ------------------------------------------- | ------------------------------ | ------------------------------------------------ |
|
|
388
|
+
| `minHeight: '100vh'` | Outer Stack | No Spark minHeight prop |
|
|
389
|
+
| `minHeight: 0` | Scroll Box | Flex scroll fix — Box `minHeight` only accepts 0 |
|
|
390
|
+
| `overflowY: 'auto'` | Scroll Box | No axis-specific overflow prop |
|
|
391
|
+
| `backgroundColor: background.neutral` | Content area | Accessed via useTheme(), not a Box prop |
|
|
392
|
+
| `boxShadow: inset 0 -2px ...` | DataTable `headerClassName` | Canonical header underline — no token equivalent |
|
|
393
|
+
| `textTransform: 'capitalize'`, `svg stroke` | DataTable `headerClassName` th | Canonical header text/icon styling |
|
|
394
|
+
| `width: '100%'` | DataTable `className` | Table fills the scroll wrapper |
|
|
369
395
|
|
|
370
396
|
---
|
|
371
397
|
|
|
@@ -375,8 +401,8 @@ exactly as written. Do not substitute alternatives.
|
|
|
375
401
|
`<Stack height="full">`. Container constrains width and removes full-height
|
|
376
402
|
layout and scroll containment behaviour
|
|
377
403
|
- NEVER place DataTable directly inside the content area Stack — always use the
|
|
378
|
-
`<Box display="flex" flexDirection="column"
|
|
379
|
-
|
|
404
|
+
`<Box display="flex" flexDirection="column">` + inner scroll `<Box>` wrapper
|
|
405
|
+
from Section 5. Omitting it breaks page scroll containment
|
|
380
406
|
- NEVER put pagination inside DataTable
|
|
381
407
|
- NEVER render pagination when all records fit on one page — only show it when
|
|
382
408
|
total > pageSize
|
|
@@ -401,13 +427,49 @@ exactly as written. Do not substitute alternatives.
|
|
|
401
427
|
list page — those values belong in detail-page.md only
|
|
402
428
|
- NEVER render an external loading spinner/text outside DataTable — always use
|
|
403
429
|
the `isLoading` prop on DataTable to show loading state
|
|
404
|
-
- NEVER use `Stack + Text weight="semibold"` to label a
|
|
405
|
-
|
|
406
|
-
`
|
|
407
|
-
|
|
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`
|
|
430
|
+
- NEVER use `Stack + Text weight="semibold"` to label a select or multi-select
|
|
431
|
+
filter — labels are provided accessibly but never visibly:
|
|
432
|
+
`labelVisibility="hidden"` on `Field`, or `aria-labelledby` + `VisuallyHidden`
|
|
433
|
+
for `MultiSelect`
|
|
412
434
|
- NEVER use `className` with a string from `@emotion/css` on DataTable — use
|
|
413
435
|
`SerializedStyles` from `@emotion/react`'s `css` tagged template
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Validation checklist
|
|
440
|
+
|
|
441
|
+
Run before marking any list-page task complete and fix every violation; the
|
|
442
|
+
uplift protocol in `node_modules/@spark-web/design-system/CLAUDE.md` runs this
|
|
443
|
+
list PASS/FAIL against existing pages.
|
|
444
|
+
|
|
445
|
+
1. PageHeader from `@spark-web/page-header` renders the page H1 via `label` — no
|
|
446
|
+
manual Stack + Heading.
|
|
447
|
+
2. Outer wrapper is `Stack height="full"` with the documented
|
|
448
|
+
`minHeight: '100vh'` exception — not Container, and no padding on the outer
|
|
449
|
+
wrapper.
|
|
450
|
+
3. Content area Stack has `padding="large" gap="large"`, neutral background via
|
|
451
|
+
the `css` prop, and `flex={1}` / `display="flex"` / `flexDirection="column"`
|
|
452
|
+
/ `overflow="hidden"` as Spark Box props — nothing else.
|
|
453
|
+
4. If search/filtering exists: filter row is
|
|
454
|
+
`Columns gap="large" collapseBelow="desktop"`, search input first (leftmost),
|
|
455
|
+
all labels hidden but accessible — `labelVisibility="hidden"` on each
|
|
456
|
+
`Field`, `aria-labelledby` + `VisuallyHidden` for `MultiSelect` (or the
|
|
457
|
+
consumer-overlay equivalent).
|
|
458
|
+
5. DataTable sits inside the flex scroll wrapper from Section 5 — never directly
|
|
459
|
+
in the content Stack.
|
|
460
|
+
6. DataTable receives `isLoading` and `emptyState` — no external spinner/loading
|
|
461
|
+
text.
|
|
462
|
+
7. Status columns use `<Badge>` (dot + label) with a tone from the surface tone
|
|
463
|
+
mapping — never plain text, never StatusBadge, never `tone="pending"`.
|
|
464
|
+
8. Actions column (if present) is last, `size: 80`, empty header; MeatballMenu
|
|
465
|
+
when 2+ row actions; row click/hover follows the surface rules; no hardcoded
|
|
466
|
+
column widths except the actions column.
|
|
467
|
+
9. Pagination renders outside DataTable, only when `total > pageSize` (default
|
|
468
|
+
20), with `total` from a dedicated count query.
|
|
469
|
+
10. No raw CSS values beyond the Documented exceptions table (which includes the
|
|
470
|
+
Section 6 canonical header styling).
|
|
471
|
+
11. Every component is `@spark-web/*` or an explicit consumer-overlay
|
|
472
|
+
substitute; anything missing is flagged with a `// COMPONENT GAP:` comment.
|
|
473
|
+
12. DataTable header uses the canonical `headerClassName` styling from Section
|
|
474
|
+
6, passed as `SerializedStyles` from `@emotion/react`'s `css` — never a
|
|
475
|
+
string class from `@emotion/css`.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Internal admin — portal-hub consumer overlay
|
|
2
|
+
|
|
3
|
+
Consumer overlay for the **portal-hub** repo (see Consumer overlays in
|
|
4
|
+
`node_modules/@spark-web/design-system/patterns/CLAUDE.md`) — it overrides the
|
|
5
|
+
internal-admin pattern files and component-level CLAUDE.md files ONLY where it
|
|
6
|
+
explicitly says so; every other rule applies to portal-hub unchanged.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Filter fields (overrides list-page.md Section 4)
|
|
11
|
+
|
|
12
|
+
`@spark-web/multi-select` is not available to app code in portal-hub (it is a
|
|
13
|
+
dependency of the internal ui-components package only). Filter and search fields
|
|
14
|
+
come from `@brighte/ui-components` instead. This override applies ONLY to
|
|
15
|
+
component substitutions; all other Section 4 rules — search input always first,
|
|
16
|
+
filter ordering broadest to most specific, hidden labels, omit the section when
|
|
17
|
+
no filtering exists — apply unchanged. The substitutions:
|
|
18
|
+
|
|
19
|
+
- Multi-select filter dropdowns use `MultiSelectField` from
|
|
20
|
+
`@brighte/ui-components`
|
|
21
|
+
- Single-select filter dropdowns use `SelectField` from `@brighte/ui-components`
|
|
22
|
+
- The search input uses `TextInputField` from `@brighte/ui-components` with the
|
|
23
|
+
same start-adornment `SearchIcon`
|
|
24
|
+
|
|
25
|
+
Always pass `fieldProps={{ labelVisibility: 'hidden' as const }}` on both
|
|
26
|
+
`MultiSelectField` and `SelectField` — define it as a constant outside the
|
|
27
|
+
component to avoid re-renders:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
const FieldProps = { labelVisibility: 'hidden' as const };
|
|
31
|
+
|
|
32
|
+
<Columns gap="large" collapseBelow="desktop">
|
|
33
|
+
<TextInputField
|
|
34
|
+
control={control}
|
|
35
|
+
name="search"
|
|
36
|
+
label="Search"
|
|
37
|
+
placeholder="Search by..."
|
|
38
|
+
FieldProps={FieldProps}
|
|
39
|
+
>
|
|
40
|
+
<InputAdornment placement="start">
|
|
41
|
+
<SearchIcon size="xxsmall" tone="muted" />
|
|
42
|
+
</InputAdornment>
|
|
43
|
+
</TextInputField>
|
|
44
|
+
<MultiSelectField
|
|
45
|
+
control={control}
|
|
46
|
+
name="fieldName"
|
|
47
|
+
label="Label"
|
|
48
|
+
options={options}
|
|
49
|
+
placeholder="Filter by..."
|
|
50
|
+
fieldProps={FieldProps}
|
|
51
|
+
/>
|
|
52
|
+
</Columns>;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Note the casing: the prop is `FieldProps` (capital F) on `TextInputField` and
|
|
56
|
+
`fieldProps` (lowercase f) on `MultiSelectField`/`SelectField` — match the
|
|
57
|
+
snippet exactly.
|
|
58
|
+
|
|
59
|
+
Do NOTs (portal-hub):
|
|
60
|
+
|
|
61
|
+
- NEVER import `@spark-web/multi-select` directly in portal-hub app code; use
|
|
62
|
+
`MultiSelectField` from `@brighte/ui-components`
|
|
63
|
+
- NEVER omit the hidden-label `fieldProps` constant on SelectField filter
|
|
64
|
+
dropdowns — define it once outside the component and pass it everywhere
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Text inputs (overrides the 'Controlled usage with form state' sections in text-input/text-area CLAUDE.md)
|
|
69
|
+
|
|
70
|
+
In portal-hub, prefer the `Field`-wrapped bindings from `@brighte/ui-components`
|
|
71
|
+
over hand-wiring `@spark-web/text-input` / `@spark-web/text-area` into forms:
|
|
72
|
+
|
|
73
|
+
- Prefer `TextInputField` from `@brighte/ui-components` when used with
|
|
74
|
+
`react-hook-form`. Pass `placeholder` directly, and use `FieldProps` to pass
|
|
75
|
+
`description` (renders as muted hint text below the label):
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<TextInputField
|
|
79
|
+
control={control}
|
|
80
|
+
name="subject"
|
|
81
|
+
label="Subject"
|
|
82
|
+
placeholder="Type here..."
|
|
83
|
+
FieldProps={{ description: 'Enter a brief summary of the issue' }}
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- Prefer `TextAreaField` from `@brighte/ui-components` when used with
|
|
88
|
+
`react-hook-form`. Pass `placeholder` directly:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<TextAreaField
|
|
92
|
+
control={control}
|
|
93
|
+
name="note"
|
|
94
|
+
label="Note"
|
|
95
|
+
placeholder="Type your note here..."
|
|
96
|
+
/>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Section cards (overrides detail-page.md Section 7)
|
|
102
|
+
|
|
103
|
+
portal-hub uses a custom wrapper at `@components/PortalTable/SectionCard` — not
|
|
104
|
+
`@spark-web/section-card`. The API differs from the Spark component:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { SectionCard } from '@components/PortalTable/SectionCard';
|
|
108
|
+
|
|
109
|
+
<SectionCard label="Section Title">{/* section content */}</SectionCard>;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Prop | Type | Notes |
|
|
113
|
+
| ---------- | ----------- | ---------------------------------------------------------------------------- |
|
|
114
|
+
| `label` | `string` | Optional — card header text; usually provided — header only renders when set |
|
|
115
|
+
| `tag` | `TagProps` | Optional — tag rendered in the card header |
|
|
116
|
+
| `action` | `ReactNode` | Optional — right-side header control |
|
|
117
|
+
| `controls` | `ReactNode` | Optional — additional header controls |
|
|
118
|
+
|
|
119
|
+
Return `null` for sections conditionally hidden — never render an empty card.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Modal sizing (ACCREDITATION_MODAL_CSS)
|
|
124
|
+
|
|
125
|
+
The shared admin modal size constant described in
|
|
126
|
+
`node_modules/@spark-web/modal-dialog/CLAUDE.md` already exists in portal-hub —
|
|
127
|
+
do not define a new one. The standard size constant used across all admin
|
|
128
|
+
confirmation modals is defined in `apps/admin-portal/src/utils/constants.tsx`:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
export const ACCREDITATION_MODAL_CSS = {
|
|
132
|
+
width: '100vw',
|
|
133
|
+
maxWidth: '550px',
|
|
134
|
+
} as const;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Always pass this constant via `css={ACCREDITATION_MODAL_CSS}` — never set width
|
|
138
|
+
inline or use a raw pixel value.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Row-as-link reference implementation
|
|
143
|
+
|
|
144
|
+
The row-as-link navigation pattern documented in
|
|
145
|
+
`node_modules/@spark-web/data-table/CLAUDE.md` has a reference implementation in
|
|
146
|
+
portal-hub at `apps/admin-portal/src/components/RowLink` (uses TanStack Router).
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## TablePagination
|
|
151
|
+
|
|
152
|
+
portal-hub supplies its own `TablePagination` component. It is the
|
|
153
|
+
`TablePagination` referenced by both the list-page and detail-page patterns.
|
|
154
|
+
Props used by the patterns:
|
|
155
|
+
|
|
156
|
+
| Prop | Notes |
|
|
157
|
+
| ------------- | ------------------------------------------------- |
|
|
158
|
+
| `total` | Total record count — from a dedicated count query |
|
|
159
|
+
| `pageSize` | 20 on list pages; 5 on detail-page section tables |
|
|
160
|
+
| `dataShowing` | Number of rows on the current page |
|
|
161
|
+
| `onChange` | Page change handler |
|
|
162
|
+
| `current` | Current page number |
|
|
163
|
+
|
|
164
|
+
This is a confirmed COMPONENT GAP — no `@spark-web` pagination component exists
|
|
165
|
+
yet. Until one ships, every consumer supplies its own pagination component.
|