@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.
- package/LICENSE.md +21 -0
- package/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +1203 -316
- package/dist/app-shell.js +4562 -3307
- package/dist/{appshell-CtS8Y3LW.js → appshell-B0wh5Lq0.js} +850 -549
- package/dist/style-entry.d.ts +118 -98
- package/dist/testing.d.ts +118 -98
- package/dist/testing.js +1 -1
- package/dist/themes/_template.css +1 -1
- package/dist/vite-plugin.d.ts +118 -98
- package/package.json +13 -13
- package/skills/app-shell-patterns/SKILL.md +108 -24
- package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +115 -0
- package/skills/app-shell-patterns/references/api/create-typed-paths.md +117 -0
- package/skills/app-shell-patterns/references/api/define-i18n-labels.md +143 -0
- package/skills/app-shell-patterns/references/api/define-module.md +247 -0
- package/skills/app-shell-patterns/references/api/define-resource.md +307 -0
- package/skills/app-shell-patterns/references/api/guards/hidden.md +202 -0
- package/skills/app-shell-patterns/references/api/guards/overview.md +354 -0
- package/skills/app-shell-patterns/references/api/guards/pass.md +129 -0
- package/skills/app-shell-patterns/references/api/guards/redirect-to.md +307 -0
- package/skills/app-shell-patterns/references/api/router/use-location.md +240 -0
- package/skills/app-shell-patterns/references/api/router/use-navigate.md +138 -0
- package/skills/app-shell-patterns/references/api/router/use-params.md +218 -0
- package/skills/app-shell-patterns/references/api/use-ai-chat.md +110 -0
- package/skills/app-shell-patterns/references/api/use-app-shell-config.md +126 -0
- package/skills/app-shell-patterns/references/api/use-app-shell-data.md +146 -0
- package/skills/app-shell-patterns/references/api/use-app-shell.md +143 -0
- package/skills/app-shell-patterns/references/api/use-auth.md +145 -0
- package/skills/app-shell-patterns/references/api/use-override-breadcrumb.md +74 -0
- package/skills/app-shell-patterns/references/api/use-page-meta.md +96 -0
- package/skills/app-shell-patterns/references/api/use-register-command-palette-actions.md +90 -0
- package/skills/app-shell-patterns/references/api/use-resolved-locale.md +67 -0
- package/skills/app-shell-patterns/references/api/use-route-error.md +214 -0
- package/skills/app-shell-patterns/references/api/use-theme.md +108 -0
- package/skills/app-shell-patterns/references/api/use-time-zone.md +80 -0
- package/skills/app-shell-patterns/references/api/use-toast.md +107 -0
- package/skills/app-shell-patterns/references/components/action-panel.md +168 -0
- package/skills/app-shell-patterns/references/components/activity-card.md +142 -0
- package/skills/app-shell-patterns/references/components/ai-chat.md +315 -0
- package/skills/app-shell-patterns/references/components/alert.md +202 -0
- package/skills/app-shell-patterns/references/components/app-shell.md +530 -0
- package/skills/app-shell-patterns/references/components/appearance-switcher.md +87 -0
- package/skills/app-shell-patterns/references/components/attachment.md +138 -0
- package/skills/app-shell-patterns/references/components/autocomplete.md +278 -0
- package/skills/app-shell-patterns/references/components/avatar.md +80 -0
- package/skills/app-shell-patterns/references/components/badge.md +309 -0
- package/skills/app-shell-patterns/references/components/button.md +164 -0
- package/skills/app-shell-patterns/references/components/card.md +109 -0
- package/skills/app-shell-patterns/references/components/checkbox.md +169 -0
- package/skills/app-shell-patterns/references/components/combobox.md +318 -0
- package/skills/app-shell-patterns/references/components/command-palette.md +413 -0
- package/skills/app-shell-patterns/references/components/csv-importer.md +214 -0
- package/skills/app-shell-patterns/references/components/data-table.md +953 -0
- package/skills/app-shell-patterns/references/components/date-picker.md +248 -0
- package/skills/app-shell-patterns/references/components/default-header.md +75 -0
- package/skills/app-shell-patterns/references/components/default-sidebar.md +136 -0
- package/skills/app-shell-patterns/references/components/description-card.md +569 -0
- package/skills/app-shell-patterns/references/components/dialog.md +164 -0
- package/skills/app-shell-patterns/references/components/document-progress-card.md +154 -0
- package/skills/app-shell-patterns/references/components/form.md +378 -0
- package/skills/app-shell-patterns/references/components/global-header-layout.md +132 -0
- package/skills/app-shell-patterns/references/components/grid.md +184 -0
- package/skills/app-shell-patterns/references/components/input.md +83 -0
- package/skills/app-shell-patterns/references/components/layout.md +539 -0
- package/skills/app-shell-patterns/references/components/menu.md +218 -0
- package/skills/app-shell-patterns/references/components/metric-card.md +94 -0
- package/skills/app-shell-patterns/references/components/select.md +283 -0
- package/skills/app-shell-patterns/references/components/sheet.md +237 -0
- package/skills/app-shell-patterns/references/components/sidebar-group.md +466 -0
- package/skills/app-shell-patterns/references/components/sidebar-item.md +458 -0
- package/skills/app-shell-patterns/references/components/sidebar-layout.md +258 -0
- package/skills/app-shell-patterns/references/components/spinner.md +81 -0
- package/skills/app-shell-patterns/references/components/table.md +188 -0
- package/skills/app-shell-patterns/references/components/tabs.md +158 -0
- package/skills/app-shell-patterns/references/components/textarea.md +122 -0
- package/skills/app-shell-patterns/references/components/timeline.md +317 -0
- package/skills/app-shell-patterns/references/components/toolbar.md +138 -0
- package/skills/app-shell-patterns/references/components/tooltip.md +144 -0
- package/skills/app-shell-patterns/references/components/with-guard.md +445 -0
- package/skills/app-shell-patterns/references/concepts/authentication.md +301 -0
- package/skills/app-shell-patterns/references/concepts/custom-components.md +59 -0
- package/skills/app-shell-patterns/references/concepts/file-based-routing.md +350 -0
- package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +11 -4
- package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
- package/skills/app-shell-patterns/references/concepts/routing-navigation.md +387 -0
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +388 -0
- package/skills/app-shell-patterns/references/{fundamental/design-system.md → concepts/styling-theming.md} +260 -105
- package/skills/app-shell-patterns/references/migrations.md +34 -0
- package/skills/app-shell-patterns/references/pages/document-detail.md +1125 -0
- package/skills/app-shell-patterns/references/patterns/form-composer.md +44 -46
- package/skills/app-shell-patterns/references/patterns/form-modal.md +39 -100
- package/skills/app-shell-patterns/references/patterns/form-sectioned.md +12 -38
- package/skills/app-shell-patterns/references/patterns/form-single-page.md +11 -32
- package/skills/app-shell-patterns/references/patterns/form-wizard.md +12 -53
- package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +11 -27
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +14 -28
- package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +11 -86
- package/skills/app-shell-patterns/references/fundamental/components.md +0 -981
- package/skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md +0 -201
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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={() =>
|
|
105
|
-
|
|
95
|
+
onClick={() => {
|
|
96
|
+
setBody("");
|
|
97
|
+
setErrors({});
|
|
98
|
+
}}
|
|
99
|
+
disabled={body.length === 0}
|
|
106
100
|
>
|
|
107
101
|
Discard
|
|
108
102
|
</Button>
|
|
109
|
-
<Button
|
|
110
|
-
|
|
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
|
-
</
|
|
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).
|
|
128
|
-
- The `Textarea` needs an accessible name. There is usually no visible label in a composer, so
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
</
|
|
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
|
-
|
|
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={
|
|
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
|
-
<
|
|
155
|
-
|
|
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
|
|
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
|
|
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
|
-
</
|
|
109
|
+
</form>
|
|
171
110
|
</Dialog.Content>
|
|
172
111
|
</Dialog.Root>
|
|
173
112
|
</Layout>
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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?"
|