@tailor-platform/app-shell 1.13.0 → 1.15.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/LICENSE.md +21 -0
- package/README.md +1 -1
- package/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +1215 -319
- package/dist/app-shell.js +4264 -2975
- package/dist/{appshell-CSQWUk6q.js → appshell-DLVBFo5J.js} +731 -543
- package/dist/style-entry.d.ts +110 -110
- package/dist/testing.d.ts +110 -110
- package/dist/testing.js +1 -1
- package/dist/themes/_template.css +1 -1
- package/dist/vite-plugin.d.ts +117 -111
- package/dist/vite-plugin.js +1 -1
- package/package.json +8 -8
- package/skills/app-shell-patterns/SKILL.md +15 -11
- package/skills/app-shell-patterns/references/fundamental/components.md +142 -24
- package/skills/app-shell-patterns/references/fundamental/design-system.md +24 -17
- package/skills/app-shell-patterns/references/fundamental/graphql.md +1 -1
- package/skills/app-shell-patterns/references/pages/detail.md +1148 -0
- package/skills/app-shell-patterns/references/patterns/form-composer.md +2 -2
- package/skills/app-shell-patterns/references/patterns/form-modal.md +84 -30
- package/skills/app-shell-patterns/references/patterns/form-sectioned.md +136 -64
- package/skills/app-shell-patterns/references/patterns/form-single-page.md +57 -25
- package/skills/app-shell-patterns/references/patterns/form-wizard.md +157 -69
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +1 -1
- package/skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md +0 -201
|
@@ -4,15 +4,16 @@ name: Wizard Form
|
|
|
4
4
|
category: pattern
|
|
5
5
|
subcategory: form
|
|
6
6
|
description: Multi-stage create flow with 3-7 steps and per-step validation gates
|
|
7
|
-
requiredImports: [Layout, Card, Form, Fieldset, Field,
|
|
8
|
-
tags: [form, wizard, multi-step,
|
|
7
|
+
requiredImports: [Layout, Card, Badge, Form, Fieldset, Field, Select, Button]
|
|
8
|
+
tags: [form, wizard, multi-step, stepper, onboarding]
|
|
9
9
|
do:
|
|
10
10
|
- Multi-stage Create with 3-7 steps
|
|
11
|
-
-
|
|
11
|
+
- Onboarding and request flows where users should focus on one step at a time
|
|
12
12
|
- Per-step validation gates progression
|
|
13
13
|
dont:
|
|
14
14
|
- Single screen of fields — use form/modal or form/single-page
|
|
15
15
|
- More than 7 steps — split into separate routed pages or reduce scope
|
|
16
|
+
- CSV/spreadsheet import — use the CsvImporter component, not a hand-built wizard
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
# pattern/form/wizard
|
|
@@ -20,102 +21,166 @@ dont:
|
|
|
20
21
|
## When to Use
|
|
21
22
|
|
|
22
23
|
- Multi-stage Create with 3–7 steps
|
|
23
|
-
-
|
|
24
|
+
- Onboarding and request flows where users should focus on one step at a time
|
|
24
25
|
- Per-step validation gates progression
|
|
25
26
|
|
|
27
|
+
For CSV/spreadsheet import specifically, use the `CsvImporter` component — it already implements
|
|
28
|
+
the upload → map → validate → confirm flow. Don't rebuild it here.
|
|
29
|
+
|
|
26
30
|
## Page Implementation
|
|
27
31
|
|
|
28
32
|
```tsx
|
|
29
33
|
/* pattern: form/wizard */
|
|
30
34
|
import { useState } from "react";
|
|
31
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
Badge,
|
|
37
|
+
Button,
|
|
38
|
+
Card,
|
|
39
|
+
Field,
|
|
40
|
+
Fieldset,
|
|
41
|
+
Form,
|
|
42
|
+
Layout,
|
|
43
|
+
Select,
|
|
44
|
+
} from "@tailor-platform/app-shell";
|
|
45
|
+
|
|
46
|
+
const OWNERS = ["Tanaka", "Sato", "Suzuki", "Yamada"];
|
|
47
|
+
const STEPS = ["Basic info", "Assignment", "Schedule", "Review"] as const;
|
|
48
|
+
|
|
49
|
+
type Draft = {
|
|
50
|
+
title: string;
|
|
51
|
+
description: string;
|
|
52
|
+
owner: string;
|
|
53
|
+
startDate: string;
|
|
54
|
+
estimate: string;
|
|
55
|
+
};
|
|
32
56
|
|
|
33
|
-
const
|
|
57
|
+
const INITIAL: Draft = {
|
|
58
|
+
title: "",
|
|
59
|
+
description: "",
|
|
60
|
+
owner: "",
|
|
61
|
+
startDate: "",
|
|
62
|
+
estimate: "",
|
|
63
|
+
};
|
|
34
64
|
|
|
35
65
|
type Props = {
|
|
36
|
-
onComplete: () => void;
|
|
66
|
+
onComplete: (draft: Draft) => void;
|
|
67
|
+
onCancel: () => void;
|
|
37
68
|
};
|
|
38
69
|
|
|
39
|
-
export default function WizardForm({ onComplete }: Props) {
|
|
40
|
-
const [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
export default function WizardForm({ onComplete, onCancel }: Props) {
|
|
71
|
+
const [step, setStep] = useState(0);
|
|
72
|
+
// Accumulated values. Each step's Form unmounts on navigation, so the draft
|
|
73
|
+
// is what makes Back non-destructive — fields re-read it via `defaultValue`.
|
|
74
|
+
const [draft, setDraft] = useState<Draft>(INITIAL);
|
|
75
|
+
|
|
76
|
+
const isLastStep = step === STEPS.length - 1;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Each step is its own `Form`, and "Next" is a `type="submit"` button.
|
|
80
|
+
* `onFormSubmit` fires only after that step's fields pass validation, so
|
|
81
|
+
* progression is gated natively — no manual per-step validity check, and
|
|
82
|
+
* no deferring every error to the final submit.
|
|
83
|
+
*/
|
|
84
|
+
const handleStepSubmit = (values: Record<string, unknown>) => {
|
|
85
|
+
const merged = { ...draft, ...(values as Partial<Draft>) };
|
|
86
|
+
setDraft(merged);
|
|
87
|
+
if (isLastStep) {
|
|
88
|
+
onComplete(merged);
|
|
89
|
+
return;
|
|
53
90
|
}
|
|
91
|
+
setStep(step + 1);
|
|
54
92
|
};
|
|
55
93
|
|
|
56
94
|
return (
|
|
57
95
|
<Layout>
|
|
58
|
-
<Layout.Header title="
|
|
96
|
+
<Layout.Header title="Create task" />
|
|
59
97
|
<Layout.Column>
|
|
60
98
|
<Card.Root>
|
|
61
99
|
<Card.Content>
|
|
62
100
|
<div className="flex items-center gap-2">
|
|
63
|
-
{STEPS.map((
|
|
101
|
+
{STEPS.map((label, i) => (
|
|
64
102
|
<Badge
|
|
65
|
-
key={
|
|
66
|
-
variant={i ===
|
|
103
|
+
key={label}
|
|
104
|
+
variant={i === step ? "default" : i < step ? "success" : "neutral"}
|
|
67
105
|
>
|
|
68
|
-
{i + 1}. {
|
|
106
|
+
{i + 1}. {label}
|
|
69
107
|
</Badge>
|
|
70
108
|
))}
|
|
71
109
|
</div>
|
|
72
110
|
</Card.Content>
|
|
73
111
|
</Card.Root>
|
|
74
112
|
|
|
75
|
-
<
|
|
76
|
-
<Card.
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<Field.
|
|
113
|
+
<Form key={step} noValidate onFormSubmit={handleStepSubmit}>
|
|
114
|
+
<Card.Root>
|
|
115
|
+
<Card.Header title={STEPS[step]} />
|
|
116
|
+
<Card.Content>
|
|
117
|
+
{step === 0 && (
|
|
118
|
+
<Fieldset.Root className="space-y-4">
|
|
119
|
+
<Field.Root name="title">
|
|
120
|
+
<Field.Label>Title</Field.Label>
|
|
121
|
+
<Field.Control required defaultValue={draft.title} />
|
|
122
|
+
<Field.Error match="valueMissing">Title is required.</Field.Error>
|
|
123
|
+
</Field.Root>
|
|
124
|
+
<Field.Root name="description">
|
|
125
|
+
<Field.Label>Description</Field.Label>
|
|
126
|
+
<Field.Control defaultValue={draft.description} />
|
|
127
|
+
</Field.Root>
|
|
128
|
+
</Fieldset.Root>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{step === 1 && (
|
|
132
|
+
<Field.Root name="owner">
|
|
133
|
+
<Field.Label>Owner</Field.Label>
|
|
134
|
+
{/* Uncontrolled like every other field — `defaultValue`
|
|
135
|
+
restores the prior choice when the user steps Back. */}
|
|
136
|
+
<Select
|
|
137
|
+
items={OWNERS}
|
|
138
|
+
defaultValue={draft.owner || null}
|
|
139
|
+
placeholder="Assign owner"
|
|
140
|
+
/>
|
|
82
141
|
</Field.Root>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{step === 2 && (
|
|
145
|
+
<Fieldset.Root className="grid gap-4 md:grid-cols-2">
|
|
146
|
+
<Field.Root name="startDate">
|
|
147
|
+
<Field.Label>Start date</Field.Label>
|
|
148
|
+
<Field.Control type="date" required defaultValue={draft.startDate} />
|
|
149
|
+
<Field.Error match="valueMissing">Start date is required.</Field.Error>
|
|
150
|
+
</Field.Root>
|
|
151
|
+
<Field.Root name="estimate">
|
|
152
|
+
<Field.Label>Estimate (hours)</Field.Label>
|
|
153
|
+
<Field.Control type="number" min={0} defaultValue={draft.estimate} />
|
|
154
|
+
</Field.Root>
|
|
155
|
+
</Fieldset.Root>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{step === 3 && (
|
|
159
|
+
<dl className="grid grid-cols-2 gap-2 text-sm">
|
|
160
|
+
<dt className="text-muted-foreground">Title</dt>
|
|
161
|
+
<dd>{draft.title}</dd>
|
|
162
|
+
<dt className="text-muted-foreground">Owner</dt>
|
|
163
|
+
<dd>{draft.owner || "—"}</dd>
|
|
164
|
+
<dt className="text-muted-foreground">Start date</dt>
|
|
165
|
+
<dd>{draft.startDate || "—"}</dd>
|
|
166
|
+
<dt className="text-muted-foreground">Estimate</dt>
|
|
167
|
+
<dd>{draft.estimate ? `${draft.estimate}h` : "—"}</dd>
|
|
168
|
+
</dl>
|
|
169
|
+
)}
|
|
170
|
+
</Card.Content>
|
|
171
|
+
</Card.Root>
|
|
172
|
+
|
|
173
|
+
<div className="flex justify-between pt-4">
|
|
174
|
+
<Button
|
|
175
|
+
type="button"
|
|
176
|
+
variant="ghost"
|
|
177
|
+
onClick={() => (step === 0 ? onCancel() : setStep(step - 1))}
|
|
178
|
+
>
|
|
179
|
+
{step === 0 ? "Cancel" : "Back"}
|
|
180
|
+
</Button>
|
|
181
|
+
<Button type="submit">{isLastStep ? "Create" : "Next"}</Button>
|
|
182
|
+
</div>
|
|
183
|
+
</Form>
|
|
119
184
|
</Layout.Column>
|
|
120
185
|
</Layout>
|
|
121
186
|
);
|
|
@@ -128,9 +193,32 @@ export default function WizardForm({ onComplete }: Props) {
|
|
|
128
193
|
- Back-navigation must preserve prior step's input
|
|
129
194
|
- Validation must be per-step — don't defer until final submit
|
|
130
195
|
- Step indicator collapses to "Step 2 of 4" label below 1024px
|
|
196
|
+
- One `<Form>` rendered per step, keyed by step index so it remounts cleanly
|
|
197
|
+
- Accumulated values live in a `draft` state object above the `Form`; each step's fields read
|
|
198
|
+
their initial value from it via `defaultValue`
|
|
199
|
+
|
|
200
|
+
## Form state
|
|
201
|
+
|
|
202
|
+
Rules that apply to every `form/*` pattern are in **`components.md`** → Forms, with a worked example
|
|
203
|
+
in **`form/modal`** → Form state: `Form` + `Field` is the default stack, submit via `onFormSubmit`,
|
|
204
|
+
dropdowns need only a wrapping `Field.Root` (no `name`, no state), server errors route through
|
|
205
|
+
`errors`, and React Hook Form is an optional escape hatch.
|
|
206
|
+
|
|
207
|
+
The wizard's specific mechanic: **make "Next" a `type="submit"` button.** `onFormSubmit` fires only
|
|
208
|
+
after the current step's fields pass validation, so progression is gated natively — no manual
|
|
209
|
+
validity check, and no deferring errors to the final submit. The handler merges that step's values
|
|
210
|
+
into `draft` and advances; on the last step it calls the completion callback instead.
|
|
211
|
+
|
|
212
|
+
Because each step's `Form` unmounts on navigation, values must be lifted into `draft` — that is
|
|
213
|
+
what makes Back non-destructive. Every field, dropdowns included, is uncontrolled and re-reads its
|
|
214
|
+
prior value from `draft` via `defaultValue`; nothing needs an `onChange`.
|
|
131
215
|
|
|
132
216
|
## Anti-patterns
|
|
133
217
|
|
|
134
218
|
- More than 7 steps — users lose context and abandon
|
|
135
219
|
- No back-navigation preservation — pressing Back loses prior step's input
|
|
136
220
|
- Validation deferred until final submit — failures force full re-traversal
|
|
221
|
+
- A plain `onClick` "Next" that advances without validating — use `type="submit"` and let
|
|
222
|
+
`onFormSubmit` gate it
|
|
223
|
+
- Wrapping all steps in one `<Form>` and hiding inactive ones — hidden required fields block submit
|
|
224
|
+
- Hand-building a CSV import wizard — use `CsvImporter`
|
|
@@ -87,7 +87,7 @@ export default function MultiSelect({ orders, onArchive, onExport }: Props) {
|
|
|
87
87
|
<Table.Root>
|
|
88
88
|
<Table.Header>
|
|
89
89
|
<Table.Row>
|
|
90
|
-
<Table.Head className="
|
|
90
|
+
<Table.Head className="w-10">
|
|
91
91
|
<input
|
|
92
92
|
type="checkbox"
|
|
93
93
|
checked={selectedIds.size === orders.length && orders.length > 0}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
slug: pattern/detail/hero-with-actions
|
|
3
|
-
name: Hero With Actions Detail
|
|
4
|
-
category: pattern
|
|
5
|
-
subcategory: detail
|
|
6
|
-
description: Single-record detail view with workflow actions and activity timeline
|
|
7
|
-
requiredImports:
|
|
8
|
-
[
|
|
9
|
-
Layout,
|
|
10
|
-
Badge,
|
|
11
|
-
Button,
|
|
12
|
-
Menu,
|
|
13
|
-
DescriptionCard,
|
|
14
|
-
Card,
|
|
15
|
-
Table,
|
|
16
|
-
ActionPanel,
|
|
17
|
-
ActivityCard,
|
|
18
|
-
Grid,
|
|
19
|
-
MetricCard,
|
|
20
|
-
DocumentProgressCard,
|
|
21
|
-
Alert,
|
|
22
|
-
]
|
|
23
|
-
tags: [detail, actions, timeline, workflow, two-column]
|
|
24
|
-
do:
|
|
25
|
-
- Single-record detail view (Order #1234, Supplier ABC, Product SKU-42)
|
|
26
|
-
- Record has a workflow with named actions (Approve, Cancel, Ship)
|
|
27
|
-
- An activity/audit history is meaningful to surface
|
|
28
|
-
dont:
|
|
29
|
-
- The page primarily compares two grids or record sets (match/reconcile)
|
|
30
|
-
- The record is purely informational with no actions and no history — use list/dense-scan row click into a sheet instead
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
# pattern/detail/hero-with-actions
|
|
34
|
-
|
|
35
|
-
## When to Use
|
|
36
|
-
|
|
37
|
-
- Single-record detail view (Order #1234, Supplier ABC, Product SKU-42)
|
|
38
|
-
- Record has a workflow with named actions (Approve, Cancel, Ship)
|
|
39
|
-
- An activity/audit history is meaningful to surface
|
|
40
|
-
|
|
41
|
-
## Layout
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
+---------------------------------------------------------+
|
|
45
|
-
| Layout.Header title [Badge] [Primary CTA] |
|
|
46
|
-
+----------------------------------+----------------------+
|
|
47
|
-
| Layout.Column (main) | Layout.Column right |
|
|
48
|
-
| DescriptionCard Summary | ActionPanel |
|
|
49
|
-
| - field : value | - Approve |
|
|
50
|
-
| - field : value | - Cancel |
|
|
51
|
-
| Card Attachments | |
|
|
52
|
-
| - file rows | ActivityCard |
|
|
53
|
-
| Card Line items | - history entry |
|
|
54
|
-
| inline Table.Root | - history entry |
|
|
55
|
-
| | (…N more side cards |
|
|
56
|
-
| | per use case) |
|
|
57
|
-
+----------------------------------+----------------------+
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Composition rules:
|
|
61
|
-
|
|
62
|
-
- Both columns accept N cards based on the use case — add as many sections as the record requires.
|
|
63
|
-
- Each main-column section sits in a `Card` container — except `DescriptionCard`, which already provides its own container and must not be double-wrapped in a `Card`.
|
|
64
|
-
- Right-column cards (`ActionPanel`, `ActivityCard`, related summaries, etc.) are composed the same way. `ActionPanel` and `ActivityCard` are the most common; neither is mandatory.
|
|
65
|
-
|
|
66
|
-
Responsive: <1024 collapses to single column with right column rendered below main; 1024–1280 two columns with narrow side; >1280 two columns at full width.
|
|
67
|
-
|
|
68
|
-
## Layout variants
|
|
69
|
-
|
|
70
|
-
Choose the column structure from the **record** — the pattern is "hero + workflow + (optional) history," and the layout flexes. The skill never mandates a fixed column count.
|
|
71
|
-
|
|
72
|
-
- **Main + right rail** (most common) — the record has workflow actions and/or activity/related summaries → right `Layout.Column area="right"` holds `ActionPanel`, `ActivityCard`, integration cards.
|
|
73
|
-
- **Single main column** — the record is mostly descriptive with few/no workflow actions and no meaningful history → one `Layout.Column`. Don't invent a rail just to fill space.
|
|
74
|
-
- **Main + left rail** (`area="left"`, wider 320px) — when a persistent section index / summary should lead the reading order.
|
|
75
|
-
- Either column holds **N cards** as the record requires.
|
|
76
|
-
|
|
77
|
-
## Optional cards
|
|
78
|
-
|
|
79
|
-
Add these only when the record calls for them — none is mandatory. Each is a card like any other in the columns above.
|
|
80
|
-
|
|
81
|
-
- **Metric strip** — when the record has headline KPIs (totals, counts), lead the main column with `MetricCard`s wrapped in a `Grid` (never one per row). See the `Grid` and `MetricCard` entries in `components.md`.
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
<Grid columns={{ initial: 1, md: 2, xl: 4 }} gap={4}>
|
|
85
|
-
<MetricCard title="Net total" value="$12,480" trend={{ direction: "up", value: "+4%" }} />
|
|
86
|
-
<MetricCard title="Lines" value="18" />
|
|
87
|
-
<MetricCard title="Received" value="11" />
|
|
88
|
-
<MetricCard title="Outstanding" value="7" />
|
|
89
|
-
</Grid>
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
- **`DocumentProgressCard`** — when the record has a fulfilment / lifecycle breakdown (received vs returned vs pending, shipment status). Sits in the main column or the right rail; derive `percent` and `segments` in the consumer.
|
|
93
|
-
- **`Alert`** — a record-level inline banner (blocking error, on-hold warning) above the columns; use the **error** variant for the pattern's required error state. Transient feedback uses `useToast`, not `Alert`.
|
|
94
|
-
|
|
95
|
-
## Page Implementation
|
|
96
|
-
|
|
97
|
-
```tsx
|
|
98
|
-
/* pattern: detail/hero-with-actions */
|
|
99
|
-
import {
|
|
100
|
-
Layout,
|
|
101
|
-
Badge,
|
|
102
|
-
Button,
|
|
103
|
-
Card,
|
|
104
|
-
DescriptionCard,
|
|
105
|
-
Table,
|
|
106
|
-
ActionPanel,
|
|
107
|
-
ActivityCard,
|
|
108
|
-
} from "@tailor-platform/app-shell";
|
|
109
|
-
import type { Order } from "./mock";
|
|
110
|
-
|
|
111
|
-
type Props = {
|
|
112
|
-
order: Order;
|
|
113
|
-
onApprove: () => void;
|
|
114
|
-
onCancel: () => void;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
export default function HeroWithActionsDetail({ order, onApprove, onCancel }: Props) {
|
|
118
|
-
return (
|
|
119
|
-
<Layout>
|
|
120
|
-
<Layout.Header
|
|
121
|
-
title={`Order ${order.number}`}
|
|
122
|
-
actions={[
|
|
123
|
-
<Badge key="status" variant="success">
|
|
124
|
-
{order.status}
|
|
125
|
-
</Badge>,
|
|
126
|
-
<Button key="edit">Edit</Button>,
|
|
127
|
-
]}
|
|
128
|
-
/>
|
|
129
|
-
<Layout.Column>
|
|
130
|
-
<DescriptionCard
|
|
131
|
-
title="Summary"
|
|
132
|
-
data={order}
|
|
133
|
-
columns={3}
|
|
134
|
-
fields={[
|
|
135
|
-
{ key: "number", label: "Order number" },
|
|
136
|
-
{
|
|
137
|
-
key: "status",
|
|
138
|
-
label: "Status",
|
|
139
|
-
type: "badge",
|
|
140
|
-
meta: { badgeVariantMap: { Confirmed: "success", Draft: "neutral" } },
|
|
141
|
-
},
|
|
142
|
-
{ key: "customer", label: "Customer" },
|
|
143
|
-
{ key: "total", label: "Total" },
|
|
144
|
-
]}
|
|
145
|
-
/>
|
|
146
|
-
<Card.Root>
|
|
147
|
-
<Card.Header title="Line items" />
|
|
148
|
-
<Card.Content className="astw:px-0">
|
|
149
|
-
<Table.Root containerClassName="astw:px-6">
|
|
150
|
-
<Table.Header>
|
|
151
|
-
<Table.Row>
|
|
152
|
-
<Table.Head>SKU</Table.Head>
|
|
153
|
-
<Table.Head align="right">Qty</Table.Head>
|
|
154
|
-
<Table.Head align="right">Total</Table.Head>
|
|
155
|
-
</Table.Row>
|
|
156
|
-
</Table.Header>
|
|
157
|
-
<Table.Body>
|
|
158
|
-
{order.lineItems.map((item) => (
|
|
159
|
-
<Table.Row key={item.id}>
|
|
160
|
-
<Table.Cell>{item.sku}</Table.Cell>
|
|
161
|
-
<Table.Cell align="right">{item.qty}</Table.Cell>
|
|
162
|
-
<Table.Cell align="right">${item.total.toLocaleString()}</Table.Cell>
|
|
163
|
-
</Table.Row>
|
|
164
|
-
))}
|
|
165
|
-
</Table.Body>
|
|
166
|
-
</Table.Root>
|
|
167
|
-
</Card.Content>
|
|
168
|
-
</Card.Root>
|
|
169
|
-
</Layout.Column>
|
|
170
|
-
<Layout.Column area="right">
|
|
171
|
-
<ActionPanel
|
|
172
|
-
title="Actions"
|
|
173
|
-
actions={[
|
|
174
|
-
{ key: "approve", label: "Approve", icon: <span>✓</span>, onClick: onApprove },
|
|
175
|
-
{ key: "cancel", label: "Cancel", icon: <span>✕</span>, onClick: onCancel },
|
|
176
|
-
]}
|
|
177
|
-
/>
|
|
178
|
-
<ActivityCard title="Activity" items={order.activities} />
|
|
179
|
-
</Layout.Column>
|
|
180
|
-
</Layout>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## Constraints
|
|
186
|
-
|
|
187
|
-
- **Header carries the single primary CTA + the status `Badge`** — not workflow actions. Workflow actions (Approve, Reject, Archive, …) live in the `ActionPanel`. If the header needs more than ~2 actions, move the overflow into a `Menu`.
|
|
188
|
-
- **Never duplicate an action** across `Layout.Header` and `ActionPanel` — each action has exactly one home.
|
|
189
|
-
- Every content section MUST sit inside `Card.Root` (or `DescriptionCard`, which already self-contains). Raw divs are not allowed.
|
|
190
|
-
- `ActionPanel` is workflow-only — never back-navigation (that lives in the breadcrumb).
|
|
191
|
-
- `Table.Root` inside a Card requires `containerClassName="astw:px-6"`.
|
|
192
|
-
|
|
193
|
-
## Anti-patterns
|
|
194
|
-
|
|
195
|
-
- No status `Badge` on stateful entities — users can't tell where the record is in its lifecycle.
|
|
196
|
-
- `ActionPanel` mixed with metadata in the same card — keep workflow separate from descriptive fields.
|
|
197
|
-
- Bare `<div>` sections in the main column — every content section MUST sit inside `Card.Root`.
|
|
198
|
-
- `ActionPanel` containing back-navigation (e.g. "Back to Product List") — back navigation lives in `Layout.Header`'s breadcrumb.
|
|
199
|
-
- A `Table.Root` inside a Card without `containerClassName="astw:px-6"` — the first column lands flush against the card edge.
|
|
200
|
-
- The same action in both `Layout.Header` and the `ActionPanel` — duplicating it makes neither read as canonical. Pick one home.
|
|
201
|
-
- Giving every status the same loud weight — the record's primary/lifecycle status is a **filled** semantic badge; secondary statuses (fulfilment, billing) use **`outline-*`** (see `design-system.md` → Composition & emphasis rules).
|