@tailor-platform/app-shell 1.14.0 → 1.16.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.
Files changed (101) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/app-shell-core.css +1 -1
  3. package/dist/app-shell.d.ts +1203 -316
  4. package/dist/app-shell.js +4562 -3307
  5. package/dist/{appshell-CtS8Y3LW.js → appshell-B0wh5Lq0.js} +850 -549
  6. package/dist/style-entry.d.ts +118 -98
  7. package/dist/testing.d.ts +118 -98
  8. package/dist/testing.js +1 -1
  9. package/dist/themes/_template.css +1 -1
  10. package/dist/vite-plugin.d.ts +118 -98
  11. package/package.json +13 -13
  12. package/skills/app-shell-patterns/SKILL.md +108 -24
  13. package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +115 -0
  14. package/skills/app-shell-patterns/references/api/create-typed-paths.md +117 -0
  15. package/skills/app-shell-patterns/references/api/define-i18n-labels.md +143 -0
  16. package/skills/app-shell-patterns/references/api/define-module.md +247 -0
  17. package/skills/app-shell-patterns/references/api/define-resource.md +307 -0
  18. package/skills/app-shell-patterns/references/api/guards/hidden.md +202 -0
  19. package/skills/app-shell-patterns/references/api/guards/overview.md +354 -0
  20. package/skills/app-shell-patterns/references/api/guards/pass.md +129 -0
  21. package/skills/app-shell-patterns/references/api/guards/redirect-to.md +307 -0
  22. package/skills/app-shell-patterns/references/api/router/use-location.md +240 -0
  23. package/skills/app-shell-patterns/references/api/router/use-navigate.md +138 -0
  24. package/skills/app-shell-patterns/references/api/router/use-params.md +218 -0
  25. package/skills/app-shell-patterns/references/api/use-ai-chat.md +110 -0
  26. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +126 -0
  27. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +146 -0
  28. package/skills/app-shell-patterns/references/api/use-app-shell.md +143 -0
  29. package/skills/app-shell-patterns/references/api/use-auth.md +145 -0
  30. package/skills/app-shell-patterns/references/api/use-override-breadcrumb.md +74 -0
  31. package/skills/app-shell-patterns/references/api/use-page-meta.md +96 -0
  32. package/skills/app-shell-patterns/references/api/use-register-command-palette-actions.md +90 -0
  33. package/skills/app-shell-patterns/references/api/use-resolved-locale.md +67 -0
  34. package/skills/app-shell-patterns/references/api/use-route-error.md +214 -0
  35. package/skills/app-shell-patterns/references/api/use-theme.md +108 -0
  36. package/skills/app-shell-patterns/references/api/use-time-zone.md +80 -0
  37. package/skills/app-shell-patterns/references/api/use-toast.md +107 -0
  38. package/skills/app-shell-patterns/references/components/action-panel.md +168 -0
  39. package/skills/app-shell-patterns/references/components/activity-card.md +142 -0
  40. package/skills/app-shell-patterns/references/components/ai-chat.md +315 -0
  41. package/skills/app-shell-patterns/references/components/alert.md +202 -0
  42. package/skills/app-shell-patterns/references/components/app-shell.md +530 -0
  43. package/skills/app-shell-patterns/references/components/appearance-switcher.md +87 -0
  44. package/skills/app-shell-patterns/references/components/attachment.md +138 -0
  45. package/skills/app-shell-patterns/references/components/autocomplete.md +278 -0
  46. package/skills/app-shell-patterns/references/components/avatar.md +80 -0
  47. package/skills/app-shell-patterns/references/components/badge.md +309 -0
  48. package/skills/app-shell-patterns/references/components/button.md +164 -0
  49. package/skills/app-shell-patterns/references/components/card.md +109 -0
  50. package/skills/app-shell-patterns/references/components/checkbox.md +169 -0
  51. package/skills/app-shell-patterns/references/components/combobox.md +318 -0
  52. package/skills/app-shell-patterns/references/components/command-palette.md +413 -0
  53. package/skills/app-shell-patterns/references/components/csv-importer.md +214 -0
  54. package/skills/app-shell-patterns/references/components/data-table.md +953 -0
  55. package/skills/app-shell-patterns/references/components/date-picker.md +248 -0
  56. package/skills/app-shell-patterns/references/components/default-header.md +75 -0
  57. package/skills/app-shell-patterns/references/components/default-sidebar.md +136 -0
  58. package/skills/app-shell-patterns/references/components/description-card.md +569 -0
  59. package/skills/app-shell-patterns/references/components/dialog.md +164 -0
  60. package/skills/app-shell-patterns/references/components/document-progress-card.md +154 -0
  61. package/skills/app-shell-patterns/references/components/form.md +378 -0
  62. package/skills/app-shell-patterns/references/components/global-header-layout.md +132 -0
  63. package/skills/app-shell-patterns/references/components/grid.md +184 -0
  64. package/skills/app-shell-patterns/references/components/input.md +83 -0
  65. package/skills/app-shell-patterns/references/components/layout.md +539 -0
  66. package/skills/app-shell-patterns/references/components/menu.md +218 -0
  67. package/skills/app-shell-patterns/references/components/metric-card.md +94 -0
  68. package/skills/app-shell-patterns/references/components/select.md +283 -0
  69. package/skills/app-shell-patterns/references/components/sheet.md +237 -0
  70. package/skills/app-shell-patterns/references/components/sidebar-group.md +466 -0
  71. package/skills/app-shell-patterns/references/components/sidebar-item.md +458 -0
  72. package/skills/app-shell-patterns/references/components/sidebar-layout.md +258 -0
  73. package/skills/app-shell-patterns/references/components/spinner.md +81 -0
  74. package/skills/app-shell-patterns/references/components/table.md +188 -0
  75. package/skills/app-shell-patterns/references/components/tabs.md +158 -0
  76. package/skills/app-shell-patterns/references/components/textarea.md +122 -0
  77. package/skills/app-shell-patterns/references/components/timeline.md +317 -0
  78. package/skills/app-shell-patterns/references/components/toolbar.md +138 -0
  79. package/skills/app-shell-patterns/references/components/tooltip.md +144 -0
  80. package/skills/app-shell-patterns/references/components/with-guard.md +445 -0
  81. package/skills/app-shell-patterns/references/concepts/authentication.md +301 -0
  82. package/skills/app-shell-patterns/references/concepts/custom-components.md +59 -0
  83. package/skills/app-shell-patterns/references/concepts/file-based-routing.md +350 -0
  84. package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +11 -4
  85. package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
  86. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +387 -0
  87. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +388 -0
  88. package/skills/app-shell-patterns/references/{fundamental/design-system.md → concepts/styling-theming.md} +260 -105
  89. package/skills/app-shell-patterns/references/migrations.md +34 -0
  90. package/skills/app-shell-patterns/references/pages/document-detail.md +1125 -0
  91. package/skills/app-shell-patterns/references/patterns/form-composer.md +44 -46
  92. package/skills/app-shell-patterns/references/patterns/form-modal.md +39 -100
  93. package/skills/app-shell-patterns/references/patterns/form-sectioned.md +12 -38
  94. package/skills/app-shell-patterns/references/patterns/form-single-page.md +11 -32
  95. package/skills/app-shell-patterns/references/patterns/form-wizard.md +12 -53
  96. package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +11 -27
  97. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +14 -28
  98. package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
  99. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +11 -86
  100. package/skills/app-shell-patterns/references/fundamental/components.md +0 -981
  101. package/skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md +0 -201
@@ -1,20 +1,11 @@
1
1
  ---
2
- slug: pattern/form/composer
3
- name: Composer
4
- category: pattern
5
- subcategory: form
2
+ title: Composer
6
3
  description: Free-text composer — a multi-line input above a trailing action row, for notes, replies, and comments submitted in place
7
- requiredImports: [Card, Textarea, Button, Checkbox]
8
- tags: [composer, textarea, note, reply, comment, actions, discard, send]
9
- do:
10
- - A free-text box the user fills and submits in place — add a note, reply on a thread, leave a comment, give a rejection reason
11
- - The prose IS the interaction; any other controls only qualify how the text is submitted
12
- dont:
13
- - The text is one field among many on a record form — use form/modal, form/single-page or form/sectioned with a Textarea field
14
- - A chat input where Enter sends and there is no Discard — that is a different layout
15
4
  ---
16
5
 
17
- # pattern/form/composer
6
+ <!-- GENERATED by docs-kit from docs-src/patterns/form-composer.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Composer
18
9
 
19
10
  ## When to Use
20
11
 
@@ -60,35 +51,36 @@ Leave it empty and the buttons still sit correctly at the right; `justify-betwee
60
51
 
61
52
  ## Page Implementation
62
53
 
63
- ```tsx
64
- /* pattern: form/composer */
65
- import { useState } from "react";
66
- import { Button, Card, Checkbox, Textarea } from "@tailor-platform/app-shell";
67
-
68
- type Props = {
69
- onSubmit: (body: string, internal: boolean) => void;
70
- submitting?: boolean;
71
- };
54
+ <example-preview name="form-composer"></example-preview>
72
55
 
73
- export default function Composer({ onSubmit, submitting = false }: Props) {
56
+ ```tsx
57
+ function FormComposer() {
74
58
  const [body, setBody] = useState("");
75
59
  const [internal, setInternal] = useState(false);
60
+ const [errors, setErrors] = useState<Record<string, string>>({});
61
+
62
+ const handleSubmit = () => {
63
+ window.alert(`Sending ${internal ? "internal note" : "reply"}: ${body}`);
64
+ setErrors({});
65
+ setBody("");
66
+ };
76
67
 
77
68
  return (
78
69
  <Card.Root>
79
70
  <Card.Content>
80
- <div className="space-y-3">
81
- <Textarea
82
- aria-label="Reply"
83
- value={body}
84
- onChange={(e) => setBody(e.target.value)}
85
- placeholder={internal ? "Internal context for teammates…" : "Reply to customer…"}
86
- rows={4}
87
- />
71
+ <Form noValidate errors={errors} onFormSubmit={handleSubmit} className="space-y-3">
72
+ <Field.Root name="body">
73
+ <Field.Label className="sr-only">Reply</Field.Label>
74
+ <Textarea
75
+ required
76
+ rows={4}
77
+ value={body}
78
+ onChange={(event) => setBody(event.target.value)}
79
+ placeholder={internal ? "Internal context for teammates…" : "Reply to customer…"}
80
+ />
81
+ <Field.Error />
82
+ </Field.Root>
88
83
  <div className="flex items-center justify-between gap-2">
89
- {/* Options slot — 0..N controls that qualify the submission. An
90
- internal-note toggle here; swap for an attachment button, a
91
- visibility Select, a template picker, or drop it entirely. */}
92
84
  <div className="flex min-w-0 items-center gap-2">
93
85
  <Checkbox
94
86
  label="Internal note (not sent to customer)"
@@ -96,39 +88,43 @@ export default function Composer({ onSubmit, submitting = false }: Props) {
96
88
  onCheckedChange={setInternal}
97
89
  />
98
90
  </div>
99
- {/* Action slot — fixed shape: optional ghost Discard, then exactly
100
- one primary submit. Never more than one primary. */}
101
91
  <div className="flex shrink-0 gap-2">
102
92
  <Button
93
+ type="button"
103
94
  variant="ghost"
104
- onClick={() => setBody("")}
105
- disabled={body.length === 0 || submitting}
95
+ onClick={() => {
96
+ setBody("");
97
+ setErrors({});
98
+ }}
99
+ disabled={body.length === 0}
106
100
  >
107
101
  Discard
108
102
  </Button>
109
- <Button
110
- onClick={() => onSubmit(body, internal)}
111
- disabled={body.trim().length === 0 || submitting}
112
- >
113
- {submitting ? "Sending…" : "Send"}
103
+ <Button type="submit" disabled={body.trim().length === 0}>
104
+ Send
114
105
  </Button>
115
106
  </div>
116
107
  </div>
117
- </div>
108
+ </Form>
118
109
  </Card.Content>
119
110
  </Card.Root>
120
111
  );
121
112
  }
122
113
  ```
123
114
 
115
+ <!-- source: composer.tsx -->
116
+
124
117
  ## Constraints
125
118
 
126
119
  - The body control is `Textarea`, never `Input` — `Input` is locked to `h-9` and clips prose to one 36px line.
127
- - Size the body with `rows` (3–5 is the usual range). Don't reach for an `astw:h-*` / `astw:min-h-*` override: `astw:` utilities only resolve if that exact class was compiled into the AppShell package CSS, so an arbitrary one written in a consuming app silently does nothing.
128
- - The `Textarea` needs an accessible name. There is usually no visible label in a composer, so pass `aria-label`; a placeholder is not a label.
120
+ - Size the body with `rows` (3–5 is the usual range). Do not override its height with classes: `astw:` is internal to AppShell, and `Textarea`'s supported sizing API is `rows`.
121
+ - The `Textarea` needs an accessible name. There is usually no visible label in a composer, so use a `sr-only` `Field.Label`; a placeholder is not a label. Prefer this over `aria-label` — the `Field.Root` is already there, and a real label keeps the error and description wiring available.
129
122
  - One primary `Button` in the action row. Discard is `ghost`, and any left-side action is `ghost` too.
130
123
  - The action row is one row at every width — the left side shrinks, the buttons do not wrap under the body.
131
124
  - Disable the submit on empty or whitespace-only input, and disable both buttons while `submitting`.
125
+ - The composer is a `Form`, not a bare widget. The body sits in a `Field.Root name="…"`, Send is `type="submit"`, and Discard is `type="button"` — inside a `<Form>` an untyped `<button>` defaults to `submit`.
126
+ - Keep the body **controlled**. It is read during render to gate Send and to swap the placeholder, which a submit-time handler cannot do. This is the one `form/*` pattern where field state is load-bearing rather than redundant.
127
+ - Route server rejections (moderation, rate limit, thread closed) through `Form`'s `errors` prop into `Field.Error`, not a toast. A composer that can be rejected needs somewhere for the reason to land next to the text the user still has.
132
128
  - Wrap the composer in `Card.Root` when it sits among other cards (a detail page, the end of a thread). A composer that is already inside a `Sheet` or `Dialog` body does not need its own card.
133
129
 
134
130
  ## Anti-patterns
@@ -139,4 +135,6 @@ export default function Composer({ onSubmit, submitting = false }: Props) {
139
135
  - The action row stacked above the `Textarea`, or the buttons wrapped onto their own line — the composer stops reading as one unit.
140
136
  - A left-side toggle that silently changes what the submit does without changing the submit's own affordance — if "internal note" changes the destination, the placeholder (or the label) should say so.
141
137
  - Submit enabled on an empty body, so the user can post nothing.
138
+ - A bare `<div>` of controls with an `onClick` submit — no `Form`, so a server rejection has nowhere to go and the body has no field identity.
139
+ - Clearing the body on a _failed_ submit — the user loses what they wrote. Clear only after the submit succeeds.
142
140
  - A body so tall (`rows={12}`) that the action row falls below the fold — the composer stops reading as one unit.
@@ -1,23 +1,11 @@
1
1
  ---
2
- slug: pattern/form/modal
3
- name: Modal Form
4
- category: pattern
5
- subcategory: form
2
+ title: Modal Form
6
3
  description: Default form pattern for Create/Edit — keeps user in context on the parent screen
7
- requiredImports: [Dialog, Button, Form, Field, Layout]
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
4
  ---
19
5
 
20
- # pattern/form/modal
6
+ <!-- GENERATED by docs-kit from docs-src/patterns/form-modal.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Modal Form
21
9
 
22
10
  ## When to Use
23
11
 
@@ -28,21 +16,10 @@ dont:
28
16
 
29
17
  ## Page Implementation
30
18
 
31
- ```tsx
32
- /* pattern: form/modal */
33
- import { Button, Dialog, Field, Form } from "@tailor-platform/app-shell";
34
-
35
- type Address = {
36
- label: string;
37
- street: string;
38
- city: string;
39
- };
40
-
41
- type Props = {
42
- onSave: (data: Address) => void;
43
- };
19
+ <example-preview name="modal-form"></example-preview>
44
20
 
45
- export default function ModalForm({ onSave }: Props) {
21
+ ```tsx
22
+ function ModalForm() {
46
23
  return (
47
24
  <Dialog.Root>
48
25
  <Dialog.Trigger render={<Button />}>Add address</Dialog.Trigger>
@@ -51,34 +28,32 @@ export default function ModalForm({ onSave }: Props) {
51
28
  <Dialog.Title>Add address</Dialog.Title>
52
29
  <Dialog.Description>Add a shipping address to this order.</Dialog.Description>
53
30
  </Dialog.Header>
54
- {/*
55
- * `Form` + `onFormSubmit` replaces a hand-rolled `<form onSubmit>` +
56
- * `FormData`: values arrive parsed, and validation runs before the
57
- * handler fires. Every text field is uncontrolled — no state needed.
58
- */}
59
- <Form<Address> noValidate onFormSubmit={(values) => onSave(values)}>
60
- <div className="space-y-4 py-4">
31
+ <form
32
+ onSubmit={(event) => {
33
+ event.preventDefault();
34
+ const data = new FormData(event.currentTarget);
35
+ window.alert(`Saving ${data.get("label") ?? ""}`);
36
+ }}
37
+ >
38
+ <div className="flex flex-col gap-4 py-4">
61
39
  <Field.Root name="label">
62
40
  <Field.Label>Label</Field.Label>
63
- <Field.Control required placeholder="Head office" />
64
- <Field.Error match="valueMissing">Label is required.</Field.Error>
41
+ <Field.Control render={<Input />} />
65
42
  </Field.Root>
66
43
  <Field.Root name="street">
67
44
  <Field.Label>Street</Field.Label>
68
- <Field.Control required />
69
- <Field.Error match="valueMissing">Street is required.</Field.Error>
45
+ <Field.Control render={<Input />} />
70
46
  </Field.Root>
71
47
  <Field.Root name="city">
72
48
  <Field.Label>City</Field.Label>
73
- <Field.Control required />
74
- <Field.Error match="valueMissing">City is required.</Field.Error>
49
+ <Field.Control render={<Input />} />
75
50
  </Field.Root>
76
51
  </div>
77
52
  <Dialog.Footer>
78
53
  <Dialog.Close render={<Button variant="ghost" />}>Cancel</Dialog.Close>
79
54
  <Button type="submit">Save</Button>
80
55
  </Dialog.Footer>
81
- </Form>
56
+ </form>
82
57
  </Dialog.Content>
83
58
  </Dialog.Root>
84
59
  );
@@ -87,87 +62,51 @@ export default function ModalForm({ onSave }: Props) {
87
62
 
88
63
  ## Route-driven Variant
89
64
 
90
- ```tsx
91
- /* pattern: form/modal (route-driven variant) */
92
- import { useState } from "react";
93
- import { Button, Dialog, Field, Form, Layout } from "@tailor-platform/app-shell";
94
-
95
- type ProductDraft = {
96
- name: string;
97
- };
98
-
99
- type Props = {
100
- isCreateOpen: boolean;
101
- onNavigateToCreate: () => void;
102
- onNavigateToList: () => void;
103
- onSave: (data: ProductDraft) => Promise<{ errors?: Record<string, string> }>;
104
- };
105
-
106
- /**
107
- * Route-driven modal: the form has its own URL but renders as a popup
108
- * over the list. Both `/products` and `/products/create` render this
109
- * same component — the parent list stays visible underneath.
110
- */
111
- export default function ModalFormRouted({
112
- isCreateOpen,
113
- onNavigateToCreate,
114
- onNavigateToList,
115
- onSave,
116
- }: Props) {
117
- // Server-side validation errors, keyed by field `name`. `Form` routes each
118
- // one to the matching `Field.Error` and clears it when the user edits that
119
- // field — so failures never need their own alert banner.
120
- const [errors, setErrors] = useState<Record<string, string>>({});
121
-
122
- const handleSubmit = async (values: ProductDraft) => {
123
- const result = await onSave(values);
124
- if (result.errors) {
125
- setErrors(result.errors);
126
- return;
127
- }
128
- setErrors({});
129
- onNavigateToList();
130
- };
65
+ The route-driven variant renders the same component at both the parent path and the `create` / `edit` path, with `onOpenChange` navigating back so the URL stays in sync.
131
66
 
67
+ <example-preview name="modal-form-routed"></example-preview>
68
+
69
+ ```tsx
70
+ function ModalFormRouted() {
71
+ const [isCreateOpen, setCreateOpen] = useState(false);
132
72
  return (
133
73
  <Layout>
134
74
  <Layout.Header
135
75
  title="Products"
136
76
  actions={[
137
- <Button key="create" onClick={onNavigateToCreate}>
77
+ <Button key="create" onClick={() => setCreateOpen(true)}>
138
78
  Create
139
79
  </Button>,
140
80
  ]}
141
81
  />
142
82
  <Layout.Column>{/* products list — see list/dense-scan */}</Layout.Column>
143
83
 
144
- <Dialog.Root
145
- open={isCreateOpen}
146
- onOpenChange={(open) => {
147
- if (!open) onNavigateToList();
148
- }}
149
- >
84
+ <Dialog.Root open={isCreateOpen} onOpenChange={setCreateOpen}>
150
85
  <Dialog.Content>
151
86
  <Dialog.Header>
152
87
  <Dialog.Title>Create product</Dialog.Title>
153
88
  </Dialog.Header>
154
- <Form<ProductDraft> noValidate errors={errors} onFormSubmit={handleSubmit}>
155
- <div className="space-y-4 py-4">
89
+ <form
90
+ onSubmit={(event) => {
91
+ event.preventDefault();
92
+ const data = new FormData(event.currentTarget);
93
+ window.alert(`Saving ${data.get("name") ?? ""}`);
94
+ setCreateOpen(false);
95
+ }}
96
+ >
97
+ <div className="flex flex-col gap-4 py-4">
156
98
  <Field.Root name="name">
157
99
  <Field.Label>Name</Field.Label>
158
- <Field.Control required />
159
- {/* Catch-all: renders the native message, a `match` message, or
160
- the server error routed in via the `errors` prop above. */}
161
- <Field.Error />
100
+ <Field.Control render={<Input />} />
162
101
  </Field.Root>
163
102
  </div>
164
103
  <Dialog.Footer>
165
- <Button type="button" variant="ghost" onClick={onNavigateToList}>
104
+ <Button variant="ghost" onClick={() => setCreateOpen(false)}>
166
105
  Cancel
167
106
  </Button>
168
107
  <Button type="submit">Save</Button>
169
108
  </Dialog.Footer>
170
- </Form>
109
+ </form>
171
110
  </Dialog.Content>
172
111
  </Dialog.Root>
173
112
  </Layout>
@@ -1,21 +1,11 @@
1
1
  ---
2
- slug: pattern/form/sectioned
3
- name: Sectioned Form
4
- category: pattern
5
- subcategory: form
2
+ title: Sectioned Form
6
3
  description: Complex form with 15+ fields organized into named fieldset sections
7
- requiredImports: [Layout, Card, Form, Fieldset, Field, 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
4
  ---
17
5
 
18
- # pattern/form/sectioned
6
+ <!-- GENERATED by docs-kit from docs-src/patterns/form-sectioned.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Sectioned Form
19
9
 
20
10
  ## When to Use
21
11
 
@@ -24,32 +14,14 @@ dont:
24
14
 
25
15
  ## Page Implementation
26
16
 
27
- ```tsx
28
- /* pattern: form/sectioned */
29
- import { Button, Card, Combobox, Field, Fieldset, Form, Layout } from "@tailor-platform/app-shell";
30
-
31
- const CURRENCIES = ["USD", "EUR", "GBP", "JPY"];
32
-
33
- const WAREHOUSES = [
34
- { id: 1, name: "Tokyo DC" },
35
- { id: 2, name: "Osaka DC" },
36
- { id: 3, name: "Nagoya DC" },
37
- ];
17
+ <example-preview name="form-sectioned"></example-preview>
38
18
 
39
- const SECTIONS = [
40
- { id: "identity", title: "Identity", description: "How the product is named and referenced." },
41
- { id: "pricing", title: "Pricing", description: "List price and the currency it is quoted in." },
42
- { id: "inventory", title: "Inventory", description: "Opening stock and replenishment trigger." },
43
- ];
44
-
45
- const anchorIdFor = (id: string) => `section-${id}`;
46
-
47
- type Props = {
48
- onSave: (data: Record<string, unknown>) => void;
49
- onCancel: () => void;
50
- };
19
+ ```tsx
20
+ function FormSectioned() {
21
+ const onSave = (values: Record<string, unknown>) =>
22
+ window.alert(`Saving ${Object.keys(values).length} fields`);
23
+ const onCancel = () => {};
51
24
 
52
- export default function SectionedForm({ onSave, onCancel }: Props) {
53
25
  return (
54
26
  <Layout>
55
27
  <Layout.Header
@@ -163,6 +135,8 @@ export default function SectionedForm({ onSave, onCancel }: Props) {
163
135
  }
164
136
  ```
165
137
 
138
+ <!-- source: sectioned-form.tsx -->
139
+
166
140
  ## Constraints
167
141
 
168
142
  - Max ~6 sections — more than that is too hard to scan; promote to `form/wizard`
@@ -1,21 +1,11 @@
1
1
  ---
2
- slug: pattern/form/single-page
3
- name: Single Page Form
4
- category: pattern
5
- subcategory: form
2
+ title: Single Page Form
6
3
  description: Routed full-page form for moderate field count (6-15) without natural sectioning
7
- requiredImports: [Layout, Form, Field, Textarea, 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
4
  ---
17
5
 
18
- # pattern/form/single-page
6
+ <!-- GENERATED by docs-kit from docs-src/patterns/form-single-page.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Single Page Form
19
9
 
20
10
  ## When to Use
21
11
 
@@ -24,26 +14,13 @@ dont:
24
14
 
25
15
  ## Page Implementation
26
16
 
27
- ```tsx
28
- /* pattern: form/single-page */
29
- import { Button, Field, Form, Textarea, Layout, Select } from "@tailor-platform/app-shell";
30
-
31
- const CATEGORIES = ["Electronics", "Furniture", "Clothing", "Food"];
17
+ <example-preview name="form-single-page"></example-preview>
32
18
 
33
- type ProductDraft = {
34
- name: string;
35
- sku: string;
36
- category: string;
37
- price: string;
38
- description: string;
39
- };
40
-
41
- type Props = {
42
- onSave: (data: ProductDraft) => void;
43
- onCancel: () => void;
44
- };
19
+ ```tsx
20
+ function FormSinglePage() {
21
+ const onSave = (values: ProductDraft) => window.alert(`Saving ${values.name}`);
22
+ const onCancel = () => {};
45
23
 
46
- export default function SinglePageForm({ onSave, onCancel }: Props) {
47
24
  return (
48
25
  <Layout>
49
26
  {/*
@@ -107,6 +84,8 @@ export default function SinglePageForm({ onSave, onCancel }: Props) {
107
84
  }
108
85
  ```
109
86
 
87
+ <!-- source: single-page-form.tsx -->
88
+
110
89
  ## Constraints
111
90
 
112
91
  - Single column full width below 1024px; single column max-w constrained at 1024–1280px
@@ -1,22 +1,11 @@
1
1
  ---
2
- slug: pattern/form/wizard
3
- name: Wizard Form
4
- category: pattern
5
- subcategory: form
2
+ title: Wizard Form
6
3
  description: Multi-stage create flow with 3-7 steps and per-step validation gates
7
- requiredImports: [Layout, Card, Badge, Form, Fieldset, Field, Select, Button]
8
- tags: [form, wizard, multi-step, stepper, onboarding]
9
- do:
10
- - Multi-stage Create with 3-7 steps
11
- - Onboarding and request flows where users should focus on one step at a time
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
- - CSV/spreadsheet import — use the CsvImporter component, not a hand-built wizard
17
4
  ---
18
5
 
19
- # pattern/form/wizard
6
+ <!-- GENERATED by docs-kit from docs-src/patterns/form-wizard.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Wizard Form
20
9
 
21
10
  ## When to Use
22
11
 
@@ -29,45 +18,13 @@ the upload → map → validate → confirm flow. Don't rebuild it here.
29
18
 
30
19
  ## Page Implementation
31
20
 
21
+ <example-preview name="form-wizard"></example-preview>
22
+
32
23
  ```tsx
33
- /* pattern: form/wizard */
34
- import { useState } from "react";
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
- };
56
-
57
- const INITIAL: Draft = {
58
- title: "",
59
- description: "",
60
- owner: "",
61
- startDate: "",
62
- estimate: "",
63
- };
64
-
65
- type Props = {
66
- onComplete: (draft: Draft) => void;
67
- onCancel: () => void;
68
- };
69
-
70
- export default function WizardForm({ onComplete, onCancel }: Props) {
24
+ function FormWizard() {
25
+ const onComplete = (finalDraft: Draft) => window.alert(`Creating ${finalDraft.title}`);
26
+ const onCancel = () => {};
27
+
71
28
  const [step, setStep] = useState(0);
72
29
  // Accumulated values. Each step's Form unmounts on navigation, so the draft
73
30
  // is what makes Back non-destructive — fields re-read it via `defaultValue`.
@@ -187,6 +144,8 @@ export default function WizardForm({ onComplete, onCancel }: Props) {
187
144
  }
188
145
  ```
189
146
 
147
+ <!-- source: wizard-form.tsx -->
148
+
190
149
  ## Constraints
191
150
 
192
151
  - Max 7 steps — more than that causes user abandonment
@@ -1,21 +1,11 @@
1
1
  ---
2
- slug: pattern/interaction/confirm
3
- name: Confirm
4
- category: pattern
5
- subcategory: interaction
2
+ title: Confirm
6
3
  description: Confirmation dialog before destructive or irreversible actions
7
- requiredImports: [Dialog, Button, Input]
8
- tags: [dialog, confirm, destructive, delete, irreversible]
9
- do:
10
- - Before a destructive or irreversible action (delete, cancel, void, archive)
11
- - Before bulk actions that affect many records
12
- - When the action's consequence isn't obvious from the trigger
13
- dont:
14
- - Routine reversible actions — use interaction/toast with optional Undo instead
15
- - Form submission for non-destructive create/edit — submit handlers don't need a confirm
16
4
  ---
17
5
 
18
- # pattern/interaction/confirm
6
+ <!-- GENERATED by docs-kit from docs-src/patterns/interaction-confirm.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Confirm
19
9
 
20
10
  ## When to Use
21
11
 
@@ -25,31 +15,23 @@ dont:
25
15
 
26
16
  ## Page Implementation
27
17
 
28
- ```tsx
29
- /* pattern: interaction/confirm */
30
- import { Button, Dialog } from "@tailor-platform/app-shell";
31
-
32
- type Props = {
33
- orderId: string;
34
- onDelete: () => void;
35
- };
18
+ <example-preview name="interaction-confirm"></example-preview>
36
19
 
37
- export default function ConfirmDialog({ orderId, onDelete }: Props) {
20
+ ```tsx
21
+ function InteractionConfirm() {
38
22
  return (
39
23
  <Dialog.Root>
40
24
  <Dialog.Trigger render={<Button variant="destructive" />}>Delete</Dialog.Trigger>
41
25
  <Dialog.Content>
42
26
  <Dialog.Header>
43
- <Dialog.Title>Delete order {orderId}?</Dialog.Title>
27
+ <Dialog.Title>Delete order ORD-1024?</Dialog.Title>
44
28
  <Dialog.Description>
45
29
  This will remove the order and all its line items. This action cannot be undone.
46
30
  </Dialog.Description>
47
31
  </Dialog.Header>
48
32
  <Dialog.Footer>
49
33
  <Dialog.Close render={<Button variant="outline" />}>Cancel</Dialog.Close>
50
- <Button variant="destructive" onClick={onDelete}>
51
- Delete
52
- </Button>
34
+ <Dialog.Close render={<Button variant="destructive" />}>Delete</Dialog.Close>
53
35
  </Dialog.Footer>
54
36
  </Dialog.Content>
55
37
  </Dialog.Root>
@@ -57,6 +39,8 @@ export default function ConfirmDialog({ orderId, onDelete }: Props) {
57
39
  }
58
40
  ```
59
41
 
42
+ <!-- source: confirm.tsx -->
43
+
60
44
  ## Copy Rules
61
45
 
62
46
  - Title is a question, names the object: "Delete order ORD-1234?", "Cancel invoice INV-001?"