@tailor-platform/app-shell 1.2.0 → 1.3.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.
@@ -0,0 +1,157 @@
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
+ [Layout, Badge, Button, Menu, DescriptionCard, Card, Table, ActionPanel, ActivityCard]
9
+ tags: [detail, actions, timeline, workflow, two-column]
10
+ do:
11
+ - Single-record detail view (Order #1234, Supplier ABC, Product SKU-42)
12
+ - Record has a workflow with named actions (Approve, Cancel, Ship)
13
+ - An activity/audit history is meaningful to surface
14
+ dont:
15
+ - The page primarily compares two grids or record sets (match/reconcile)
16
+ - The record is purely informational with no actions and no history — use list/dense-scan row click into a sheet instead
17
+ ---
18
+
19
+ # pattern/detail/hero-with-actions
20
+
21
+ ## When to Use
22
+
23
+ - Single-record detail view (Order #1234, Supplier ABC, Product SKU-42)
24
+ - Record has a workflow with named actions (Approve, Cancel, Ship)
25
+ - An activity/audit history is meaningful to surface
26
+
27
+ ## Layout
28
+
29
+ ```
30
+ +---------------------------------------------------------+
31
+ | Layout.Header title [Badge] [Primary CTA] |
32
+ +----------------------------------+----------------------+
33
+ | Layout.Column (main) | Layout.Column right |
34
+ | DescriptionCard Summary | ActionPanel |
35
+ | - field : value | - Approve |
36
+ | - field : value | - Cancel |
37
+ | Card Attachments | |
38
+ | - file rows | ActivityCard |
39
+ | Card Line items | - history entry |
40
+ | inline Table.Root | - history entry |
41
+ | | (…N more side cards |
42
+ | | per use case) |
43
+ +----------------------------------+----------------------+
44
+ ```
45
+
46
+ Composition rules:
47
+
48
+ - Both columns accept N cards based on the use case — add as many sections as the record requires.
49
+ - 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`.
50
+ - Right-column cards (`ActionPanel`, `ActivityCard`, related summaries, etc.) are composed the same way. `ActionPanel` and `ActivityCard` are the most common; neither is mandatory.
51
+
52
+ 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.
53
+
54
+ ## Page Implementation
55
+
56
+ ```tsx
57
+ /* pattern: detail/hero-with-actions */
58
+ import {
59
+ Layout,
60
+ Badge,
61
+ Button,
62
+ Card,
63
+ DescriptionCard,
64
+ Table,
65
+ ActionPanel,
66
+ ActivityCard,
67
+ } from "@tailor-platform/app-shell";
68
+ import type { Order } from "./mock";
69
+
70
+ type Props = {
71
+ order: Order;
72
+ onApprove: () => void;
73
+ onCancel: () => void;
74
+ };
75
+
76
+ export default function HeroWithActionsDetail({ order, onApprove, onCancel }: Props) {
77
+ return (
78
+ <Layout>
79
+ <Layout.Header
80
+ title={`Order ${order.number}`}
81
+ actions={[
82
+ <Badge key="status" variant="success">
83
+ {order.status}
84
+ </Badge>,
85
+ <Button key="edit">Edit</Button>,
86
+ ]}
87
+ />
88
+ <Layout.Column>
89
+ <DescriptionCard
90
+ title="Summary"
91
+ data={order}
92
+ columns={3}
93
+ fields={[
94
+ { key: "number", label: "Order number" },
95
+ {
96
+ key: "status",
97
+ label: "Status",
98
+ type: "badge",
99
+ meta: { badgeVariantMap: { Confirmed: "success", Draft: "neutral" } },
100
+ },
101
+ { key: "customer", label: "Customer" },
102
+ { key: "total", label: "Total" },
103
+ ]}
104
+ />
105
+ <Card.Root>
106
+ <Card.Header title="Line items" />
107
+ <Card.Content className="astw:px-0">
108
+ <Table.Root containerClassName="astw:px-6">
109
+ <Table.Header>
110
+ <Table.Row>
111
+ <Table.Head>SKU</Table.Head>
112
+ <Table.Head>Qty</Table.Head>
113
+ <Table.Head>Total</Table.Head>
114
+ </Table.Row>
115
+ </Table.Header>
116
+ <Table.Body>
117
+ {order.lineItems.map((item) => (
118
+ <Table.Row key={item.id}>
119
+ <Table.Cell>{item.sku}</Table.Cell>
120
+ <Table.Cell>{item.qty}</Table.Cell>
121
+ <Table.Cell>${item.total.toLocaleString()}</Table.Cell>
122
+ </Table.Row>
123
+ ))}
124
+ </Table.Body>
125
+ </Table.Root>
126
+ </Card.Content>
127
+ </Card.Root>
128
+ </Layout.Column>
129
+ <Layout.Column area="right">
130
+ <ActionPanel
131
+ title="Actions"
132
+ actions={[
133
+ { key: "approve", label: "Approve", icon: <span>✓</span>, onClick: onApprove },
134
+ { key: "cancel", label: "Cancel", icon: <span>✕</span>, onClick: onCancel },
135
+ ]}
136
+ />
137
+ <ActivityCard title="Activity" items={order.activities} />
138
+ </Layout.Column>
139
+ </Layout>
140
+ );
141
+ }
142
+ ```
143
+
144
+ ## Constraints
145
+
146
+ - More than 2 primary actions in the header → move overflow into a `Menu`.
147
+ - Every content section MUST sit inside `Card.Root` (or `DescriptionCard`, which already self-contains). Raw divs are not allowed.
148
+ - `ActionPanel` is workflow-only — never back-navigation.
149
+ - `Table.Root` inside a Card requires `containerClassName="astw:px-6"`.
150
+
151
+ ## Anti-patterns
152
+
153
+ - No status `Badge` on stateful entities — users can't tell where the record is in its lifecycle.
154
+ - `ActionPanel` mixed with metadata in the same card — keep workflow separate from descriptive fields.
155
+ - Bare `<div>` sections in the main column — every content section MUST sit inside `Card.Root`.
156
+ - `ActionPanel` containing back-navigation (e.g. "Back to Product List") — back navigation lives in `Layout.Header`'s breadcrumb.
157
+ - A `Table.Root` inside a Card without `containerClassName="astw:px-6"` — the first column lands flush against the card edge.
@@ -0,0 +1,169 @@
1
+ ---
2
+ slug: pattern/form/modal
3
+ name: Modal Form
4
+ category: pattern
5
+ subcategory: form
6
+ description: Default form pattern for Create/Edit — keeps user in context on the parent screen
7
+ requiredImports: [Dialog, Button, Form, Field, Input]
8
+ tags: [form, modal, dialog, create, edit, inline-add]
9
+ do:
10
+ - Default for most Create and Edit forms — keeps user in context on parent screen
11
+ - Inline add of a related entity from another screen (add address from order detail)
12
+ - Quick configuration changes and single-purpose forms (rename, change status)
13
+ - Any form the design hasn't explicitly called out as a full-page routed screen
14
+ dont:
15
+ - Design explicitly calls for a full-page (non-overlay) routed Create or Edit
16
+ - Form is complex with 15+ fields or multiple grouped sections — use form/sectioned
17
+ - Multi-stage flow with per-step validation — use form/wizard
18
+ ---
19
+
20
+ # pattern/form/modal
21
+
22
+ ## When to Use
23
+
24
+ - Default for most Create and Edit forms — keeps user in context on parent screen
25
+ - Inline add of a related entity from another screen (add address from order detail)
26
+ - Quick configuration changes and single-purpose forms (rename, change status)
27
+ - Any form the design hasn't explicitly called out as a full-page routed screen
28
+
29
+ ## Page Implementation
30
+
31
+ ```tsx
32
+ /* pattern: form/modal */
33
+ import { Button, Dialog, Input, Field } from "@tailor-platform/app-shell";
34
+
35
+ type Props = {
36
+ onSave: (data: { label: string; street: string; city: string }) => void;
37
+ };
38
+
39
+ export default function ModalForm({ onSave }: Props) {
40
+ return (
41
+ <Dialog.Root>
42
+ <Dialog.Trigger render={<Button />}>Add address</Dialog.Trigger>
43
+ <Dialog.Content>
44
+ <Dialog.Header>
45
+ <Dialog.Title>Add address</Dialog.Title>
46
+ <Dialog.Description>Add a shipping address to this order.</Dialog.Description>
47
+ </Dialog.Header>
48
+ <form
49
+ onSubmit={(e) => {
50
+ e.preventDefault();
51
+ const formData = new FormData(e.currentTarget);
52
+ onSave({
53
+ label: formData.get("label") as string,
54
+ street: formData.get("street") as string,
55
+ city: formData.get("city") as string,
56
+ });
57
+ }}
58
+ >
59
+ <div className="space-y-4 py-4">
60
+ <Field.Root name="label">
61
+ <Field.Label>Label</Field.Label>
62
+ <Field.Control render={<Input />} />
63
+ </Field.Root>
64
+ <Field.Root name="street">
65
+ <Field.Label>Street</Field.Label>
66
+ <Field.Control render={<Input />} />
67
+ </Field.Root>
68
+ <Field.Root name="city">
69
+ <Field.Label>City</Field.Label>
70
+ <Field.Control render={<Input />} />
71
+ </Field.Root>
72
+ </div>
73
+ <Dialog.Footer>
74
+ <Dialog.Close render={<Button variant="ghost" />}>Cancel</Dialog.Close>
75
+ <Button type="submit">Save</Button>
76
+ </Dialog.Footer>
77
+ </form>
78
+ </Dialog.Content>
79
+ </Dialog.Root>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ## Route-driven Variant
85
+
86
+ ```tsx
87
+ /* pattern: form/modal (route-driven variant) */
88
+ import { Button, Dialog, Input, Layout, Field } from "@tailor-platform/app-shell";
89
+
90
+ type Props = {
91
+ isCreateOpen: boolean;
92
+ onNavigateToCreate: () => void;
93
+ onNavigateToList: () => void;
94
+ onSave: (data: { name: string }) => void;
95
+ };
96
+
97
+ /**
98
+ * Route-driven modal: the form has its own URL but renders as a popup
99
+ * over the list. Both `/products` and `/products/create` render this
100
+ * same component — the parent list stays visible underneath.
101
+ */
102
+ export default function ModalFormRouted({
103
+ isCreateOpen,
104
+ onNavigateToCreate,
105
+ onNavigateToList,
106
+ onSave,
107
+ }: Props) {
108
+ return (
109
+ <Layout>
110
+ <Layout.Header
111
+ title="Products"
112
+ actions={[
113
+ <Button key="create" onClick={onNavigateToCreate}>
114
+ Create
115
+ </Button>,
116
+ ]}
117
+ />
118
+ <Layout.Column>{/* products list — see list/dense-scan */}</Layout.Column>
119
+
120
+ <Dialog.Root
121
+ open={isCreateOpen}
122
+ onOpenChange={(open) => {
123
+ if (!open) onNavigateToList();
124
+ }}
125
+ >
126
+ <Dialog.Content>
127
+ <Dialog.Header>
128
+ <Dialog.Title>Create product</Dialog.Title>
129
+ </Dialog.Header>
130
+ <form
131
+ onSubmit={(e) => {
132
+ e.preventDefault();
133
+ const formData = new FormData(e.currentTarget);
134
+ onSave({ name: formData.get("name") as string });
135
+ }}
136
+ >
137
+ <div className="space-y-4 py-4">
138
+ <Field.Root name="name">
139
+ <Field.Label>Name</Field.Label>
140
+ <Field.Control render={<Input />} />
141
+ </Field.Root>
142
+ </div>
143
+ <Dialog.Footer>
144
+ <Button variant="ghost" onClick={onNavigateToList}>
145
+ Cancel
146
+ </Button>
147
+ <Button type="submit">Save</Button>
148
+ </Dialog.Footer>
149
+ </form>
150
+ </Dialog.Content>
151
+ </Dialog.Root>
152
+ </Layout>
153
+ );
154
+ }
155
+ ```
156
+
157
+ ## Constraints
158
+
159
+ - Dialog renders full-screen sheet below 1024px; centered max-w-md at 1024–1280px
160
+ - Route-driven variant requires both parent path and create/edit path to render the same component
161
+ - `onOpenChange` must navigate back — just calling `setOpen(false)` leaves the URL broken
162
+
163
+ ## Anti-patterns
164
+
165
+ - Nesting modals — opening a Dialog from inside another Dialog
166
+ - Modal containing a wizard — promote to a routed `form/wizard`
167
+ - Save closes the dialog but parent state is stale — wire refetch or optimistic update
168
+ - Building a routed Create/Edit page when the design didn't explicitly call for one — modal is the default
169
+ - Registering the create path as a separate top-level route — that unmounts the parent list
@@ -0,0 +1,126 @@
1
+ ---
2
+ slug: pattern/form/sectioned
3
+ name: Sectioned Form
4
+ category: pattern
5
+ subcategory: form
6
+ description: Complex form with 15+ fields organized into named fieldset sections
7
+ requiredImports: [Layout, Form, Fieldset, Field, Input, Select, Combobox, Button]
8
+ tags: [form, sections, fieldset, settings, complex]
9
+ do:
10
+ - Form is complex with 15+ fields or multiple grouped sections (Identity, Pricing, Inventory)
11
+ - Configure-style settings pages with named boundaries
12
+ dont:
13
+ - Simple Create/Edit — use form/modal (the default)
14
+ - Routed Create/Edit at moderate size with no grouping — use form/single-page
15
+ - Step-gated validation across stages — use form/wizard
16
+ ---
17
+
18
+ # pattern/form/sectioned
19
+
20
+ ## When to Use
21
+
22
+ - Form is complex with 15+ fields or multiple grouped sections (Identity, Pricing, Inventory)
23
+ - Configure-style settings pages with named boundaries
24
+
25
+ ## Page Implementation
26
+
27
+ ```tsx
28
+ /* pattern: form/sectioned */
29
+ import { Button, Layout, Input, Select, Field, Fieldset } from "@tailor-platform/app-shell";
30
+
31
+ type Props = {
32
+ onSave: (data: Record<string, string>) => void;
33
+ onCancel: () => void;
34
+ };
35
+
36
+ export default function SectionedForm({ onSave, onCancel }: Props) {
37
+ return (
38
+ <Layout>
39
+ <Layout.Header
40
+ title="Product settings"
41
+ actions={[
42
+ <Button key="cancel" variant="outline" onClick={onCancel}>
43
+ Cancel
44
+ </Button>,
45
+ <Button key="save" type="submit" form="sectioned-form">
46
+ Save
47
+ </Button>,
48
+ ]}
49
+ />
50
+ <Layout.Column>
51
+ <form
52
+ id="sectioned-form"
53
+ onSubmit={(e) => {
54
+ e.preventDefault();
55
+ const formData = new FormData(e.currentTarget);
56
+ const entries: Record<string, string> = {};
57
+ formData.forEach((value, key) => {
58
+ entries[key] = value as string;
59
+ });
60
+ onSave(entries);
61
+ }}
62
+ className="space-y-8"
63
+ >
64
+ <Fieldset.Root>
65
+ <Fieldset.Legend>Identity</Fieldset.Legend>
66
+ <div className="space-y-4">
67
+ <Field.Root name="name">
68
+ <Field.Label>Name</Field.Label>
69
+ <Field.Control render={<Input />} />
70
+ </Field.Root>
71
+ <Field.Root name="sku">
72
+ <Field.Label>SKU</Field.Label>
73
+ <Field.Control render={<Input />} />
74
+ </Field.Root>
75
+ <Field.Root name="description">
76
+ <Field.Label>Description</Field.Label>
77
+ <Field.Control render={<Input />} />
78
+ </Field.Root>
79
+ </div>
80
+ </Fieldset.Root>
81
+
82
+ <Fieldset.Root>
83
+ <Fieldset.Legend>Pricing</Fieldset.Legend>
84
+ <div className="space-y-4">
85
+ <Field.Root name="price">
86
+ <Field.Label>Price</Field.Label>
87
+ <Field.Control render={<Input type="number" />} />
88
+ </Field.Root>
89
+ <Field.Root name="currency">
90
+ <Field.Label>Currency</Field.Label>
91
+ <Select items={["USD", "EUR", "GBP", "JPY"]} placeholder="Select currency" />
92
+ </Field.Root>
93
+ </div>
94
+ </Fieldset.Root>
95
+
96
+ <Fieldset.Root>
97
+ <Fieldset.Legend>Inventory</Fieldset.Legend>
98
+ <div className="space-y-4">
99
+ <Field.Root name="quantity">
100
+ <Field.Label>Initial quantity</Field.Label>
101
+ <Field.Control render={<Input type="number" />} />
102
+ </Field.Root>
103
+ <Field.Root name="reorderPoint">
104
+ <Field.Label>Reorder point</Field.Label>
105
+ <Field.Control render={<Input type="number" />} />
106
+ </Field.Root>
107
+ </div>
108
+ </Fieldset.Root>
109
+ </form>
110
+ </Layout.Column>
111
+ </Layout>
112
+ );
113
+ }
114
+ ```
115
+
116
+ ## Constraints
117
+
118
+ - Max ~6 sections — more than that is too hard to scan; promote to `form/wizard`
119
+ - Required-marker convention must be consistent across all sections
120
+ - Section legends must match anchor-nav labels
121
+
122
+ ## Anti-patterns
123
+
124
+ - More than ~6 sections — hard to scan; promote to `form/wizard`
125
+ - Required-marker convention varies between sections — pick one rule and apply everywhere
126
+ - Section legends that don't match anchor-nav labels
@@ -0,0 +1,105 @@
1
+ ---
2
+ slug: pattern/form/single-page
3
+ name: Single Page Form
4
+ category: pattern
5
+ subcategory: form
6
+ description: Routed full-page form for moderate field count (6-15) without natural sectioning
7
+ requiredImports: [Layout, Form, Field, Fieldset, Input, Select, Button]
8
+ tags: [form, page, create, edit, routed]
9
+ do:
10
+ - A routed Create or Edit page that the design has explicitly called out (e.g. /orders/create)
11
+ - Moderate field count (roughly 6-15) without natural sectioning, completed in one pass
12
+ dont:
13
+ - Default Create/Edit with no explicit routing requirement — use form/modal (the default)
14
+ - 15+ fields or grouped concerns — use form/sectioned
15
+ - Multi-stage flow — use form/wizard
16
+ ---
17
+
18
+ # pattern/form/single-page
19
+
20
+ ## When to Use
21
+
22
+ - A routed Create or Edit page that the design has explicitly called out
23
+ - Moderate field count (roughly 6–15) without natural sectioning, completed in one pass
24
+
25
+ ## Page Implementation
26
+
27
+ ```tsx
28
+ /* pattern: form/single-page */
29
+ import { Button, Layout, Input, Select, Field } from "@tailor-platform/app-shell";
30
+
31
+ type Props = {
32
+ onSave: (data: Record<string, string>) => void;
33
+ onCancel: () => void;
34
+ };
35
+
36
+ export default function SinglePageForm({ onSave, onCancel }: Props) {
37
+ return (
38
+ <Layout>
39
+ <Layout.Header
40
+ title="Create Product"
41
+ actions={[
42
+ <Button key="cancel" variant="outline" onClick={onCancel}>
43
+ Cancel
44
+ </Button>,
45
+ <Button key="save" type="submit" form="single-page-form">
46
+ Save
47
+ </Button>,
48
+ ]}
49
+ />
50
+ <Layout.Column>
51
+ <form
52
+ id="single-page-form"
53
+ onSubmit={(e) => {
54
+ e.preventDefault();
55
+ const formData = new FormData(e.currentTarget);
56
+ const entries: Record<string, string> = {};
57
+ formData.forEach((value, key) => {
58
+ entries[key] = value as string;
59
+ });
60
+ onSave(entries);
61
+ }}
62
+ className="space-y-4 max-w-2xl"
63
+ >
64
+ <Field.Root name="name">
65
+ <Field.Label>Name</Field.Label>
66
+ <Field.Control render={<Input />} />
67
+ </Field.Root>
68
+ <Field.Root name="sku">
69
+ <Field.Label>SKU</Field.Label>
70
+ <Field.Control render={<Input />} />
71
+ </Field.Root>
72
+ <Field.Root name="category">
73
+ <Field.Label>Category</Field.Label>
74
+ <Select
75
+ items={["Electronics", "Furniture", "Clothing", "Food"]}
76
+ placeholder="Select category"
77
+ />
78
+ </Field.Root>
79
+ <Field.Root name="price">
80
+ <Field.Label>Price</Field.Label>
81
+ <Field.Control render={<Input type="number" />} />
82
+ </Field.Root>
83
+ <Field.Root name="description">
84
+ <Field.Label>Description</Field.Label>
85
+ <Field.Control render={<Input />} />
86
+ </Field.Root>
87
+ </form>
88
+ </Layout.Column>
89
+ </Layout>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ## Constraints
95
+
96
+ - Single column full width below 1024px; single column max-w constrained at 1024–1280px
97
+ - Without an explicit routed-page requirement, the answer is `form/modal`
98
+ - A `/create` or `/edit` route in the screen spec does NOT require a full-page replacement
99
+
100
+ ## Anti-patterns
101
+
102
+ - Two-column layout for unrelated fields — breaks the linear reading order
103
+ - No required-field markers — users can't predict which fields will error
104
+ - Errors shown above the form rather than below the offending field
105
+ - Choosing this pattern for a Create flow because it's a Create flow — without explicit need, use `form/modal`
@@ -0,0 +1,134 @@
1
+ ---
2
+ slug: pattern/form/wizard
3
+ name: Wizard Form
4
+ category: pattern
5
+ subcategory: form
6
+ description: Multi-stage create flow with 3-7 steps and per-step validation gates
7
+ requiredImports: [Layout, Card, Form, Fieldset, Field, Input, Select, Badge, Button]
8
+ tags: [form, wizard, multi-step, import, stepper]
9
+ do:
10
+ - Multi-stage Create with 3-7 steps
11
+ - Import flows (upload → map → validate → confirm)
12
+ - Per-step validation gates progression
13
+ dont:
14
+ - Single screen of fields — use form/modal or form/single-page
15
+ - More than 7 steps — split into separate routed pages or reduce scope
16
+ ---
17
+
18
+ # pattern/form/wizard
19
+
20
+ ## When to Use
21
+
22
+ - Multi-stage Create with 3–7 steps
23
+ - Import flows (upload → map → validate → confirm)
24
+ - Per-step validation gates progression
25
+
26
+ ## Page Implementation
27
+
28
+ ```tsx
29
+ /* pattern: form/wizard */
30
+ import { useState } from "react";
31
+ import { Button, Card, Layout, Badge, Input, Field } from "@tailor-platform/app-shell";
32
+
33
+ const STEPS = ["Upload", "Map", "Review", "Done"] as const;
34
+
35
+ type Props = {
36
+ onComplete: () => void;
37
+ };
38
+
39
+ export default function WizardForm({ onComplete }: Props) {
40
+ const [currentStep, setCurrentStep] = useState(0);
41
+
42
+ const handleNext = () => {
43
+ if (currentStep < STEPS.length - 1) {
44
+ setCurrentStep(currentStep + 1);
45
+ } else {
46
+ onComplete();
47
+ }
48
+ };
49
+
50
+ const handleBack = () => {
51
+ if (currentStep > 0) {
52
+ setCurrentStep(currentStep - 1);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <Layout>
58
+ <Layout.Header title="Import products" />
59
+ <Layout.Column>
60
+ <Card.Root>
61
+ <Card.Content>
62
+ <div className="flex items-center gap-2">
63
+ {STEPS.map((step, i) => (
64
+ <Badge
65
+ key={step}
66
+ variant={i === currentStep ? "default" : i < currentStep ? "success" : "neutral"}
67
+ >
68
+ {i + 1}. {step}
69
+ </Badge>
70
+ ))}
71
+ </div>
72
+ </Card.Content>
73
+ </Card.Root>
74
+
75
+ <Card.Root>
76
+ <Card.Content>
77
+ {currentStep === 0 && (
78
+ <div className="space-y-4">
79
+ <Field.Root name="file">
80
+ <Field.Label>CSV file</Field.Label>
81
+ <Field.Control render={<Input type="file" accept=".csv" />} />
82
+ </Field.Root>
83
+ </div>
84
+ )}
85
+ {currentStep === 1 && (
86
+ <div className="space-y-4">
87
+ <p className="text-fg-muted">Map CSV columns to product fields</p>
88
+ <Field.Root name="nameColumn">
89
+ <Field.Label>Name column</Field.Label>
90
+ <Field.Control render={<Input placeholder="e.g. Column A" />} />
91
+ </Field.Root>
92
+ <Field.Root name="skuColumn">
93
+ <Field.Label>SKU column</Field.Label>
94
+ <Field.Control render={<Input placeholder="e.g. Column B" />} />
95
+ </Field.Root>
96
+ </div>
97
+ )}
98
+ {currentStep === 2 && (
99
+ <div className="space-y-4">
100
+ <p className="text-fg-muted">Review your import — 42 products will be created.</p>
101
+ </div>
102
+ )}
103
+ {currentStep === 3 && (
104
+ <div className="space-y-4">
105
+ <p className="text-fg-default">Import complete! 42 products created.</p>
106
+ </div>
107
+ )}
108
+ </Card.Content>
109
+ </Card.Root>
110
+
111
+ <div className="flex justify-between">
112
+ <Button variant="ghost" onClick={handleBack} disabled={currentStep === 0}>
113
+ Back
114
+ </Button>
115
+ <Button onClick={handleNext}>{currentStep === STEPS.length - 1 ? "Done" : "Next"}</Button>
116
+ </div>
117
+ </Layout.Column>
118
+ </Layout>
119
+ );
120
+ }
121
+ ```
122
+
123
+ ## Constraints
124
+
125
+ - Max 7 steps — more than that causes user abandonment
126
+ - Back-navigation must preserve prior step's input
127
+ - Validation must be per-step — don't defer until final submit
128
+ - Step indicator collapses to "Step 2 of 4" label below 1024px
129
+
130
+ ## Anti-patterns
131
+
132
+ - More than 7 steps — users lose context and abandon
133
+ - No back-navigation preservation — pressing Back loses prior step's input
134
+ - Validation deferred until final submit — failures force full re-traversal