@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.
Files changed (128) hide show
  1. package/dist/docs/AGENTS.md +9 -0
  2. package/dist/docs/components/accordion.md +489 -0
  3. package/dist/docs/components/address-card.md +147 -0
  4. package/dist/docs/components/agent-logo.md +194 -0
  5. package/dist/docs/components/alert-dialog.md +631 -0
  6. package/dist/docs/components/alert.md +448 -0
  7. package/dist/docs/components/amount-input.md +305 -0
  8. package/dist/docs/components/amount.md +794 -0
  9. package/dist/docs/components/app-shell.md +559 -0
  10. package/dist/docs/components/asset-chip.md +294 -0
  11. package/dist/docs/components/autocomplete.md +619 -0
  12. package/dist/docs/components/avatar.md +321 -0
  13. package/dist/docs/components/badge.md +339 -0
  14. package/dist/docs/components/badged-logo.md +116 -0
  15. package/dist/docs/components/banner.md +379 -0
  16. package/dist/docs/components/bar-chart.md +208 -0
  17. package/dist/docs/components/breadcrumbs.md +380 -0
  18. package/dist/docs/components/button-group.md +286 -0
  19. package/dist/docs/components/button.md +650 -0
  20. package/dist/docs/components/calendar.md +375 -0
  21. package/dist/docs/components/callout.md +96 -0
  22. package/dist/docs/components/checkbox-group.md +521 -0
  23. package/dist/docs/components/checkbox.md +467 -0
  24. package/dist/docs/components/choice-group.md +516 -0
  25. package/dist/docs/components/choicebox.md +641 -0
  26. package/dist/docs/components/code.md +100 -0
  27. package/dist/docs/components/combobox.md +939 -0
  28. package/dist/docs/components/container.md +257 -0
  29. package/dist/docs/components/copy-button.md +642 -0
  30. package/dist/docs/components/date-picker.md +514 -0
  31. package/dist/docs/components/dialog.md +1332 -0
  32. package/dist/docs/components/drawer.md +966 -0
  33. package/dist/docs/components/empty.md +340 -0
  34. package/dist/docs/components/field.md +570 -0
  35. package/dist/docs/components/fieldset.md +398 -0
  36. package/dist/docs/components/frame.md +434 -0
  37. package/dist/docs/components/gauge.md +299 -0
  38. package/dist/docs/components/get-started-visual.md +117 -0
  39. package/dist/docs/components/grid.md +288 -0
  40. package/dist/docs/components/hero-visual.md +155 -0
  41. package/dist/docs/components/hex.md +487 -0
  42. package/dist/docs/components/icon-tile.md +292 -0
  43. package/dist/docs/components/identicon.md +371 -0
  44. package/dist/docs/components/inline.md +233 -0
  45. package/dist/docs/components/inset.md +225 -0
  46. package/dist/docs/components/kbd.md +423 -0
  47. package/dist/docs/components/list-group.md +981 -0
  48. package/dist/docs/components/loading-dots.md +237 -0
  49. package/dist/docs/components/menu.md +934 -0
  50. package/dist/docs/components/native-select.md +204 -0
  51. package/dist/docs/components/network-logo.md +248 -0
  52. package/dist/docs/components/number-field.md +543 -0
  53. package/dist/docs/components/otp-field.md +616 -0
  54. package/dist/docs/components/pagination.md +516 -0
  55. package/dist/docs/components/popover.md +544 -0
  56. package/dist/docs/components/progress.md +388 -0
  57. package/dist/docs/components/qr.md +314 -0
  58. package/dist/docs/components/radio-group.md +528 -0
  59. package/dist/docs/components/secret-value.md +406 -0
  60. package/dist/docs/components/select.md +751 -0
  61. package/dist/docs/components/separator.md +379 -0
  62. package/dist/docs/components/sidebar-menu.md +922 -0
  63. package/dist/docs/components/skeleton.md +517 -0
  64. package/dist/docs/components/slider.md +539 -0
  65. package/dist/docs/components/snippet.md +424 -0
  66. package/dist/docs/components/spinner.md +216 -0
  67. package/dist/docs/components/split-bar.md +139 -0
  68. package/dist/docs/components/split-button.md +464 -0
  69. package/dist/docs/components/stack.md +299 -0
  70. package/dist/docs/components/status-chip.md +226 -0
  71. package/dist/docs/components/status-indicator.md +119 -0
  72. package/dist/docs/components/status-row.md +174 -0
  73. package/dist/docs/components/stepper.md +451 -0
  74. package/dist/docs/components/success-hero.md +157 -0
  75. package/dist/docs/components/surface.md +639 -0
  76. package/dist/docs/components/switch.md +472 -0
  77. package/dist/docs/components/table.md +967 -0
  78. package/dist/docs/components/tabs.md +627 -0
  79. package/dist/docs/components/tempo-logo.md +185 -0
  80. package/dist/docs/components/text-input.md +624 -0
  81. package/dist/docs/components/textarea.md +352 -0
  82. package/dist/docs/components/toast.md +535 -0
  83. package/dist/docs/components/token-logo.md +371 -0
  84. package/dist/docs/components/token.md +247 -0
  85. package/dist/docs/components/tooltip.md +490 -0
  86. package/dist/docs/components/verification-code.md +116 -0
  87. package/dist/docs/components/visual.md +249 -0
  88. package/dist/docs/components.md +203 -0
  89. package/dist/docs/foundations/accessibility.md +377 -0
  90. package/dist/docs/foundations/agents.md +150 -0
  91. package/dist/docs/foundations/color.md +522 -0
  92. package/dist/docs/foundations/content/tone.md +194 -0
  93. package/dist/docs/foundations/content/voice.md +194 -0
  94. package/dist/docs/foundations/content/writing-style.md +406 -0
  95. package/dist/docs/foundations/iconography.md +282 -0
  96. package/dist/docs/foundations/intent.md +360 -0
  97. package/dist/docs/foundations/layout.md +508 -0
  98. package/dist/docs/foundations/linter.md +296 -0
  99. package/dist/docs/foundations/motion.md +151 -0
  100. package/dist/docs/foundations/radius-and-padding.md +86 -0
  101. package/dist/docs/foundations/typography.md +266 -0
  102. package/dist/docs/index.md +137 -0
  103. package/dist/docs/introduction/faq.md +132 -0
  104. package/dist/docs/introduction/getting-started.md +377 -0
  105. package/dist/docs/patterns/blockchain-values.md +709 -0
  106. package/dist/docs/patterns/dashboards.md +638 -0
  107. package/dist/docs/patterns/destructive-actions.md +626 -0
  108. package/dist/docs/patterns/empty-states.md +612 -0
  109. package/dist/docs/patterns/errors-and-validation.md +808 -0
  110. package/dist/docs/patterns/feedback-and-status.md +518 -0
  111. package/dist/docs/patterns/forms.md +679 -0
  112. package/dist/docs/patterns/loading.md +630 -0
  113. package/dist/docs/patterns/navigation.md +397 -0
  114. package/dist/docs/patterns/payments.md +765 -0
  115. package/dist/docs/patterns/responsive-layout.md +997 -0
  116. package/dist/docs/patterns/tables-and-lists.md +964 -0
  117. package/dist/docs/products/tempo-console.md +671 -0
  118. package/dist/docs/products/tempo-performance.md +437 -0
  119. package/dist/docs/products/tempo-wallet.md +438 -0
  120. package/dist/web/components/OtpField.css +1 -1
  121. package/dist/web/components/OtpField.d.ts +1 -1
  122. package/dist/web/components/OtpField.d.ts.map +1 -1
  123. package/dist/web/components/OtpField.js +3 -4
  124. package/dist/web/components/OtpField.js.map +1 -1
  125. package/dist/web/components/OtpField.js.zyzz.json +1 -1
  126. package/dist/web/index.js.zyzz.json +1 -1
  127. package/package.json +1 -1
  128. package/src/web/components/OtpField.tsx +2 -24
@@ -0,0 +1,808 @@
1
+ # Errors and Validation
2
+
3
+ Check values at the right moment, and state how to fix each problem.
4
+
5
+ ## Overview
6
+
7
+ An error message states what happened and how to fix it, beside the problem. It never blames the reader and never discards what they entered. Problems come from three sources:
8
+
9
+ * **Validation:** An entered value is unusable, such as a malformed address. Show it on the field.
10
+ * **Network Failure:** A request did not complete, so nothing changed. Show it beside the failed action.
11
+ * **Transaction Failure (Failed):** The network received a transaction and rejected it, such as a transfer without funds for its fee.
12
+
13
+ [Forms](/docs/patterns/forms) covers layout. [Feedback and Status](/docs/patterns/feedback-and-status) defines the Couldn't load and Failed states.
14
+
15
+ ## Structure
16
+
17
+ 1. **Field errors:** Each invalid field shows its own `Field.Error`, which replaces the field's description.
18
+ 2. **Summary:** If a submit finds two or more problems, a negative [Alert](/docs/components/alert) at the top lists them.
19
+ 3. **Failed request:** The form keeps its values, and an [Alert](/docs/components/alert) above the actions states the failure.
20
+ 4. **Retry:** The primary action stays enabled, so a second press retries.
21
+
22
+ ```tsx
23
+ import { Alert, AmountInput, Button, Field, Frame, style, Surface, TextInput, Toast, Token } from '@tempoxyz/ds'
24
+ import { useRef, useState } from 'react'
25
+
26
+ export const fill = true
27
+ const balance = 1250
28
+
29
+ type Errors = { amount?: string | undefined; recipient?: string | undefined }
30
+
31
+ type Problem = { id: string; message: string }
32
+
33
+ function recipientError(value: string) {
34
+ if (value === '') return "Enter the recipient's address."
35
+ if (!/^0x[0-9a-fA-F]{40}$/.test(value)) return 'Enter a 42-character address that starts with 0x.'
36
+ return undefined
37
+ }
38
+
39
+ function amountError(value: string) {
40
+ if (value === '' || Number(value) === 0) return 'Enter an amount greater than 0.'
41
+ if (Number(value) > balance) return 'Enter 1,250.00 USDC or less, your available balance.'
42
+ return undefined
43
+ }
44
+
45
+ function PaymentForm() {
46
+ const toast = Toast.useToast()
47
+ const summaryRef = useRef<HTMLDivElement>(null)
48
+ const [amount, setAmount] = useState('')
49
+ const [errors, setErrors] = useState<Errors>({})
50
+ const [pending, setPending] = useState(false)
51
+ const [recipient, setRecipient] = useState('')
52
+ const [summary, setSummary] = useState(false)
53
+
54
+ const problems: Problem[] = []
55
+ if (errors.recipient) problems.push({ id: 'recipient', message: errors.recipient })
56
+ if (errors.amount) problems.push({ id: 'amount', message: errors.amount })
57
+
58
+ return (
59
+ <Surface
60
+ {...styles.form()}
61
+ render={
62
+ <form
63
+ noValidate
64
+ onSubmit={(event) => {
65
+ event.preventDefault()
66
+
67
+ const next = { amount: amountError(amount), recipient: recipientError(recipient) }
68
+ setErrors(next)
69
+
70
+ const count = [next.recipient, next.amount].filter(Boolean).length
71
+ setSummary(count > 1)
72
+
73
+ // One problem moves focus to its field. Several move focus to the summary that lists them.
74
+ // The summary mounts on this render, so focus moves to it once it exists.
75
+ if (count > 1) return void setTimeout(() => summaryRef.current?.focus())
76
+ if (next.recipient) return void document.getElementById('recipient')?.focus()
77
+ if (next.amount) return void document.getElementById('amount')?.focus()
78
+
79
+ setPending(true)
80
+ setTimeout(() => {
81
+ setPending(false)
82
+ toast.add({
83
+ description: `${amount} USDC to ${recipient.slice(0, 6)}…${recipient.slice(-4)}`,
84
+ intent: 'positive',
85
+ title: 'Payment sent',
86
+ })
87
+ }, 1200)
88
+ }}
89
+ />
90
+ }
91
+ >
92
+ <Frame.Root>
93
+ <Frame.Header>
94
+ <Frame.Title>Send payment</Frame.Title>
95
+ </Frame.Header>
96
+ {summary && problems.length > 0 && (
97
+ <Alert intent="negative" ref={summaryRef} tabIndex={-1}>
98
+ <Alert.Icon />
99
+ <Alert.Content>
100
+ <p {...styles.heading()}>
101
+ {problems.length === 1 ? 'Fix 1 problem' : `Fix ${problems.length} problems`} to
102
+ send this payment
103
+ </p>
104
+ <ul {...styles.list()}>
105
+ {problems.map((problem) => (
106
+ <li key={problem.id}>
107
+ <a
108
+ {...styles.link()}
109
+ href={`#${problem.id}`}
110
+ onClick={(event) => {
111
+ event.preventDefault()
112
+ document.getElementById(problem.id)?.focus()
113
+ }}
114
+ >
115
+ {problem.message}
116
+ </a>
117
+ </li>
118
+ ))}
119
+ </ul>
120
+ </Alert.Content>
121
+ </Alert>
122
+ )}
123
+ <Field.Root invalid={!!errors.recipient}>
124
+ <Field.Label>Recipient</Field.Label>
125
+ <TextInput.Group>
126
+ <TextInput.Control
127
+ autoComplete="off"
128
+ onBlur={(event) => {
129
+ // Format errors wait until you leave the field. An empty field waits for submit.
130
+ const value = event.currentTarget.value
131
+ if (value !== '') setErrors({ ...errors, recipient: recipientError(value) })
132
+ }}
133
+ onValueChange={(value) => {
134
+ setRecipient(value)
135
+ if (errors.recipient) setErrors({ ...errors, recipient: recipientError(value) })
136
+ }}
137
+ id="recipient"
138
+ placeholder="0x…"
139
+ spellCheck={false}
140
+ value={recipient}
141
+ />
142
+ </TextInput.Group>
143
+ <Field.Description>A Tempo address that starts with 0x.</Field.Description>
144
+ <Field.Error match={!!errors.recipient}>{errors.recipient}</Field.Error>
145
+ </Field.Root>
146
+ <Field.Root invalid={!!errors.amount}>
147
+ <AmountInput.Root
148
+ onValueChange={(value) => {
149
+ setAmount(value)
150
+ if (errors.amount) setErrors({ ...errors, amount: amountError(value) })
151
+ }}
152
+ value={amount}
153
+ >
154
+ <Field.Label>Amount</Field.Label>
155
+ <AmountInput.Control id="amount" placeholder="0.00" />
156
+ <Token readOnly symbol="USDC" />
157
+ <Field.Description>Available: 1,250.00 USDC</Field.Description>
158
+ <Field.Error match={!!errors.amount}>{errors.amount}</Field.Error>
159
+ </AmountInput.Root>
160
+ </Field.Root>
161
+ <Frame.Footer>
162
+ <Frame.Actions>
163
+ <Button loading={pending} type="submit">
164
+ Send payment
165
+ </Button>
166
+ </Frame.Actions>
167
+ </Frame.Footer>
168
+ </Frame.Root>
169
+ </Surface>
170
+ )
171
+ }
172
+
173
+ export function Demo() {
174
+ return (
175
+ <div {...styles.page()}>
176
+ <Toast.Provider>
177
+ <PaymentForm />
178
+ <Toast.Viewport />
179
+ </Toast.Provider>
180
+ </div>
181
+ )
182
+ }
183
+
184
+ namespace styles {
185
+ export const form = style({
186
+ boxSizing: 'border-box',
187
+ maxWidth: '28rem !custom',
188
+ width: '100% !custom',
189
+ })
190
+
191
+ export const heading = style({ margin: '0', typography: 'body.b2Strong' })
192
+
193
+ export const link = style({ color: 'inherit !custom', textUnderlineOffset: '2px !custom' })
194
+
195
+ export const list = style({ margin: '0', paddingInlineStart: '20' })
196
+
197
+ export const page = style({
198
+ backgroundColor: 'background.primary',
199
+ display: 'grid',
200
+ justifyItems: 'center',
201
+ padding: '16',
202
+ })
203
+ }
204
+ ```
205
+
206
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/PaymentValidation.tsx)
207
+
208
+ This reference layout uses [Field](/docs/components/field), [AmountInput](/docs/components/amount-input), and [Alert](/docs/components/alert). A short address shows a format error on Tab. An empty amount on submit shows the summary.
209
+
210
+ ## Guidelines
211
+
212
+ ### Place Errors Near Their Cause
213
+
214
+ * **One Field (Field.Error):** Set `invalid` on the [Field](/docs/components/field), with the error below.
215
+ * **A Section or Action (Alert):** Show a negative [Alert](/docs/components/alert) above what it describes.
216
+ * **The Whole Page (Banner):** Show a negative [Banner](/docs/components/banner) at the top of the page.
217
+ * **A Background Task (Toast):** Show a [Toast](/docs/components/toast). If a retry exists, add it as an action.
218
+
219
+ ### Validate at the Right Moment
220
+
221
+ Validate when the reader can act on the result. An empty required field is not an error until the reader tries to continue.
222
+
223
+ * **On Submit (Default):** Check every field, even empty required ones, in `onSubmit`. Pass each result to `invalid`.
224
+ * **On Blur (Tab):** Check a format when focus leaves the field. Pass `validate` and `validationMode="onBlur"`.
225
+ * **On Change (Clear only):** Clear a shown error as soon as the value is valid, without another submit.
226
+
227
+ Do not validate a format on every keystroke before blur, since every partial address is malformed.
228
+
229
+ **Do:** Check an amount against the balance on blur, so the limit shows before submit.
230
+
231
+ ```tsx
232
+ <Field.Root invalid>
233
+ <AmountInput.Root defaultValue="300">
234
+ <Field.Label>Amount</Field.Label>
235
+ <AmountInput.Control />
236
+ <Field.Error match>Exceeds your balance of $250.00.</Field.Error>
237
+ </AmountInput.Root>
238
+ </Field.Root>
239
+ ```
240
+
241
+ **Don't:** Flag an empty field before the reader tries to continue.
242
+
243
+ ```tsx
244
+ <Field.Root invalid>
245
+ <Field.Label>Recipient address</Field.Label>
246
+ <TextInput.Group>
247
+ <TextInput.Control />
248
+ </TextInput.Group>
249
+ <Field.Error match>Enter an address.</Field.Error>
250
+ </Field.Root>
251
+ ```
252
+
253
+ ### Write Errors That Say How to Fix the Value
254
+
255
+ State what to enter, in one sentence that starts with a verb, such as Enter a 42-character address that starts with 0x. Most messages need only the fix, not the fault.
256
+
257
+ * **Say How to Fix It:** Write "Enter a valid email", not "Invalid email" or "You entered an invalid email".
258
+ * **Name the Limit:** If a limit exists, name it, such as a length or the available balance.
259
+ * **Use Plain Words:** Never show raw codes, stack traces, or revert strings alone. Skip apologies and exclamation marks.
260
+ * **Keep the Value:** Keep the typed value, so the reader corrects it instead of starting over.
261
+ * **Reuse the Label:** Use the field's label in the message, so it also reads well in a summary.
262
+
263
+ **Do:** State what a valid value looks like.
264
+
265
+ ```tsx
266
+ /**
267
+ * Do example for "Write Errors That Say How to Fix the Value" on the Errors and Validation page.
268
+ * State what a valid value looks like.
269
+ * @module
270
+ */
271
+ import { Field, style, TextInput } from '@tempoxyz/ds'
272
+
273
+ /** Renders the static preview, which the guideline card shows inert. */
274
+ export function Demo() {
275
+ return (
276
+ <Field.Root invalid {...styles.fill()}>
277
+ <Field.Label>Recipient</Field.Label>
278
+ <TextInput.Group>
279
+ <TextInput.Control defaultValue="0x71C7656EC7ab88b098" />
280
+ </TextInput.Group>
281
+ <Field.Error match>Enter a 42-character address that starts with 0x.</Field.Error>
282
+ </Field.Root>
283
+ )
284
+ }
285
+
286
+ namespace styles {
287
+ export const fill = style({ width: '100% !custom' })
288
+ }
289
+ ```
290
+
291
+ **Don't:** Write generic errors such as Invalid input or Error.
292
+
293
+ ```tsx
294
+ /**
295
+ * Don't example for "Write Errors That Say How to Fix the Value" on the Errors and Validation page.
296
+ * Don't write generic errors such as Invalid input or Error.
297
+ * @module
298
+ */
299
+ import { Field, style, TextInput } from '@tempoxyz/ds'
300
+
301
+ /** Renders the static preview, which the guideline card shows inert. */
302
+ export function Demo() {
303
+ return (
304
+ <Field.Root invalid {...styles.fill()}>
305
+ <Field.Label>Recipient</Field.Label>
306
+ <TextInput.Group>
307
+ <TextInput.Control defaultValue="0x71C7656EC7ab88b098" />
308
+ </TextInput.Group>
309
+ <Field.Error match>Invalid input.</Field.Error>
310
+ </Field.Root>
311
+ )
312
+ }
313
+
314
+ namespace styles {
315
+ export const fill = style({ width: '100% !custom' })
316
+ }
317
+ ```
318
+
319
+ ### Pair Every Red Fill With Text
320
+
321
+ `invalid` fills the field red and plays a short nudge, which stops under reduced motion. Always render a `Field.Error` with the reason.
322
+
323
+ **Do:** Show a `Field.Error` with every invalid field.
324
+
325
+ ```tsx
326
+ /**
327
+ * Do example for "Pair Every Red Fill With Text" on the Errors and Validation page.
328
+ * Show a `Field.Error` with every invalid field.
329
+ * @module
330
+ */
331
+ import { AmountInput, Field, style, Token } from '@tempoxyz/ds'
332
+
333
+ /** Renders the static preview, which the guideline card shows inert. */
334
+ export function Demo() {
335
+ return (
336
+ <Field.Root invalid {...styles.fill()}>
337
+ <AmountInput.Root defaultValue="1500">
338
+ <Field.Label>Amount</Field.Label>
339
+ <AmountInput.Control />
340
+ <Token readOnly symbol="USDC" />
341
+ <Field.Error match>Enter 1,250.00 USDC or less, your available balance.</Field.Error>
342
+ </AmountInput.Root>
343
+ </Field.Root>
344
+ )
345
+ }
346
+
347
+ namespace styles {
348
+ export const fill = style({ width: '100% !custom' })
349
+ }
350
+ ```
351
+
352
+ **Don't:** Rely on the red fill alone to mark a problem.
353
+
354
+ ```tsx
355
+ /**
356
+ * Don't example for "Pair Every Red Fill With Text" on the Errors and Validation page.
357
+ * Don't rely on the red fill alone to mark a problem.
358
+ * @module
359
+ */
360
+ import { AmountInput, Field, style, Token } from '@tempoxyz/ds'
361
+
362
+ /** Renders the static preview, which the guideline card shows inert. */
363
+ export function Demo() {
364
+ return (
365
+ <Field.Root invalid {...styles.fill()}>
366
+ <AmountInput.Root defaultValue="1500">
367
+ <Field.Label>Amount</Field.Label>
368
+ <AmountInput.Control />
369
+ <Token readOnly symbol="USDC" />
370
+ </AmountInput.Root>
371
+ </Field.Root>
372
+ )
373
+ }
374
+
375
+ namespace styles {
376
+ export const fill = style({ width: '100% !custom' })
377
+ }
378
+ ```
379
+
380
+ ### Summarize Several Errors
381
+
382
+ * **One problem:** On submit, move focus to its field.
383
+ * **Two or more:** Render a negative [Alert](/docs/components/alert) at the top of the form. It names the count, lists each error as a link that focuses its field, and receives focus.
384
+
385
+ **Do:** Keep each `Field.Error` beside its field, and link each summary item to it.
386
+
387
+ ```tsx
388
+ /**
389
+ * Do example for "Summarize Several Errors" on the Errors and Validation page.
390
+ * Keep each `Field.Error` beside its field, and link each summary item to it.
391
+ * @module
392
+ */
393
+ import { Alert, AmountInput, Field, Stack, style, TextInput, Token } from '@tempoxyz/ds'
394
+
395
+ /** Renders the static preview, which the guideline card shows inert. */
396
+ export function Demo() {
397
+ return (
398
+ <Stack gap="16" {...styles.fill()}>
399
+ <Alert intent="negative">
400
+ <Alert.Icon />
401
+ <Alert.Content>
402
+ 2 fields need attention.
403
+ <ul {...styles.list()}>
404
+ <li>
405
+ <a href="#summary-recipient">Enter a 42-character address.</a>
406
+ </li>
407
+ <li>
408
+ <a href="#summary-amount">Enter an amount.</a>
409
+ </li>
410
+ </ul>
411
+ </Alert.Content>
412
+ </Alert>
413
+ <Field.Root invalid>
414
+ <Field.Label>Recipient address</Field.Label>
415
+ <TextInput.Group>
416
+ <TextInput.Control defaultValue="0x8f3a91c2" id="summary-recipient" />
417
+ </TextInput.Group>
418
+ <Field.Error match>Enter a 42-character address.</Field.Error>
419
+ </Field.Root>
420
+ <Field.Root invalid>
421
+ <AmountInput.Root>
422
+ <Field.Label>Amount</Field.Label>
423
+ <AmountInput.Control id="summary-amount" />
424
+ <Token readOnly symbol="USDC" />
425
+ <Field.Error match>Enter an amount.</Field.Error>
426
+ </AmountInput.Root>
427
+ </Field.Root>
428
+ </Stack>
429
+ )
430
+ }
431
+
432
+ namespace styles {
433
+ export const fill = style({ width: '100% !custom' })
434
+
435
+ export const list = style({ margin: '0', paddingInlineStart: '16' })
436
+ }
437
+ ```
438
+
439
+ **Don't:** List errors only in the summary, away from the fields.
440
+
441
+ ```tsx
442
+ /**
443
+ * Don't example for "Summarize Several Errors" on the Errors and Validation page.
444
+ * Don't list errors only in the summary, away from the fields.
445
+ * @module
446
+ */
447
+ import { Alert, AmountInput, Field, Stack, style, TextInput, Token } from '@tempoxyz/ds'
448
+
449
+ /** Renders the static preview, which the guideline card shows inert. */
450
+ export function Demo() {
451
+ return (
452
+ <Stack gap="16" {...styles.fill()}>
453
+ <Alert intent="negative">
454
+ <Alert.Icon />
455
+ <Alert.Content>
456
+ 2 fields need attention.
457
+ <ul {...styles.list()}>
458
+ <li>Enter a 42-character address.</li>
459
+ <li>Enter an amount.</li>
460
+ </ul>
461
+ </Alert.Content>
462
+ </Alert>
463
+ <Field.Root>
464
+ <Field.Label>Recipient address</Field.Label>
465
+ <TextInput.Group>
466
+ <TextInput.Control defaultValue="0x8f3a91c2" />
467
+ </TextInput.Group>
468
+ </Field.Root>
469
+ <Field.Root>
470
+ <AmountInput.Root>
471
+ <Field.Label>Amount</Field.Label>
472
+ <AmountInput.Control />
473
+ <Token readOnly symbol="USDC" />
474
+ </AmountInput.Root>
475
+ </Field.Root>
476
+ </Stack>
477
+ )
478
+ }
479
+
480
+ namespace styles {
481
+ export const fill = style({ width: '100% !custom' })
482
+
483
+ export const list = style({ margin: '0', paddingInlineStart: '16' })
484
+ }
485
+ ```
486
+
487
+ ### Report Failed Requests Where They Happen
488
+
489
+ If a submit fails on the network, keep the form open with its values, so a retry takes one press. State the failure in a negative [Alert](/docs/components/alert) above the actions.
490
+
491
+ * **Outcome First:** Lead with what did not happen, such as "Payment not sent".
492
+ * **Say Where the Money Is:** For payments, always state whether funds moved. An unsure reader often pays twice.
493
+ * **A Next Step:** If a retry can succeed, offer Try again. Otherwise, offer a different path.
494
+
495
+ **Do:** State what failed, what it means for the funds, and what to do next.
496
+
497
+ ```tsx
498
+ /**
499
+ * Do example for "Report Failed Requests Where They Happen" on the Errors and Validation page.
500
+ * State what failed, what it means for the funds, and what to do next.
501
+ * @module
502
+ */
503
+ import { Alert, style } from '@tempoxyz/ds'
504
+
505
+ /** Renders the static preview, which the guideline card shows inert. */
506
+ export function Demo() {
507
+ return (
508
+ <Alert intent="negative" {...styles.fill()}>
509
+ <Alert.Icon />
510
+ <Alert.Content>
511
+ Couldn't send payment. No funds moved. Check your connection, then try again.
512
+ </Alert.Content>
513
+ </Alert>
514
+ )
515
+ }
516
+
517
+ namespace styles {
518
+ export const fill = style({ width: '100% !custom' })
519
+ }
520
+ ```
521
+
522
+ **Don't:** Show Something went wrong, or a status code, as the whole message.
523
+
524
+ ```tsx
525
+ /**
526
+ * Don't example for "Report Failed Requests Where They Happen" on the Errors and Validation page.
527
+ * Don't show Something went wrong, or a status code, as the whole message.
528
+ * @module
529
+ */
530
+ import { Alert, style } from '@tempoxyz/ds'
531
+
532
+ /** Renders the static preview, which the guideline card shows inert. */
533
+ export function Demo() {
534
+ return (
535
+ <Alert intent="negative" {...styles.fill()}>
536
+ <Alert.Icon />
537
+ <Alert.Content>Something went wrong. Error 503.</Alert.Content>
538
+ </Alert>
539
+ )
540
+ }
541
+
542
+ namespace styles {
543
+ export const fill = style({ width: '100% !custom' })
544
+ }
545
+ ```
546
+
547
+ Do not report a failed payment only in a [Toast](/docs/components/toast). It dismisses itself before the reader decides what to do.
548
+
549
+ ### Report Page-Wide Errors in a Banner
550
+
551
+ If a problem affects the whole page, such as a lost connection, keep the [Banner](/docs/components/banner) visible until the problem resolves.
552
+
553
+ **Do:** Use a [Banner](/docs/components/banner) for a problem that affects the whole page.
554
+
555
+ ```tsx
556
+ <Banner intent="negative">
557
+ <Banner.Content>You are offline. Changes save when you reconnect.</Banner.Content>
558
+ </Banner>
559
+ ```
560
+
561
+ **Don't:** Report a field error in a [Banner](/docs/components/banner), away from the field.
562
+
563
+ ```tsx
564
+ <Banner intent="negative">
565
+ <Banner.Content>Enter a valid email.</Banner.Content>
566
+ </Banner>
567
+ ```
568
+
569
+ ### Explain Transaction Failures
570
+
571
+ A transaction that reached the network and failed differs from a request that never arrived. Mark it Failed with a [Badge](/docs/components/badge) and keep it in activity. A failed transfer moves no funds, but its network fee may still be charged, so state both.
572
+
573
+ **Do:** Show the reason a transaction failed beside its Failed status.
574
+
575
+ ```tsx
576
+ /**
577
+ * Do example for "Explain Transaction Failures" on the Errors and Validation page.
578
+ * Show the reason a transaction failed beside its Failed status.
579
+ * @module
580
+ */
581
+ import { Badge, ListGroup, style } from '@tempoxyz/ds'
582
+ import { AlertCircle } from '@tempoxyz/ds/icons'
583
+
584
+ /** Renders the static preview, which the guideline card shows inert. */
585
+ export function Demo() {
586
+ return (
587
+ <ListGroup.Root {...styles.fill()}>
588
+ <ListGroup.Row>
589
+ <ListGroup.Info>
590
+ <ListGroup.Title>Sent 25.00 USDC</ListGroup.Title>
591
+ <ListGroup.Description>
592
+ Not enough USDC left to cover the network fee
593
+ </ListGroup.Description>
594
+ </ListGroup.Info>
595
+ <Badge>
596
+ <AlertCircle />
597
+ Failed
598
+ </Badge>
599
+ </ListGroup.Row>
600
+ </ListGroup.Root>
601
+ )
602
+ }
603
+
604
+ namespace styles {
605
+ export const fill = style({ width: '100% !custom' })
606
+ }
607
+ ```
608
+
609
+ **Don't:** Show only a raw revert string as the reason.
610
+
611
+ ```tsx
612
+ /**
613
+ * Don't example for "Explain Transaction Failures" on the Errors and Validation page.
614
+ * Don't show only a raw revert string as the reason.
615
+ * @module
616
+ */
617
+ import { Badge, ListGroup, style } from '@tempoxyz/ds'
618
+ import { AlertCircle } from '@tempoxyz/ds/icons'
619
+
620
+ /** Renders the static preview, which the guideline card shows inert. */
621
+ export function Demo() {
622
+ return (
623
+ <ListGroup.Root {...styles.fill()}>
624
+ <ListGroup.Row>
625
+ <ListGroup.Info>
626
+ <ListGroup.Title>Sent 25.00 USDC</ListGroup.Title>
627
+ <ListGroup.Description>execution reverted (0x3ee5aeb5)</ListGroup.Description>
628
+ </ListGroup.Info>
629
+ <Badge>
630
+ <AlertCircle />
631
+ Failed
632
+ </Badge>
633
+ </ListGroup.Row>
634
+ </ListGroup.Root>
635
+ )
636
+ }
637
+
638
+ namespace styles {
639
+ export const fill = style({ width: '100% !custom' })
640
+ }
641
+ ```
642
+
643
+ ### Retry Safely
644
+
645
+ ```tsx
646
+ import { Alert, Button, Frame, ListGroup, style, Surface, Toast } from '@tempoxyz/ds'
647
+ import { useRef, useState } from 'react'
648
+
649
+ export const fill = true
650
+ function Review() {
651
+ const toast = Toast.useToast()
652
+ const attempts = useRef(0)
653
+ const [failed, setFailed] = useState(false)
654
+ const [pending, setPending] = useState(false)
655
+
656
+ return (
657
+ <Surface {...styles.card()}>
658
+ <Frame.Root>
659
+ <Frame.Header>
660
+ <Frame.Title>Review payment</Frame.Title>
661
+ <Frame.Description>Check the details, then send.</Frame.Description>
662
+ </Frame.Header>
663
+ <ListGroup.Root>
664
+ <ListGroup.Row>
665
+ <ListGroup.Label>To</ListGroup.Label>
666
+ <span {...styles.mono()}>0x71C7…976F</span>
667
+ </ListGroup.Row>
668
+ <ListGroup.Row>
669
+ <ListGroup.Label>Amount</ListGroup.Label>
670
+ 25.00 USDC
671
+ </ListGroup.Row>
672
+ <ListGroup.Row>
673
+ <ListGroup.Label>Network fee</ListGroup.Label>
674
+ 0.001 USDC
675
+ </ListGroup.Row>
676
+ </ListGroup.Root>
677
+ <Frame.Footer {...styles.footer()}>
678
+ {failed && (
679
+ <Alert intent="negative">
680
+ <Alert.Icon />
681
+ <Alert.Content>
682
+ Couldn't reach the Tempo network, so your payment wasn't sent and no funds moved.
683
+ Check your connection, then try again.
684
+ </Alert.Content>
685
+ </Alert>
686
+ )}
687
+ <Frame.Actions>
688
+ <Button disabled={pending} variant="secondary">
689
+ Back
690
+ </Button>
691
+ <Button
692
+ loading={pending}
693
+ onClick={() => {
694
+ attempts.current += 1
695
+ setPending(true)
696
+ setTimeout(() => {
697
+ setPending(false)
698
+ // The first attempt fails so you can see the retry. The second succeeds.
699
+ if (attempts.current % 2 === 1) return setFailed(true)
700
+ setFailed(false)
701
+ toast.add({ intent: 'positive', title: 'Payment sent' })
702
+ }, 1200)
703
+ }}
704
+ >
705
+ Send payment
706
+ </Button>
707
+ </Frame.Actions>
708
+ </Frame.Footer>
709
+ </Frame.Root>
710
+ </Surface>
711
+ )
712
+ }
713
+
714
+ export function Demo() {
715
+ return (
716
+ <div {...styles.page()}>
717
+ <Toast.Provider>
718
+ <Review />
719
+ <Toast.Viewport />
720
+ </Toast.Provider>
721
+ </div>
722
+ )
723
+ }
724
+
725
+ namespace styles {
726
+ export const card = style({
727
+ boxSizing: 'border-box',
728
+ maxWidth: '28rem !custom',
729
+ width: '100% !custom',
730
+ })
731
+
732
+ export const footer = style({ rowGap: '16' })
733
+
734
+ export const mono = style({ fontFamily: 'mono' })
735
+
736
+ export const page = style({
737
+ backgroundColor: 'background.primary',
738
+ display: 'grid',
739
+ justifyItems: 'center',
740
+ padding: '16',
741
+ })
742
+ }
743
+ ```
744
+
745
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/PaymentRetry.tsx)
746
+
747
+ The first press fails and the second succeeds.
748
+
749
+ * **Retry With the Primary Action (loading):** Let the primary action retry. Set `loading` during each attempt, and add no second Retry button.
750
+ * **Check Unknown Outcomes (Pending):** Retry only if the first attempt never reached the network. Otherwise, check the transaction status first.
751
+ * **Wait for a New Value:** Do not retry validation errors automatically. Nothing changes until the value does.
752
+ * **Point Elsewhere:** After repeated failures, point to a next step, such as a status page or support.
753
+
754
+ ### Keep Submit Buttons Enabled
755
+
756
+ A disabled button states no reason. An enabled one lets the reader submit and read every error.
757
+
758
+ **Do:** Keep the button enabled and show errors on submit.
759
+
760
+ ```tsx
761
+ <Button>Send payment</Button>
762
+ ```
763
+
764
+ **Don't:** Disable the button without stating what is missing.
765
+
766
+ ```tsx
767
+ <Button disabled>Send payment</Button>
768
+ ```
769
+
770
+ ## States
771
+
772
+ * **Default:** Fields with descriptions and no errors. Nothing is checked before interaction.
773
+ * **Loading (loading):** Earlier errors stay visible while a check or request runs.
774
+ * **Disabled:** Skip validation for disabled fields. Screen readers do not announce an error on a disabled control.
775
+ * **Success:** After a successful submit, confirm the result with a [Toast](/docs/components/toast) or the next screen.
776
+
777
+ ## Accessibility
778
+
779
+ * **Linked Errors (aria-describedby):** A shown error is read when its control takes focus. `invalid` sets `aria-invalid`. `success` sets no state.
780
+ * **Summary Links:** A negative alert announces the summary on render. Each link moves focus to its field.
781
+
782
+ [Feedback and Status](/docs/patterns/feedback-and-status#accessibility) covers how alerts and banners announce.
783
+
784
+ ## Related
785
+
786
+ [Field](/docs/components/field)
787
+
788
+ Use for labels and error messages.
789
+
790
+ [Alert](/docs/components/alert)
791
+
792
+ Use for summaries and failures.
793
+
794
+ [Toast](/docs/components/toast)
795
+
796
+ Use for background failures.
797
+
798
+ [Forms](/docs/patterns/forms)
799
+
800
+ Covers layout and actions.
801
+
802
+ [Feedback and Status](/docs/patterns/feedback-and-status)
803
+
804
+ Covers the Failed and Couldn't load states.
805
+
806
+ [Empty States](/docs/patterns/empty-states)
807
+
808
+ Covers regions that couldn't load.