@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,67 @@
|
|
|
1
|
+
# Pattern library — surface classifier
|
|
2
|
+
|
|
3
|
+
When an agent receives a PRD or feature brief, it must read this file first
|
|
4
|
+
before reading any component or pattern documentation.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Step 1 — Identify the surface
|
|
9
|
+
|
|
10
|
+
Determine which surface is being built by reading the PRD for these signals:
|
|
11
|
+
|
|
12
|
+
| Signal in PRD | Surface |
|
|
13
|
+
| ---------------------------------------------------------------- | --------------------------------- |
|
|
14
|
+
| "admin", "internal", "back office", "ops", "manage", "dashboard" | Internal admin |
|
|
15
|
+
| "customer", "portal", "my account", "self-service" | Customer portal (not yet defined) |
|
|
16
|
+
| "website", "marketing", "landing page", "public" | Website (not yet defined) |
|
|
17
|
+
| "mobile", "app", "iOS", "Android" | Mobile app (not yet defined) |
|
|
18
|
+
|
|
19
|
+
If the surface cannot be determined from the PRD, ask for clarification before
|
|
20
|
+
proceeding. Do not assume.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Step 2 — Read the rules for that surface
|
|
25
|
+
|
|
26
|
+
| Surface | Rules location |
|
|
27
|
+
| --------------- | ------------------------------------------------------------------------- |
|
|
28
|
+
| Internal admin | `node_modules/@spark-web/design-system/patterns/internal-admin/CLAUDE.md` |
|
|
29
|
+
| Customer portal | Not yet defined — flag to the team |
|
|
30
|
+
| Website | Not yet defined — flag to the team |
|
|
31
|
+
| Mobile app | Not yet defined — flag to the team |
|
|
32
|
+
|
|
33
|
+
Read the surface rules file in full before reading any component documentation.
|
|
34
|
+
Surface rules take precedence over component rules.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Step 3 — Read the pattern for the feature type
|
|
39
|
+
|
|
40
|
+
After reading surface rules, identify the feature type and read the
|
|
41
|
+
corresponding pattern file:
|
|
42
|
+
|
|
43
|
+
| Feature type | Pattern file |
|
|
44
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------- |
|
|
45
|
+
| List of records with actions | `node_modules/@spark-web/design-system/patterns/internal-admin/list-page.md` |
|
|
46
|
+
| Create or edit a record | `node_modules/@spark-web/design-system/patterns/internal-admin/form-page.md` (coming soon) |
|
|
47
|
+
| Record detail view | `node_modules/@spark-web/design-system/patterns/internal-admin/detail-page.md` (coming soon) |
|
|
48
|
+
|
|
49
|
+
If no pattern file exists yet for the feature type, use the surface rules and
|
|
50
|
+
component documentation to make the best decision, and flag that a pattern file
|
|
51
|
+
should be created for this feature type.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Reading order for any build task
|
|
56
|
+
|
|
57
|
+
1. `node_modules/@spark-web/design-system/patterns/CLAUDE.md` ← this file
|
|
58
|
+
(surface classifier)
|
|
59
|
+
2. `node_modules/@spark-web/design-system/patterns/[surface]/CLAUDE.md` ←
|
|
60
|
+
surface interaction rules
|
|
61
|
+
3. `node_modules/@spark-web/design-system/patterns/[surface]/[pattern].md` ←
|
|
62
|
+
feature pattern (if exists)
|
|
63
|
+
4. `node_modules/@spark-web/[component]/CLAUDE.md` ← component rules
|
|
64
|
+
5. `node_modules/@spark-web/[component]/src/[component].stories.tsx` ← usage
|
|
65
|
+
examples
|
|
66
|
+
|
|
67
|
+
Never skip steps. Never read component documentation before surface rules.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Internal admin — interaction and pattern rules
|
|
2
|
+
|
|
3
|
+
These rules apply to all components and patterns built for internal admin
|
|
4
|
+
surfaces. They sit above individual component rules. When a conflict exists
|
|
5
|
+
between these rules and a component-level CLAUDE.md, these rules take precedence
|
|
6
|
+
for admin surfaces.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Row interaction rules
|
|
11
|
+
|
|
12
|
+
### Clickable rows
|
|
13
|
+
|
|
14
|
+
A row is clickable when the data model has a record-level detail view or
|
|
15
|
+
destination page. The agent must determine this from the PRD:
|
|
16
|
+
|
|
17
|
+
- If the PRD describes a detail page, record view, or drill-down → row is
|
|
18
|
+
clickable
|
|
19
|
+
- If the PRD describes a read-only data display with no record destination → row
|
|
20
|
+
is not clickable
|
|
21
|
+
- If unsure, default to clickable
|
|
22
|
+
|
|
23
|
+
### Hover state
|
|
24
|
+
|
|
25
|
+
Hover state is determined entirely by whether the row is clickable:
|
|
26
|
+
|
|
27
|
+
- Row is clickable → hover state is applied when the cursor is over the row,
|
|
28
|
+
**except** when hovering a button within the row (e.g. the MeatballMenu
|
|
29
|
+
trigger)
|
|
30
|
+
- Row is not clickable → hover state is never applied, no exceptions
|
|
31
|
+
|
|
32
|
+
Never apply a hover state to a non-clickable row. It implies interactivity that
|
|
33
|
+
does not exist and misleads the user.
|
|
34
|
+
|
|
35
|
+
When a clickable row contains a MeatballMenu, the row hover must be suppressed
|
|
36
|
+
while the cursor is over the meatball button. Two competing hover targets (row
|
|
37
|
+
and menu trigger) confuse the user about what will happen on click. Implement
|
|
38
|
+
using CSS `:has()` on the `<tr>` — see `@spark-web/data-table` CLAUDE.md for the
|
|
39
|
+
exact pattern.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Overflow menu rules
|
|
44
|
+
|
|
45
|
+
An overflow menu on a row is used only when ALL of the following are true:
|
|
46
|
+
|
|
47
|
+
- There are 2 or more actions that apply to the row record
|
|
48
|
+
- The actions relate only to that specific record, not to the page or table
|
|
49
|
+
|
|
50
|
+
Use this decision tree:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Does the row have actions?
|
|
54
|
+
No → no overflow menu, no action column
|
|
55
|
+
Yes → how many actions?
|
|
56
|
+
1 action + row is NOT clickable → inline button or icon
|
|
57
|
+
1 action + row IS clickable → overflow menu
|
|
58
|
+
2+ actions → always overflow menu
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Never place inline action buttons alongside a clickable row. A clickable row and
|
|
62
|
+
inline buttons create two competing interaction targets and confuse the user
|
|
63
|
+
about what clicking does.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Badge and pill rules
|
|
68
|
+
|
|
69
|
+
A badge or pill is used only when:
|
|
70
|
+
|
|
71
|
+
- The field represents a status with 2 or more distinct values
|
|
72
|
+
- The status values have meaningful visual distinction (e.g. active vs inactive,
|
|
73
|
+
pending vs approved vs rejected)
|
|
74
|
+
|
|
75
|
+
Do not use a badge when:
|
|
76
|
+
|
|
77
|
+
- The field is free-form text
|
|
78
|
+
- The field is a boolean with no meaningful status distinction
|
|
79
|
+
- There is only one possible value
|
|
80
|
+
|
|
81
|
+
### Badge tone mapping
|
|
82
|
+
|
|
83
|
+
This mapping is authoritative for the internal-admin surface. If a component
|
|
84
|
+
CLAUDE.md defines tone guidance, the mapping below takes precedence.
|
|
85
|
+
|
|
86
|
+
Map status values to tones using this logic derived from the PRD:
|
|
87
|
+
|
|
88
|
+
- Positive / active / approved / complete → `positive` tone
|
|
89
|
+
- Warning / approaching limit / expiring → `caution` tone
|
|
90
|
+
- Critical / rejected / failed / overdue / suspended → `critical` tone
|
|
91
|
+
- Neutral / inactive / archived / unknown → `neutral` tone
|
|
92
|
+
- Pending / in review / awaiting approval → `info` tone
|
|
93
|
+
|
|
94
|
+
**Caution vs info (pending):** Use `caution` for warnings that are
|
|
95
|
+
time-sensitive or approaching a limit. Use `info` for states where a record is
|
|
96
|
+
waiting on a human decision or review (e.g. "Pending approval", "Under review").
|
|
97
|
+
|
|
98
|
+
Component choice:
|
|
99
|
+
|
|
100
|
+
- Always use `<Badge>` (dot + label) for status columns in list-page tables —
|
|
101
|
+
never `<StatusBadge>`
|
|
102
|
+
- Use `<StatusBadge>` (colored pill, no dot) only in page headers and section
|
|
103
|
+
headers via their `statusBadge` prop — never in table status columns
|
|
104
|
+
|
|
105
|
+
Available tones for both components:
|
|
106
|
+
`accent | caution | critical | info | neutral | positive`. There is no `pending`
|
|
107
|
+
tone — always use `info` for pending/awaiting states.
|
|
108
|
+
|
|
109
|
+
When in doubt, use `neutral`. Never invent a tone that does not exist in the
|
|
110
|
+
Spark Web theme.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Rules that update as the system grows
|
|
115
|
+
|
|
116
|
+
When a new pattern or component is added to the internal admin surface, update
|
|
117
|
+
this file with any new interaction rules that apply globally. Do not duplicate
|
|
118
|
+
rules that already exist here in component-level CLAUDE.md files.
|
|
119
|
+
|
|
120
|
+
Then open the root CLAUDE.md and append this line to the Architecture section:
|
|
121
|
+
|
|
122
|
+
## Pattern library
|
|
123
|
+
|
|
124
|
+
Agent pattern and surface rules live in
|
|
125
|
+
node_modules/@spark-web/design-system/patterns/. Always read
|
|
126
|
+
node_modules/@spark-web/design-system/patterns/CLAUDE.md before any build task.
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# Internal admin — details 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, and action dropdown rules defined there all apply to this pattern.
|
|
8
|
+
|
|
9
|
+
## What this pattern is
|
|
10
|
+
|
|
11
|
+
A full page layout for displaying the detail view of a single record in an
|
|
12
|
+
internal admin interface. The page shows structured data and contextual
|
|
13
|
+
sub-tables in a two-column layout, with record-level actions surfaced through a
|
|
14
|
+
header dropdown.
|
|
15
|
+
|
|
16
|
+
## When to use this pattern
|
|
17
|
+
|
|
18
|
+
Use this pattern when the PRD describes any of the following:
|
|
19
|
+
|
|
20
|
+
- A single-record view reachable by clicking a row on a list page
|
|
21
|
+
- A page showing a record's fields, history, or associated sub-records
|
|
22
|
+
- The words "detail", "profile", "view", "record page", or "user/vendor page"
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Component docs to read
|
|
27
|
+
|
|
28
|
+
Read these before implementing — they own the component-level rules:
|
|
29
|
+
|
|
30
|
+
- `packages/action-dropdown/CLAUDE.md` — dropdown construction, ordering, hide
|
|
31
|
+
vs. disable
|
|
32
|
+
- `packages/modal-dialog/CLAUDE.md` — ContentDialog API,
|
|
33
|
+
ACCREDITATION_MODAL_CSS, destructive modal anatomy
|
|
34
|
+
- `packages/data-table/CLAUDE.md` — DataTable API, loading/empty states,
|
|
35
|
+
expandable rows
|
|
36
|
+
- `packages/tabs/CLAUDE.md` — Tabs API, internal-admin background override, null
|
|
37
|
+
guard
|
|
38
|
+
- `packages/section-card/CLAUDE.md` — SectionCard API (note: portal-hub uses a
|
|
39
|
+
custom wrapper; see Section 6 below)
|
|
40
|
+
- `packages/badge/CLAUDE.md` — status tone mapping
|
|
41
|
+
- `packages/columns/CLAUDE.md` — responsive two-column layout
|
|
42
|
+
- `packages/box/CLAUDE.md` — flex layout utilities
|
|
43
|
+
- `packages/stack/CLAUDE.md` — vertical stacking and gap
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Page structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Outer wrapper Stack paddingX="xlarge" paddingY="xxlarge" gap="xlarge"
|
|
51
|
+
Header Box spaceBetween row: [Heading level="1" + Badge] | [ActionDropdown]
|
|
52
|
+
Page-level feedback Alert conditional — only for inline (non-modal) action feedback
|
|
53
|
+
Modals all ContentDialog modals declared here, controlled by openModal state
|
|
54
|
+
Content Columns template=[1,1] gap="xlarge" collapseBelow="desktop"
|
|
55
|
+
Left column Stack gap="xlarge" — primary record fields + primary sub-tables
|
|
56
|
+
Right column Stack gap="xlarge" — secondary/contextual sections
|
|
57
|
+
SectionCard (per section)
|
|
58
|
+
DataTable PAGE_SIZE=5 items; see data-table/CLAUDE.md
|
|
59
|
+
TablePagination only when total > PAGE_SIZE
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Section 1 — Outer wrapper
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<Stack height="full" paddingX="xlarge" paddingY="xxlarge" gap="xlarge">
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
All spacing uses Spark tokens. This is distinct from list-page spacing
|
|
71
|
+
(`padding="large"` on a neutral-background Stack) — do not mix the two.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Section 2 — Page header
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<Box
|
|
79
|
+
display="flex"
|
|
80
|
+
flexDirection={{ mobile: 'column', tablet: 'row' }}
|
|
81
|
+
justifyContent={{ tablet: 'spaceBetween' }}
|
|
82
|
+
gap="medium"
|
|
83
|
+
>
|
|
84
|
+
<Box
|
|
85
|
+
display="flex"
|
|
86
|
+
flexDirection={{ mobile: 'columnReverse', tablet: 'row' }}
|
|
87
|
+
alignItems={{ mobile: 'start', tablet: 'center' }}
|
|
88
|
+
gap={{ mobile: 'medium', tablet: 'small' }}
|
|
89
|
+
>
|
|
90
|
+
<Heading level="1">{recordTitle}</Heading>
|
|
91
|
+
<Badge tone={statusTone}>{statusLabel}</Badge>
|
|
92
|
+
</Box>
|
|
93
|
+
|
|
94
|
+
{actions.length > 0 && (
|
|
95
|
+
<Box className={css({ minWidth: 130 })}>
|
|
96
|
+
<ActionDropdown label="Actions" actions={actions} />
|
|
97
|
+
</Box>
|
|
98
|
+
)}
|
|
99
|
+
</Box>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Rules:
|
|
103
|
+
|
|
104
|
+
- Status badge follows the heading — never precedes it
|
|
105
|
+
- Only render `ActionDropdown` when `actions.length > 0`
|
|
106
|
+
- No action buttons directly in the header — always use `ActionDropdown`
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Section 3 — Actions dropdown
|
|
111
|
+
|
|
112
|
+
See `packages/action-dropdown/CLAUDE.md` for full API and ordering rules.
|
|
113
|
+
|
|
114
|
+
Page-level decisions:
|
|
115
|
+
|
|
116
|
+
- **Order**: non-destructive actions first, restore-type actions second,
|
|
117
|
+
`tone: 'critical'` destructive actions always last
|
|
118
|
+
- **Hide vs. disable**: hide actions permanently unavailable for the current
|
|
119
|
+
record state (conditional spread); disable actions temporarily unavailable
|
|
120
|
+
(e.g. mutation in-flight)
|
|
121
|
+
- **Direct vs. modal**: direct actions (password reset, restore, activate) call
|
|
122
|
+
the mutation inline and surface feedback via page-level `actionStatus` Alert;
|
|
123
|
+
destructive actions (delete, suspend) always open a `ContentDialog` modal
|
|
124
|
+
first
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Section 4 — Page-level feedback
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
{
|
|
132
|
+
actionStatus && (
|
|
133
|
+
<Alert tone={actionStatus.isSuccessful ? 'positive' : 'critical'}>
|
|
134
|
+
<Text>{actionStatus.message}</Text>
|
|
135
|
+
</Alert>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
State shape: `{ isSuccessful: boolean; message: string } | undefined`
|
|
141
|
+
|
|
142
|
+
Rendered between the header and content columns. Only used for direct inline
|
|
143
|
+
mutations. Modal confirmations own their own error Alert inside the
|
|
144
|
+
`ContentDialog` — see `packages/modal-dialog/CLAUDE.md`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Section 5 — Confirmation modals
|
|
149
|
+
|
|
150
|
+
See `packages/modal-dialog/CLAUDE.md` for ContentDialog API, sizing,
|
|
151
|
+
form-in-modal anatomy, and the full destructive modal pattern.
|
|
152
|
+
|
|
153
|
+
Declare all modals in the component JSX, controlled by a single `openModal`
|
|
154
|
+
state union:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const [openModal, setOpenModal] = useState<'delete' | 'suspend' | null>(null);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
{
|
|
162
|
+
openModal === 'delete' && recordId && (
|
|
163
|
+
<DeleteModal
|
|
164
|
+
isOpen
|
|
165
|
+
onToggle={() => setOpenModal(null)}
|
|
166
|
+
recordId={recordId}
|
|
167
|
+
onSuccess={() => {
|
|
168
|
+
refetch();
|
|
169
|
+
setOpenModal(null);
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Section 6 — Content columns
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<Columns gap="xlarge" template={[1, 1]} collapseBelow="desktop">
|
|
182
|
+
<Stack gap="xlarge">{/* left column */}</Stack>
|
|
183
|
+
<Stack gap="xlarge">{/* right column */}</Stack>
|
|
184
|
+
</Columns>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Always equal columns (`template={[1, 1]}`), always collapse below desktop.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Section 7 — Section cards
|
|
192
|
+
|
|
193
|
+
Each content section is wrapped in a `SectionCard`.
|
|
194
|
+
|
|
195
|
+
**Portal-hub uses a custom wrapper** at `@components/PortalTable/SectionCard`
|
|
196
|
+
(not `@spark-web/section-card`). The API differs:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import { SectionCard } from '@components/PortalTable/SectionCard';
|
|
200
|
+
|
|
201
|
+
<SectionCard label="Section Title">{/* section content */}</SectionCard>;
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| Prop | Type | Notes |
|
|
205
|
+
| ---------- | ----------- | ------------------------------------- |
|
|
206
|
+
| `label` | `string` | Required — card header text |
|
|
207
|
+
| `action` | `ReactNode` | Optional — right-side header control |
|
|
208
|
+
| `controls` | `ReactNode` | Optional — additional header controls |
|
|
209
|
+
|
|
210
|
+
Return `null` for sections conditionally hidden — never render an empty card.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Section 8 — Section data tables
|
|
215
|
+
|
|
216
|
+
See `packages/data-table/CLAUDE.md` for DataTable API, column definitions, and
|
|
217
|
+
loading/empty state props.
|
|
218
|
+
|
|
219
|
+
Detail-page-specific rules (differ from list pages):
|
|
220
|
+
|
|
221
|
+
- **`PAGE_SIZE = 5`** — always 5 items per section table (not 20 like list
|
|
222
|
+
pages)
|
|
223
|
+
- **Pagination threshold**: render `TablePagination` only when
|
|
224
|
+
`total > PAGE_SIZE`
|
|
225
|
+
- **Reset page**: reset to 1 when the record context changes (e.g. `userId`)
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
const PAGE_SIZE = 5;
|
|
229
|
+
const [page, setPage] = useState(1);
|
|
230
|
+
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
setPage(1);
|
|
233
|
+
}, [recordId]);
|
|
234
|
+
|
|
235
|
+
// Client-side pagination — fetch all, slice
|
|
236
|
+
const allItems = data?.items ?? [];
|
|
237
|
+
const total = allItems.length;
|
|
238
|
+
const pageItems = allItems.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
|
239
|
+
|
|
240
|
+
// Server-side pagination — skip/take API
|
|
241
|
+
const { data } = useQuery({ skip: (page - 1) * PAGE_SIZE, take: PAGE_SIZE });
|
|
242
|
+
const total = countData?.count ?? 0;
|
|
243
|
+
const pageItems = data?.items ?? [];
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
<Stack gap="large">
|
|
248
|
+
<DataTable
|
|
249
|
+
items={pageItems}
|
|
250
|
+
columns={columns}
|
|
251
|
+
isLoading={isLoading}
|
|
252
|
+
emptyState={
|
|
253
|
+
<Text tone="muted" size="small" align="center">
|
|
254
|
+
No items.
|
|
255
|
+
</Text>
|
|
256
|
+
}
|
|
257
|
+
/>
|
|
258
|
+
{total > PAGE_SIZE && (
|
|
259
|
+
<TablePagination
|
|
260
|
+
total={total}
|
|
261
|
+
pageSize={PAGE_SIZE}
|
|
262
|
+
dataShowing={pageItems.length}
|
|
263
|
+
onChange={setPage}
|
|
264
|
+
current={page}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
</Stack>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Section 9 — Tabbed sections
|
|
273
|
+
|
|
274
|
+
See `packages/tabs/CLAUDE.md` for the Tabs API, dynamic tab construction, the
|
|
275
|
+
required internal-admin background override, and the null guard pattern.
|
|
276
|
+
|
|
277
|
+
Use tabs when a section has multiple sub-views (e.g. Email / SMS history). Each
|
|
278
|
+
tab panel follows the same PAGE_SIZE=5 and pagination rules as Section 8.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Structural skeleton
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
<Stack height="full" paddingX="xlarge" paddingY="xxlarge" gap="xlarge">
|
|
286
|
+
{/* Header */}
|
|
287
|
+
<Box
|
|
288
|
+
display="flex"
|
|
289
|
+
flexDirection={{ mobile: 'column', tablet: 'row' }}
|
|
290
|
+
justifyContent={{ tablet: 'spaceBetween' }}
|
|
291
|
+
gap="medium"
|
|
292
|
+
>
|
|
293
|
+
<Box
|
|
294
|
+
display="flex"
|
|
295
|
+
flexDirection={{ mobile: 'columnReverse', tablet: 'row' }}
|
|
296
|
+
alignItems={{ mobile: 'start', tablet: 'center' }}
|
|
297
|
+
gap={{ mobile: 'medium', tablet: 'small' }}
|
|
298
|
+
>
|
|
299
|
+
<Heading level="1">{recordTitle}</Heading>
|
|
300
|
+
<Badge tone={statusTone}>{statusLabel}</Badge>
|
|
301
|
+
</Box>
|
|
302
|
+
{actions.length > 0 && (
|
|
303
|
+
<Box className={css({ minWidth: 130 })}>
|
|
304
|
+
<ActionDropdown label="Actions" actions={actions} />
|
|
305
|
+
</Box>
|
|
306
|
+
)}
|
|
307
|
+
</Box>
|
|
308
|
+
|
|
309
|
+
{/* Page-level feedback — direct actions only */}
|
|
310
|
+
{actionStatus && (
|
|
311
|
+
<Alert tone={actionStatus.isSuccessful ? 'positive' : 'critical'}>
|
|
312
|
+
<Text>{actionStatus.message}</Text>
|
|
313
|
+
</Alert>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* Modals */}
|
|
317
|
+
{openModal === 'delete' && recordId && (
|
|
318
|
+
<DeleteModal
|
|
319
|
+
isOpen
|
|
320
|
+
onToggle={() => setOpenModal(null)}
|
|
321
|
+
recordId={recordId}
|
|
322
|
+
onSuccess={() => {
|
|
323
|
+
refetch();
|
|
324
|
+
setOpenModal(null);
|
|
325
|
+
}}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{/* Content */}
|
|
330
|
+
<Columns gap="xlarge" template={[1, 1]} collapseBelow="desktop">
|
|
331
|
+
<Stack gap="xlarge">
|
|
332
|
+
<SectionCard label="Details">{/* fields */}</SectionCard>
|
|
333
|
+
</Stack>
|
|
334
|
+
<Stack gap="xlarge">
|
|
335
|
+
<SectionCard label="History">{/* table + pagination */}</SectionCard>
|
|
336
|
+
</Stack>
|
|
337
|
+
</Columns>
|
|
338
|
+
</Stack>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Do NOTs
|
|
344
|
+
|
|
345
|
+
- NEVER apply list-page spacing to a detail page — outer wrapper uses
|
|
346
|
+
`paddingX="xlarge" paddingY="xxlarge"`, not `padding="large"`
|
|
347
|
+
- NEVER place a destructive action before non-destructive actions in the
|
|
348
|
+
dropdown
|
|
349
|
+
- NEVER call a destructive mutation directly from a dropdown item — open a modal
|
|
350
|
+
- NEVER surface modal errors as page-level Alerts — see `modal-dialog/CLAUDE.md`
|
|
351
|
+
- NEVER use `PAGE_SIZE = 20` on section tables — always 5 on detail pages
|
|
352
|
+
- NEVER render `TablePagination` when `total <= PAGE_SIZE`
|
|
353
|
+
- NEVER render an empty `SectionCard` for hidden sections — return `null`
|
|
354
|
+
- NEVER render `ActionDropdown` when `actions` is empty — gate with
|
|
355
|
+
`actions.length > 0`
|
|
356
|
+
- NEVER render `Tabs` inside `SectionCard` without the background override — see
|
|
357
|
+
`tabs/CLAUDE.md`
|
|
358
|
+
- NEVER use `Container` as the outer page wrapper
|