@tempoxyz/ds 0.3.9 → 0.3.11
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/dist/docs/AGENTS.md +9 -0
- package/dist/docs/components/accordion.md +489 -0
- package/dist/docs/components/address-card.md +147 -0
- package/dist/docs/components/agent-logo.md +194 -0
- package/dist/docs/components/alert-dialog.md +631 -0
- package/dist/docs/components/alert.md +448 -0
- package/dist/docs/components/amount-input.md +305 -0
- package/dist/docs/components/amount.md +794 -0
- package/dist/docs/components/app-shell.md +559 -0
- package/dist/docs/components/asset-chip.md +294 -0
- package/dist/docs/components/autocomplete.md +619 -0
- package/dist/docs/components/avatar.md +321 -0
- package/dist/docs/components/badge.md +339 -0
- package/dist/docs/components/badged-logo.md +116 -0
- package/dist/docs/components/banner.md +379 -0
- package/dist/docs/components/bar-chart.md +208 -0
- package/dist/docs/components/breadcrumbs.md +380 -0
- package/dist/docs/components/button-group.md +286 -0
- package/dist/docs/components/button.md +650 -0
- package/dist/docs/components/calendar.md +375 -0
- package/dist/docs/components/callout.md +96 -0
- package/dist/docs/components/checkbox-group.md +521 -0
- package/dist/docs/components/checkbox.md +467 -0
- package/dist/docs/components/choice-group.md +516 -0
- package/dist/docs/components/choicebox.md +641 -0
- package/dist/docs/components/code.md +100 -0
- package/dist/docs/components/combobox.md +939 -0
- package/dist/docs/components/container.md +257 -0
- package/dist/docs/components/copy-button.md +642 -0
- package/dist/docs/components/date-picker.md +514 -0
- package/dist/docs/components/dialog.md +1332 -0
- package/dist/docs/components/drawer.md +966 -0
- package/dist/docs/components/empty.md +340 -0
- package/dist/docs/components/field.md +570 -0
- package/dist/docs/components/fieldset.md +398 -0
- package/dist/docs/components/frame.md +434 -0
- package/dist/docs/components/gauge.md +299 -0
- package/dist/docs/components/get-started-visual.md +117 -0
- package/dist/docs/components/grid.md +288 -0
- package/dist/docs/components/hero-visual.md +155 -0
- package/dist/docs/components/hex.md +487 -0
- package/dist/docs/components/icon-tile.md +292 -0
- package/dist/docs/components/identicon.md +371 -0
- package/dist/docs/components/inline.md +233 -0
- package/dist/docs/components/inset.md +225 -0
- package/dist/docs/components/kbd.md +423 -0
- package/dist/docs/components/list-group.md +981 -0
- package/dist/docs/components/loading-dots.md +237 -0
- package/dist/docs/components/menu.md +934 -0
- package/dist/docs/components/native-select.md +204 -0
- package/dist/docs/components/network-logo.md +248 -0
- package/dist/docs/components/number-field.md +543 -0
- package/dist/docs/components/otp-field.md +616 -0
- package/dist/docs/components/pagination.md +516 -0
- package/dist/docs/components/popover.md +544 -0
- package/dist/docs/components/progress.md +388 -0
- package/dist/docs/components/qr.md +314 -0
- package/dist/docs/components/radio-group.md +528 -0
- package/dist/docs/components/secret-value.md +406 -0
- package/dist/docs/components/select.md +751 -0
- package/dist/docs/components/separator.md +379 -0
- package/dist/docs/components/sidebar-menu.md +922 -0
- package/dist/docs/components/skeleton.md +517 -0
- package/dist/docs/components/slider.md +539 -0
- package/dist/docs/components/snippet.md +424 -0
- package/dist/docs/components/spinner.md +216 -0
- package/dist/docs/components/split-bar.md +139 -0
- package/dist/docs/components/split-button.md +464 -0
- package/dist/docs/components/stack.md +299 -0
- package/dist/docs/components/status-chip.md +226 -0
- package/dist/docs/components/status-indicator.md +119 -0
- package/dist/docs/components/status-row.md +174 -0
- package/dist/docs/components/stepper.md +451 -0
- package/dist/docs/components/success-hero.md +157 -0
- package/dist/docs/components/surface.md +639 -0
- package/dist/docs/components/switch.md +472 -0
- package/dist/docs/components/table.md +967 -0
- package/dist/docs/components/tabs.md +627 -0
- package/dist/docs/components/tempo-logo.md +185 -0
- package/dist/docs/components/text-input.md +624 -0
- package/dist/docs/components/textarea.md +352 -0
- package/dist/docs/components/toast.md +535 -0
- package/dist/docs/components/token-logo.md +371 -0
- package/dist/docs/components/token.md +247 -0
- package/dist/docs/components/tooltip.md +490 -0
- package/dist/docs/components/verification-code.md +116 -0
- package/dist/docs/components/visual.md +249 -0
- package/dist/docs/components.md +203 -0
- package/dist/docs/foundations/accessibility.md +377 -0
- package/dist/docs/foundations/agents.md +150 -0
- package/dist/docs/foundations/color.md +522 -0
- package/dist/docs/foundations/content/tone.md +194 -0
- package/dist/docs/foundations/content/voice.md +194 -0
- package/dist/docs/foundations/content/writing-style.md +406 -0
- package/dist/docs/foundations/iconography.md +282 -0
- package/dist/docs/foundations/intent.md +360 -0
- package/dist/docs/foundations/layout.md +508 -0
- package/dist/docs/foundations/linter.md +296 -0
- package/dist/docs/foundations/motion.md +151 -0
- package/dist/docs/foundations/radius-and-padding.md +86 -0
- package/dist/docs/foundations/typography.md +266 -0
- package/dist/docs/index.md +137 -0
- package/dist/docs/introduction/faq.md +132 -0
- package/dist/docs/introduction/getting-started.md +377 -0
- package/dist/docs/patterns/blockchain-values.md +709 -0
- package/dist/docs/patterns/dashboards.md +638 -0
- package/dist/docs/patterns/destructive-actions.md +626 -0
- package/dist/docs/patterns/empty-states.md +612 -0
- package/dist/docs/patterns/errors-and-validation.md +808 -0
- package/dist/docs/patterns/feedback-and-status.md +518 -0
- package/dist/docs/patterns/forms.md +679 -0
- package/dist/docs/patterns/loading.md +630 -0
- package/dist/docs/patterns/navigation.md +397 -0
- package/dist/docs/patterns/payments.md +765 -0
- package/dist/docs/patterns/responsive-layout.md +997 -0
- package/dist/docs/patterns/tables-and-lists.md +964 -0
- package/dist/docs/products/tempo-console.md +671 -0
- package/dist/docs/products/tempo-performance.md +437 -0
- package/dist/docs/products/tempo-wallet.md +438 -0
- package/dist/web/components/OtpField.css +1 -1
- package/dist/web/components/OtpField.d.ts +1 -1
- package/dist/web/components/OtpField.d.ts.map +1 -1
- package/dist/web/components/OtpField.js +3 -4
- package/dist/web/components/OtpField.js.map +1 -1
- package/dist/web/components/OtpField.js.zyzz.json +1 -1
- package/dist/web/index.js.zyzz.json +1 -1
- package/package.json +1 -1
- package/src/web/components/OtpField.tsx +2 -24
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
Order fields, labels, and actions so a person completes a form in one pass.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Ask only for the values the task needs. Remove every field you can derive, default, or ask for later.
|
|
8
|
+
|
|
9
|
+
This page covers layout, labels, save models, and actions. [Errors and Validation](/docs/patterns/errors-and-validation) covers checking values and reporting problems.
|
|
10
|
+
|
|
11
|
+
## Choosing a Control
|
|
12
|
+
|
|
13
|
+
Choose the control from the shape of the value, not from its look.
|
|
14
|
+
|
|
15
|
+
* **Free Text (TextInput):** For a name or address. Use a [Textarea](/docs/components/textarea) for a longer memo.
|
|
16
|
+
* **An Amount of Money (AmountInput):** [AmountInput](/docs/components/amount-input) sets a decimal keyboard and rejects anything that is not a number.
|
|
17
|
+
* **Five or Fewer Options (RadioGroup):** A [RadioGroup](/docs/components/radio-group) keeps every choice visible.
|
|
18
|
+
* **Many Options (Select):** A [Select](/docs/components/select). If people need to search, use a [Combobox](/docs/components/combobox).
|
|
19
|
+
|
|
20
|
+
**Do:** Show a short set of exclusive options in a [RadioGroup](/docs/components/radio-group).
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<Fieldset.Root>
|
|
24
|
+
<Fieldset.Legend>Network fee</Fieldset.Legend>
|
|
25
|
+
<RadioGroup.Root aria-label="Network fee" defaultValue="standard">
|
|
26
|
+
<Field.Root>
|
|
27
|
+
<Field.Item>
|
|
28
|
+
<RadioGroup.Item value="standard" />
|
|
29
|
+
<Field.Label>Standard</Field.Label>
|
|
30
|
+
</Field.Item>
|
|
31
|
+
</Field.Root>
|
|
32
|
+
<Field.Root>
|
|
33
|
+
<Field.Item>
|
|
34
|
+
<RadioGroup.Item value="fast" />
|
|
35
|
+
<Field.Label>Fast</Field.Label>
|
|
36
|
+
</Field.Item>
|
|
37
|
+
</Field.Root>
|
|
38
|
+
</RadioGroup.Root>
|
|
39
|
+
</Fieldset.Root>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Don't:** Use a [Checkbox](/docs/components/checkbox) for options that exclude each other.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
<Fieldset.Root>
|
|
46
|
+
<Fieldset.Legend>Network fee</Fieldset.Legend>
|
|
47
|
+
<Field.Root>
|
|
48
|
+
<Field.Item>
|
|
49
|
+
<Checkbox defaultChecked />
|
|
50
|
+
<Field.Label>Standard</Field.Label>
|
|
51
|
+
</Field.Item>
|
|
52
|
+
</Field.Root>
|
|
53
|
+
<Field.Root>
|
|
54
|
+
<Field.Item>
|
|
55
|
+
<Checkbox />
|
|
56
|
+
<Field.Label>Fast</Field.Label>
|
|
57
|
+
</Field.Item>
|
|
58
|
+
</Field.Root>
|
|
59
|
+
</Fieldset.Root>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Help Browsers Fill Values
|
|
63
|
+
|
|
64
|
+
Set `type`, `inputMode`, and `autoComplete` on each `TextInput.Control`, so browsers suggest saved values and phones show the matching keyboard.
|
|
65
|
+
|
|
66
|
+
* **Personal Data (autoComplete):** Name each value, such as `autoComplete="email"`, as WCAG 2.2 requires for personal data.
|
|
67
|
+
* **Machine Values (spellCheck):** Turn off spell check and set `autoComplete="off"` for addresses, API keys, and other machine values.
|
|
68
|
+
|
|
69
|
+
## Structure
|
|
70
|
+
|
|
71
|
+
1. **Title:** Names the task, such as New webhook, and states the result of submit.
|
|
72
|
+
2. **Fields:** One column, in answer order, with related fields grouped.
|
|
73
|
+
3. **Each field:** Label, control, then a description or an error.
|
|
74
|
+
4. **Actions:** After the last field, in the [action order](#order-actions).
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { Button, Checkbox, Field, Fieldset, Frame, Stack, style, Surface, Textarea, TextInput, Toast } from '@tempoxyz/ds'
|
|
78
|
+
import { useState } from 'react'
|
|
79
|
+
|
|
80
|
+
export const fill = true
|
|
81
|
+
const events = [
|
|
82
|
+
['payment.received', 'Payment received'],
|
|
83
|
+
['payment.sent', 'Payment sent'],
|
|
84
|
+
['transfer.failed', 'Transfer failed'],
|
|
85
|
+
] as const
|
|
86
|
+
|
|
87
|
+
function valid(url: string) {
|
|
88
|
+
return /^https:\/\/[^\s/]+\.[^\s]+$/.test(url)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function WebhookForm() {
|
|
92
|
+
const toast = Toast.useToast()
|
|
93
|
+
const [invalid, setInvalid] = useState(false)
|
|
94
|
+
const [pending, setPending] = useState(false)
|
|
95
|
+
const [submitted, setSubmitted] = useState(false)
|
|
96
|
+
const [url, setUrl] = useState('')
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Surface
|
|
100
|
+
{...styles.form()}
|
|
101
|
+
render={
|
|
102
|
+
<form
|
|
103
|
+
noValidate
|
|
104
|
+
onSubmit={(event) => {
|
|
105
|
+
event.preventDefault()
|
|
106
|
+
setSubmitted(true)
|
|
107
|
+
|
|
108
|
+
if (!valid(url)) {
|
|
109
|
+
setInvalid(true)
|
|
110
|
+
event.currentTarget.querySelector<HTMLInputElement>('input[name="url"]')?.focus()
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setPending(true)
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
setPending(false)
|
|
117
|
+
toast.add({ description: url, intent: 'positive', title: 'Webhook created' })
|
|
118
|
+
}, 1200)
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
}
|
|
122
|
+
>
|
|
123
|
+
<Frame.Root>
|
|
124
|
+
<Frame.Header>
|
|
125
|
+
<Frame.Title>New webhook</Frame.Title>
|
|
126
|
+
<Frame.Description>
|
|
127
|
+
Tempo sends a POST request to your endpoint for each event you choose.
|
|
128
|
+
</Frame.Description>
|
|
129
|
+
</Frame.Header>
|
|
130
|
+
<Fieldset.Root disabled={pending}>
|
|
131
|
+
<Fieldset.Legend>Endpoint</Fieldset.Legend>
|
|
132
|
+
<Field.Root invalid={invalid}>
|
|
133
|
+
<Field.Label>Endpoint URL</Field.Label>
|
|
134
|
+
<TextInput.Group>
|
|
135
|
+
<TextInput.Control
|
|
136
|
+
autoComplete="off"
|
|
137
|
+
name="url"
|
|
138
|
+
onValueChange={(value) => {
|
|
139
|
+
setUrl(value)
|
|
140
|
+
// After the first submit, the error clears as soon as the value is fixed.
|
|
141
|
+
if (submitted) setInvalid(!valid(value))
|
|
142
|
+
}}
|
|
143
|
+
placeholder="https://api.example.com/tempo"
|
|
144
|
+
required
|
|
145
|
+
spellCheck={false}
|
|
146
|
+
type="url"
|
|
147
|
+
value={url}
|
|
148
|
+
/>
|
|
149
|
+
</TextInput.Group>
|
|
150
|
+
<Field.Description>Use an HTTPS URL on a server you control.</Field.Description>
|
|
151
|
+
<Field.Error match={invalid}>Enter a URL that starts with https://.</Field.Error>
|
|
152
|
+
</Field.Root>
|
|
153
|
+
<Field.Root>
|
|
154
|
+
<Field.Label>Description (optional)</Field.Label>
|
|
155
|
+
<Textarea name="description" placeholder="Order fulfillment" rows={2} />
|
|
156
|
+
</Field.Root>
|
|
157
|
+
</Fieldset.Root>
|
|
158
|
+
<Fieldset.Root disabled={pending}>
|
|
159
|
+
<Fieldset.Legend>Events</Fieldset.Legend>
|
|
160
|
+
<Stack gap="12">
|
|
161
|
+
{events.map(([value, label]) => (
|
|
162
|
+
<Field.Root key={value}>
|
|
163
|
+
<Field.Item>
|
|
164
|
+
<Checkbox defaultChecked={value === 'payment.received'} name={value} />
|
|
165
|
+
<Field.Label>{label}</Field.Label>
|
|
166
|
+
</Field.Item>
|
|
167
|
+
</Field.Root>
|
|
168
|
+
))}
|
|
169
|
+
</Stack>
|
|
170
|
+
</Fieldset.Root>
|
|
171
|
+
<Frame.Footer>
|
|
172
|
+
<Frame.Actions {...styles.actions()}>
|
|
173
|
+
<Button type="button" variant="secondary">
|
|
174
|
+
Cancel
|
|
175
|
+
</Button>
|
|
176
|
+
<Button loading={pending} type="submit">
|
|
177
|
+
Create webhook
|
|
178
|
+
</Button>
|
|
179
|
+
</Frame.Actions>
|
|
180
|
+
</Frame.Footer>
|
|
181
|
+
</Frame.Root>
|
|
182
|
+
</Surface>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function Demo() {
|
|
187
|
+
return (
|
|
188
|
+
<div {...styles.page()}>
|
|
189
|
+
<Toast.Provider>
|
|
190
|
+
<WebhookForm />
|
|
191
|
+
<Toast.Viewport />
|
|
192
|
+
</Toast.Provider>
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
namespace styles {
|
|
198
|
+
// Below 640px the actions stack at full width with the primary action on top. From 640px they sit
|
|
199
|
+
// at the end at their content width.
|
|
200
|
+
export const actions = style({
|
|
201
|
+
flexDirection: 'column-reverse',
|
|
202
|
+
rowGap: '8',
|
|
203
|
+
selectors: { '& > *': { flexBasis: 'auto !custom', width: '100% !custom' } },
|
|
204
|
+
'@media (min-width: 40rem)': {
|
|
205
|
+
flexDirection: 'row',
|
|
206
|
+
justifyContent: 'flex-end',
|
|
207
|
+
selectors: { '& > *': { flexGrow: '0 !custom', width: 'auto !custom' } },
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
export const form = style({
|
|
212
|
+
boxSizing: 'border-box',
|
|
213
|
+
maxWidth: '30rem !custom',
|
|
214
|
+
width: '100% !custom',
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
export const page = style({
|
|
218
|
+
backgroundColor: 'background.primary',
|
|
219
|
+
display: 'grid',
|
|
220
|
+
justifyItems: 'center',
|
|
221
|
+
padding: '16',
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/WebhookForm.tsx)
|
|
227
|
+
|
|
228
|
+
This reference layout uses [Surface](/docs/components/surface), [Frame](/docs/components/frame), [Fieldset](/docs/components/fieldset), and [Field](/docs/components/field). Submit an empty URL to show the error, then fix the URL to clear it.
|
|
229
|
+
|
|
230
|
+
### Use One Column
|
|
231
|
+
|
|
232
|
+
Stack fields in a single column, in the order a person expects, such as recipient, then amount, then memo. Put two fields on one row only if they form one short value, such as a month and a year.
|
|
233
|
+
|
|
234
|
+
**Do:** Stack fields in one column, in the order you answer them.
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
/**
|
|
238
|
+
* Do example for "Use One Column" on the Forms page.
|
|
239
|
+
* Stack fields in one column, in the order you answer them.
|
|
240
|
+
* @module
|
|
241
|
+
*/
|
|
242
|
+
import { Field, Stack, style, TextInput } from '@tempoxyz/ds'
|
|
243
|
+
|
|
244
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
245
|
+
export function Demo() {
|
|
246
|
+
return (
|
|
247
|
+
<Stack gap="24" {...styles.fill()}>
|
|
248
|
+
<Field.Root>
|
|
249
|
+
<Field.Label>Recipient</Field.Label>
|
|
250
|
+
<TextInput.Group>
|
|
251
|
+
<TextInput.Control placeholder="0x…" />
|
|
252
|
+
</TextInput.Group>
|
|
253
|
+
</Field.Root>
|
|
254
|
+
<Field.Root>
|
|
255
|
+
<Field.Label>Amount</Field.Label>
|
|
256
|
+
<TextInput.Group>
|
|
257
|
+
<TextInput.Control inputMode="decimal" placeholder="0.00" />
|
|
258
|
+
<TextInput.Suffix>USDC</TextInput.Suffix>
|
|
259
|
+
</TextInput.Group>
|
|
260
|
+
</Field.Root>
|
|
261
|
+
</Stack>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
namespace styles {
|
|
266
|
+
export const fill = style({ width: '100% !custom' })
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Don't:** Arrange unrelated fields in a grid, where the reading order is unclear.
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
/**
|
|
274
|
+
* Don't example for "Use One Column" on the Forms page.
|
|
275
|
+
* Don't arrange unrelated fields in a grid, where the reading order is unclear.
|
|
276
|
+
* @module
|
|
277
|
+
*/
|
|
278
|
+
import { Field, style, TextInput } from '@tempoxyz/ds'
|
|
279
|
+
|
|
280
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
281
|
+
export function Demo() {
|
|
282
|
+
return (
|
|
283
|
+
<div {...styles.grid()}>
|
|
284
|
+
<Field.Root>
|
|
285
|
+
<Field.Label>Recipient</Field.Label>
|
|
286
|
+
<TextInput.Group>
|
|
287
|
+
<TextInput.Control placeholder="0x…" />
|
|
288
|
+
</TextInput.Group>
|
|
289
|
+
</Field.Root>
|
|
290
|
+
<Field.Root>
|
|
291
|
+
<Field.Label>Amount</Field.Label>
|
|
292
|
+
<TextInput.Group>
|
|
293
|
+
<TextInput.Control inputMode="decimal" placeholder="0.00" />
|
|
294
|
+
</TextInput.Group>
|
|
295
|
+
</Field.Root>
|
|
296
|
+
<Field.Root>
|
|
297
|
+
<Field.Label>Memo</Field.Label>
|
|
298
|
+
<TextInput.Group>
|
|
299
|
+
<TextInput.Control placeholder="Invoice 1042" />
|
|
300
|
+
</TextInput.Group>
|
|
301
|
+
</Field.Root>
|
|
302
|
+
<Field.Root>
|
|
303
|
+
<Field.Label>Fee token</Field.Label>
|
|
304
|
+
<TextInput.Group>
|
|
305
|
+
<TextInput.Control placeholder="PathUSD" />
|
|
306
|
+
</TextInput.Group>
|
|
307
|
+
</Field.Root>
|
|
308
|
+
</div>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
namespace styles {
|
|
313
|
+
export const grid = style({
|
|
314
|
+
display: 'grid',
|
|
315
|
+
gap: '16',
|
|
316
|
+
gridTemplateColumns: 'repeat(2, minmax(0, 1fr)) !custom',
|
|
317
|
+
width: '100% !custom',
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Group Related Fields
|
|
323
|
+
|
|
324
|
+
Wrap the parts of one idea, such as an address, in a [Fieldset](/docs/components/fieldset) with a `Fieldset.Legend`. Set `variant` once on the fieldset, which passes it to every nested `Field.Root`. If a form outgrows two or three groups, split it into steps with a [Stepper](/docs/components/stepper).
|
|
325
|
+
|
|
326
|
+
**Do:** Group related fields under one legend and one variant.
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
<Fieldset.Root>
|
|
330
|
+
<Fieldset.Legend>Recipient</Fieldset.Legend>
|
|
331
|
+
<Field.Root>
|
|
332
|
+
<Field.Label>Name</Field.Label>
|
|
333
|
+
<TextInput.Group>
|
|
334
|
+
<TextInput.Control />
|
|
335
|
+
</TextInput.Group>
|
|
336
|
+
</Field.Root>
|
|
337
|
+
<Field.Root>
|
|
338
|
+
<Field.Label>Email</Field.Label>
|
|
339
|
+
<TextInput.Group>
|
|
340
|
+
<TextInput.Control type="email" />
|
|
341
|
+
</TextInput.Group>
|
|
342
|
+
</Field.Root>
|
|
343
|
+
</Fieldset.Root>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Don't:** Mix field variants within one group.
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
<Fieldset.Root>
|
|
350
|
+
<Fieldset.Legend>Recipient</Fieldset.Legend>
|
|
351
|
+
<Field.Root>
|
|
352
|
+
<Field.Label>Name</Field.Label>
|
|
353
|
+
<TextInput.Group>
|
|
354
|
+
<TextInput.Control />
|
|
355
|
+
</TextInput.Group>
|
|
356
|
+
</Field.Root>
|
|
357
|
+
<Field.Root>
|
|
358
|
+
<Field.Label>Email</Field.Label>
|
|
359
|
+
<TextInput.Group>
|
|
360
|
+
<TextInput.Control type="email" />
|
|
361
|
+
</TextInput.Group>
|
|
362
|
+
</Field.Root>
|
|
363
|
+
</Fieldset.Root>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Labels and Help Text
|
|
367
|
+
|
|
368
|
+
Every control needs a visible `Field.Label`, written as a short noun. Put format hints in `Field.Description`. A placeholder holds only an example value, such as `0x…` or `https://`.
|
|
369
|
+
|
|
370
|
+
### Put Labels Above Fields
|
|
371
|
+
|
|
372
|
+
[Field](/docs/components/field) puts the label 12px above every control. The label stays visible during input, and long labels wrap without narrowing the control.
|
|
373
|
+
|
|
374
|
+
* **Text Field (TextInput):** The label sits above the field, and the message below it.
|
|
375
|
+
* **Switch (Switch):** The label sits above the switch, as it does above a text field.
|
|
376
|
+
|
|
377
|
+
**Do:** Give every control a visible `Field.Label`, with format hints in `Field.Description`.
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
/**
|
|
381
|
+
* Do example for "Put Labels Above Fields" on the Forms page.
|
|
382
|
+
* Give every control a visible `Field.Label`, with format hints in `Field.Description`.
|
|
383
|
+
* @module
|
|
384
|
+
*/
|
|
385
|
+
import { Field, style, TextInput } from '@tempoxyz/ds'
|
|
386
|
+
|
|
387
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
388
|
+
export function Demo() {
|
|
389
|
+
return (
|
|
390
|
+
<Field.Root {...styles.fill()}>
|
|
391
|
+
<Field.Label>Recipient address</Field.Label>
|
|
392
|
+
<TextInput.Group>
|
|
393
|
+
<TextInput.Control placeholder="0x…" />
|
|
394
|
+
</TextInput.Group>
|
|
395
|
+
<Field.Description>A Tempo address that starts with 0x.</Field.Description>
|
|
396
|
+
</Field.Root>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
namespace styles {
|
|
401
|
+
export const fill = style({ width: '100% !custom' })
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Don't:** Use a placeholder as the label. It disappears on input.
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
/**
|
|
409
|
+
* Don't example for "Put Labels Above Fields" on the Forms page.
|
|
410
|
+
* Don't use a placeholder as the label. It disappears on input.
|
|
411
|
+
* @module
|
|
412
|
+
*/
|
|
413
|
+
import { style, TextInput } from '@tempoxyz/ds'
|
|
414
|
+
|
|
415
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
416
|
+
export function Demo() {
|
|
417
|
+
return (
|
|
418
|
+
<TextInput.Group {...styles.fill()}>
|
|
419
|
+
<TextInput.Control aria-label="Recipient address" placeholder="Recipient address" />
|
|
420
|
+
</TextInput.Group>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
namespace styles {
|
|
425
|
+
export const fill = style({ width: '100% !custom' })
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Mark Optional Fields
|
|
430
|
+
|
|
431
|
+
Most fields are required, so mark the exceptions. Set `required` on required controls, so assistive technology announces them. Set `noValidate` on the `<form>`, so browser validation popups do not replace `Field.Error`. If every field is optional, such as on a filter form, state this once in the description.
|
|
432
|
+
|
|
433
|
+
**Do:** Write (optional) in the label of each optional field.
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
/**
|
|
437
|
+
* Do example for "Mark Optional Fields" on the Forms page.
|
|
438
|
+
* Write (optional) in the label of each optional field.
|
|
439
|
+
* @module
|
|
440
|
+
*/
|
|
441
|
+
import { Field, Stack, style, TextInput } from '@tempoxyz/ds'
|
|
442
|
+
|
|
443
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
444
|
+
export function Demo() {
|
|
445
|
+
return (
|
|
446
|
+
<Stack gap="24" {...styles.fill()}>
|
|
447
|
+
<Field.Root>
|
|
448
|
+
<Field.Label>Key name</Field.Label>
|
|
449
|
+
<TextInput.Group>
|
|
450
|
+
<TextInput.Control defaultValue="Production" required />
|
|
451
|
+
</TextInput.Group>
|
|
452
|
+
</Field.Root>
|
|
453
|
+
<Field.Root>
|
|
454
|
+
<Field.Label>Expires (optional)</Field.Label>
|
|
455
|
+
<TextInput.Group>
|
|
456
|
+
<TextInput.Control placeholder="YYYY-MM-DD" />
|
|
457
|
+
</TextInput.Group>
|
|
458
|
+
</Field.Root>
|
|
459
|
+
</Stack>
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
namespace styles {
|
|
464
|
+
export const fill = style({ width: '100% !custom' })
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Don't:** Mark required fields with an asterisk and leave optional fields unmarked.
|
|
469
|
+
|
|
470
|
+
```tsx
|
|
471
|
+
/**
|
|
472
|
+
* Don't example for "Mark Optional Fields" on the Forms page.
|
|
473
|
+
* Don't mark required fields with an asterisk and leave optional fields unmarked.
|
|
474
|
+
* @module
|
|
475
|
+
*/
|
|
476
|
+
import { Field, Stack, style, TextInput } from '@tempoxyz/ds'
|
|
477
|
+
|
|
478
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
479
|
+
export function Demo() {
|
|
480
|
+
return (
|
|
481
|
+
<Stack gap="24" {...styles.fill()}>
|
|
482
|
+
<Field.Root>
|
|
483
|
+
<Field.Label>Key name *</Field.Label>
|
|
484
|
+
<TextInput.Group>
|
|
485
|
+
<TextInput.Control defaultValue="Production" />
|
|
486
|
+
</TextInput.Group>
|
|
487
|
+
</Field.Root>
|
|
488
|
+
<Field.Root>
|
|
489
|
+
<Field.Label>Expires</Field.Label>
|
|
490
|
+
<TextInput.Group>
|
|
491
|
+
<TextInput.Control placeholder="YYYY-MM-DD" />
|
|
492
|
+
</TextInput.Group>
|
|
493
|
+
</Field.Root>
|
|
494
|
+
</Stack>
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
namespace styles {
|
|
499
|
+
export const fill = style({ width: '100% !custom' })
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Settings
|
|
504
|
+
|
|
505
|
+
### Choose a Save Model
|
|
506
|
+
|
|
507
|
+
* **Save on Submit (Checkbox):** For values that work together. Nothing changes until you press the primary action.
|
|
508
|
+
* **Save on Change (Switch):** For independent settings. Confirm each change with a [Toast](/docs/components/toast), and revert it if it fails.
|
|
509
|
+
|
|
510
|
+
Do not mix the two in one section. If a person leaves a long form with unsaved changes, confirm the discard (see [Destructive Actions](/docs/patterns/destructive-actions)).
|
|
511
|
+
|
|
512
|
+
**Do:** Use a [Switch](/docs/components/switch) for a setting that applies immediately.
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
<Field.Root>
|
|
516
|
+
<Field.Label>Payment alerts</Field.Label>
|
|
517
|
+
<Switch defaultChecked />
|
|
518
|
+
<Field.Description>Email me when a payment is confirmed.</Field.Description>
|
|
519
|
+
</Field.Root>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Don't:** Pair a switch with a save button. If a change waits for submit, use a checkbox.
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
<Field.Root>
|
|
526
|
+
<Field.Label>Payment alerts</Field.Label>
|
|
527
|
+
<Switch defaultChecked />
|
|
528
|
+
<Field.Description>Email me when a payment is confirmed.</Field.Description>
|
|
529
|
+
</Field.Root>
|
|
530
|
+
<Button size="small">Save changes</Button>
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Submitting
|
|
534
|
+
|
|
535
|
+
Keep the submit button enabled and validate on press (see [Errors and Validation](/docs/patterns/errors-and-validation#keep-submit-buttons-enabled)). Set `loading` on it while the request runs, to block a second submit.
|
|
536
|
+
|
|
537
|
+
### Order Actions
|
|
538
|
+
|
|
539
|
+
This order applies to every action group, including forms, dialog footers, and toolbars. Keep it the same across every page of a product.
|
|
540
|
+
|
|
541
|
+
* **Alignment:** Align the group to the end, after the last field.
|
|
542
|
+
* **Order:** Put Cancel or Back before the primary action, so the primary action comes last.
|
|
543
|
+
* **Below 640px:** Stack the actions at full width, with the primary action first.
|
|
544
|
+
|
|
545
|
+
**Do:** Align actions to the end, with Cancel before the primary action.
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
/**
|
|
549
|
+
* Do example for "Order Actions" on the Forms page.
|
|
550
|
+
* Align actions to the end, with Cancel before the primary action.
|
|
551
|
+
* @module
|
|
552
|
+
*/
|
|
553
|
+
import { Button, Field, Stack, style, TextInput } from '@tempoxyz/ds'
|
|
554
|
+
|
|
555
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
556
|
+
export function Demo() {
|
|
557
|
+
return (
|
|
558
|
+
<Stack gap="24" {...styles.fill()}>
|
|
559
|
+
<Field.Root>
|
|
560
|
+
<Field.Label>Endpoint URL</Field.Label>
|
|
561
|
+
<TextInput.Group>
|
|
562
|
+
<TextInput.Control defaultValue="https://api.example.com/tempo" />
|
|
563
|
+
</TextInput.Group>
|
|
564
|
+
</Field.Root>
|
|
565
|
+
<div {...styles.actions()}>
|
|
566
|
+
<Button variant="secondary">Cancel</Button>
|
|
567
|
+
<Button>Create webhook</Button>
|
|
568
|
+
</div>
|
|
569
|
+
</Stack>
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
namespace styles {
|
|
574
|
+
export const actions = style({ columnGap: '8', display: 'flex', justifyContent: 'flex-end' })
|
|
575
|
+
|
|
576
|
+
export const fill = style({ width: '100% !custom' })
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Don't:** Put the primary action first and push Cancel to the far edge.
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
583
|
+
/**
|
|
584
|
+
* Don't example for "Order Actions" on the Forms page.
|
|
585
|
+
* Don't put the primary action first and push Cancel to the far edge.
|
|
586
|
+
* @module
|
|
587
|
+
*/
|
|
588
|
+
import { Button, Field, Stack, style, TextInput } from '@tempoxyz/ds'
|
|
589
|
+
|
|
590
|
+
/** Renders the static preview, which the guideline card shows inert. */
|
|
591
|
+
export function Demo() {
|
|
592
|
+
return (
|
|
593
|
+
<Stack gap="24" {...styles.fill()}>
|
|
594
|
+
<Field.Root>
|
|
595
|
+
<Field.Label>Endpoint URL</Field.Label>
|
|
596
|
+
<TextInput.Group>
|
|
597
|
+
<TextInput.Control defaultValue="https://api.example.com/tempo" />
|
|
598
|
+
</TextInput.Group>
|
|
599
|
+
</Field.Root>
|
|
600
|
+
<div {...styles.actions()}>
|
|
601
|
+
<Button>Create webhook</Button>
|
|
602
|
+
<Button variant="secondary">Cancel</Button>
|
|
603
|
+
</div>
|
|
604
|
+
</Stack>
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
namespace styles {
|
|
609
|
+
export const actions = style({ display: 'flex', justifyContent: 'space-between' })
|
|
610
|
+
|
|
611
|
+
export const fill = style({ width: '100% !custom' })
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Name the Result in the Button
|
|
616
|
+
|
|
617
|
+
Use one primary button, so Enter submits the form. Label it with the result, such as Create webhook.
|
|
618
|
+
|
|
619
|
+
**Do:** Name the result of the form in the button.
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
<Button>Send payment</Button>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Don't:** Use a generic label that hides what happens.
|
|
626
|
+
|
|
627
|
+
```tsx
|
|
628
|
+
<Button>Submit</Button>
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
## States
|
|
632
|
+
|
|
633
|
+
* **Default:** Prefill a value when it is known, such as the connected address.
|
|
634
|
+
* **Loading (Skeleton):** Wrap each control in a [Skeleton](/docs/components/skeleton) while saved values load.
|
|
635
|
+
* **Error (Field.Error):** An error under each invalid field (see [Errors and Validation](/docs/patterns/errors-and-validation)).
|
|
636
|
+
* **Disabled (disabled):** Set it on a fieldset to disable a whole group, and state the reason nearby.
|
|
637
|
+
* **Success (success):** Close the form and confirm with a [Toast](/docs/components/toast). `success` sets no ARIA state, so confirm in text too.
|
|
638
|
+
|
|
639
|
+
## Content
|
|
640
|
+
|
|
641
|
+
* **Nouns and Verbs:** Label fields with nouns and buttons with verbs.
|
|
642
|
+
* **One Sentence:** Keep each description to a single sentence.
|
|
643
|
+
|
|
644
|
+
## Accessibility
|
|
645
|
+
|
|
646
|
+
* **Visible Labels (label):** Pressing a label focuses its control. A field without a visible label, such as search, needs an `aria-label`.
|
|
647
|
+
* **Announced Help (aria-describedby):** Screen readers read the description and any shown error with the control.
|
|
648
|
+
* **Named Groups (Fieldset.Legend):** Names the native fieldset. Write it as the topic of the group.
|
|
649
|
+
* **Matching Order (Tab):** Keep the visual order the same as the DOM order, so focus moves down one field at a time.
|
|
650
|
+
|
|
651
|
+
## Related
|
|
652
|
+
|
|
653
|
+
[Field](/docs/components/field)
|
|
654
|
+
|
|
655
|
+
Use for labels, descriptions, and errors.
|
|
656
|
+
|
|
657
|
+
[Fieldset](/docs/components/fieldset)
|
|
658
|
+
|
|
659
|
+
Use for grouping fields.
|
|
660
|
+
|
|
661
|
+
[TextInput](/docs/components/text-input)
|
|
662
|
+
|
|
663
|
+
Use for text.
|
|
664
|
+
|
|
665
|
+
[AmountInput](/docs/components/amount-input)
|
|
666
|
+
|
|
667
|
+
Use for amounts of money.
|
|
668
|
+
|
|
669
|
+
[Button](/docs/components/button)
|
|
670
|
+
|
|
671
|
+
Use for submit and cancel actions.
|
|
672
|
+
|
|
673
|
+
[Errors and Validation](/docs/patterns/errors-and-validation)
|
|
674
|
+
|
|
675
|
+
Covers checking values.
|
|
676
|
+
|
|
677
|
+
[Loading](/docs/patterns/loading)
|
|
678
|
+
|
|
679
|
+
Covers submit and load states.
|