@tempoxyz/ds 0.3.10 → 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 (120) 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/package.json +1 -1
@@ -0,0 +1,765 @@
1
+ # Payments
2
+
3
+ Build send, receive, and deposit flows, from the amount to the receipt.
4
+
5
+ ## Overview
6
+
7
+ A payment moves money and cannot be undone. Each step shows exactly what will happen, catches mistakes before confirmation, and reports the outcome plainly. [Tempo Wallet](/docs/products/tempo-wallet) (wallet.tempo.xyz) uses three flows: Send, Receive, and Add funds. [Blockchain Values](/docs/patterns/blockchain-values) covers how each amount, address, and hash in these flows is formatted.
8
+
9
+ > [!NOTE]
10
+ > The live examples are reference layouts built from library components and `style`, not shipped components. They simulate the network, so no funds move.
11
+
12
+ ## Recommended Structure
13
+
14
+ Run each flow in focus: a centered column about 28rem wide with global navigation hidden, or a [Dialog](/docs/components/dialog), which becomes a bottom sheet on narrow screens. Mark progress with a [Stepper](/docs/components/stepper).
15
+
16
+ * **Details (1):** The asset, the amount, and the recipient.
17
+ * **Review (2):** The full payment, with the confirm action.
18
+ * **Status (3):** Pending, then sent or failed, on one screen.
19
+ * **Receipt (4):** A record that stays available from activity.
20
+
21
+ ```tsx
22
+ import { Amount, AmountInput, Button, Field, formatHex, Frame, Hex, Identicon, ListGroup, Stack, Stepper, style, Surface, TextInput, Token } from '@tempoxyz/ds'
23
+ import { CheckCircle } from '@tempoxyz/ds/icons'
24
+ import { useEffect, useRef, useState } from 'react'
25
+
26
+ export const fill = true
27
+ const balance = '1240.50'
28
+ const fee = '0.00003'
29
+ const hash = '0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060'
30
+
31
+ function valid(address: string) {
32
+ return /^0x[0-9a-fA-F]{40}$/.test(address)
33
+ }
34
+
35
+ // Adds decimal strings in 6-decimal base units, so the total never picks up floating-point error.
36
+ function add(a: string, b: string) {
37
+ const units = (value: string) => {
38
+ const [whole = '', fraction = ''] = value.split('.')
39
+ return BigInt(`${whole || '0'}${fraction.padEnd(6, '0').slice(0, 6)}`)
40
+ }
41
+ const digits = (units(a) + units(b)).toString().padStart(7, '0')
42
+ return `${digits.slice(0, -6)}.${digits.slice(-6).replace(/0+$/, '').padEnd(2, '0')}`
43
+ }
44
+
45
+ export function Demo() {
46
+ const [step, setStep] = useState<'details' | 'review' | 'sent'>('details')
47
+ const [amount, setAmount] = useState('')
48
+ const [recipient, setRecipient] = useState('')
49
+ // Errors show after a field loses focus or the form submits, never while you type.
50
+ const [touched, setTouched] = useState({ amount: false, recipient: false })
51
+ const [sending, setSending] = useState(false)
52
+ const heading = useRef<HTMLHeadingElement>(null)
53
+ const previous = useRef(step)
54
+
55
+ // Each step replaces the last, so focus moves to the new heading instead of falling back to the page.
56
+ useEffect(() => {
57
+ if (previous.current !== step) heading.current?.focus()
58
+ previous.current = step
59
+ }, [step])
60
+
61
+ const amountError = (() => {
62
+ if (!touched.amount) return undefined
63
+ if (!Number(amount)) return 'Enter an amount to send.'
64
+ if (Number(amount) > Number(balance)) return 'Enter an amount up to 1,240.50 PathUSD.'
65
+ return undefined
66
+ })()
67
+ const recipientError =
68
+ touched.recipient && !valid(recipient)
69
+ ? 'Enter an address that starts with 0x and has 40 more characters.'
70
+ : undefined
71
+
72
+ if (step === 'sent')
73
+ return (
74
+ <Surface {...styles.card()}>
75
+ <Frame.Root>
76
+ <div {...styles.result()}>
77
+ <CheckCircle aria-hidden="true" {...styles.success()} />
78
+ <Frame.Title ref={heading} tabIndex={-1}>
79
+ Payment sent
80
+ </Frame.Title>
81
+ <Amount size="md" symbol="PathUSD" value={amount} />
82
+ <p {...styles.text()}>To {formatHex(recipient)}</p>
83
+ </div>
84
+ <ListGroup.Root {...styles.rows()}>
85
+ <ListGroup.Row>
86
+ <ListGroup.Label>Transaction</ListGroup.Label>
87
+ <Hex copy value={hash} />
88
+ </ListGroup.Row>
89
+ </ListGroup.Root>
90
+ <Frame.Footer>
91
+ <Button
92
+ onClick={() => {
93
+ setAmount('')
94
+ setRecipient('')
95
+ setTouched({ amount: false, recipient: false })
96
+ setStep('details')
97
+ }}
98
+ size="large"
99
+ >
100
+ Done
101
+ </Button>
102
+ </Frame.Footer>
103
+ </Frame.Root>
104
+ </Surface>
105
+ )
106
+
107
+ if (step === 'review')
108
+ return (
109
+ <Surface {...styles.card()}>
110
+ <Frame.Root>
111
+ <Stack gap="12">
112
+ <Stepper.Root aria-label="Send payment" count={2} value={2} />
113
+ <Frame.Title ref={heading} tabIndex={-1}>
114
+ Review payment
115
+ </Frame.Title>
116
+ </Stack>
117
+ <Amount size="lg" symbol="PathUSD" value={amount} />
118
+ {/* The review shows the whole address, so you can check it against where you got it. */}
119
+ <div {...styles.recipient()}>
120
+ <span {...styles.label()}>To</span>
121
+ <span {...styles.address()}>
122
+ <Identicon aria-hidden address={recipient} size="24" />
123
+ <code {...styles.code()}>{recipient}</code>
124
+ </span>
125
+ </div>
126
+ <ListGroup.Root {...styles.rows()}>
127
+ <ListGroup.Row>
128
+ <ListGroup.Label>Network fee</ListGroup.Label>
129
+ <Amount symbol="PathUSD" value={fee} />
130
+ </ListGroup.Row>
131
+ <ListGroup.Row>
132
+ <ListGroup.Label>Total</ListGroup.Label>
133
+ <Amount symbol="PathUSD" value={add(amount, fee)} />
134
+ </ListGroup.Row>
135
+ </ListGroup.Root>
136
+ <Frame.Footer {...styles.footer()}>
137
+ <Button
138
+ loading={sending}
139
+ onClick={() => {
140
+ setSending(true)
141
+ setTimeout(() => {
142
+ setSending(false)
143
+ setStep('sent')
144
+ }, 1500)
145
+ }}
146
+ size="large"
147
+ >
148
+ Confirm and send
149
+ </Button>
150
+ <Button disabled={sending} onClick={() => setStep('details')} variant="tertiary">
151
+ Edit payment
152
+ </Button>
153
+ </Frame.Footer>
154
+ </Frame.Root>
155
+ </Surface>
156
+ )
157
+
158
+ return (
159
+ <Surface
160
+ render={
161
+ <form
162
+ noValidate
163
+ onSubmit={(event) => {
164
+ event.preventDefault()
165
+ setTouched({ amount: true, recipient: true })
166
+ const amountValid = Number(amount) > 0 && Number(amount) <= Number(balance)
167
+ if (amountValid && valid(recipient)) return setStep('review')
168
+
169
+ // Focus moves to the first field that needs fixing.
170
+ const name = amountValid ? 'recipient' : 'amount'
171
+ event.currentTarget.querySelector<HTMLInputElement>(`input[name="${name}"]`)?.focus()
172
+ }}
173
+ />
174
+ }
175
+ {...styles.card()}
176
+ >
177
+ <Frame.Root>
178
+ <Stack gap="12">
179
+ <Stepper.Root aria-label="Send payment" count={2} value={1} />
180
+ <Frame.Title ref={heading} tabIndex={-1}>
181
+ Send
182
+ </Frame.Title>
183
+ </Stack>
184
+ <Field.Root invalid={amountError !== undefined}>
185
+ <AmountInput.Root
186
+ onValueChange={(value) => {
187
+ setAmount(value)
188
+ setTouched({ ...touched, amount: false })
189
+ }}
190
+ value={amount}
191
+ >
192
+ <Field.Label>Amount</Field.Label>
193
+ <AmountInput.Control
194
+ name="amount"
195
+ onBlur={() => setTouched({ ...touched, amount: amount !== '' })}
196
+ placeholder="0.00"
197
+ />
198
+ <Token readOnly symbol="PathUSD" />
199
+ <Field.Description>Balance: 1,240.50 PathUSD</Field.Description>
200
+ <Field.Error match={amountError !== undefined}>{amountError}</Field.Error>
201
+ </AmountInput.Root>
202
+ </Field.Root>
203
+ <Field.Root invalid={recipientError !== undefined}>
204
+ <Field.Label>Recipient</Field.Label>
205
+ <TextInput.Group>
206
+ <TextInput.Control
207
+ autoComplete="off"
208
+ name="recipient"
209
+ onBlur={() => setTouched({ ...touched, recipient: recipient !== '' })}
210
+ onValueChange={(value) => {
211
+ setRecipient(value.trim())
212
+ setTouched({ ...touched, recipient: false })
213
+ }}
214
+ placeholder="0x…"
215
+ spellCheck={false}
216
+ value={recipient}
217
+ />
218
+ </TextInput.Group>
219
+ <Field.Description>Paste a Tempo address.</Field.Description>
220
+ <Field.Error match={recipientError !== undefined}>{recipientError}</Field.Error>
221
+ </Field.Root>
222
+ <Frame.Footer>
223
+ <Button type="submit">Review payment</Button>
224
+ </Frame.Footer>
225
+ </Frame.Root>
226
+ </Surface>
227
+ )
228
+ }
229
+
230
+ namespace styles {
231
+ export const address = style({ alignItems: 'center', columnGap: '12', display: 'flex' })
232
+
233
+ export const card = style({
234
+ boxSizing: 'border-box',
235
+ marginInline: 'auto !custom',
236
+ maxWidth: '448',
237
+ width: '100% !custom',
238
+ })
239
+
240
+ export const code = style({ minWidth: '0', typography: 'mono.m1', wordBreak: 'break-all' })
241
+
242
+ export const footer = style({ rowGap: '8' })
243
+
244
+ export const label = style({ color: 'content.secondary', typography: 'body.b3' })
245
+
246
+ export const recipient = style({ display: 'flex', flexDirection: 'column', rowGap: '8' })
247
+
248
+ export const result = style({
249
+ alignItems: 'center',
250
+ display: 'flex',
251
+ flexDirection: 'column',
252
+ rowGap: '8',
253
+ textAlign: 'center',
254
+ })
255
+
256
+ // Surface pads 24px, so the rows reach its edges and keep their own 20px inset.
257
+ export const rows = style({ marginInline: '-24px !custom' })
258
+
259
+ export const success = style({ color: 'content.positive', fontSize: '36' })
260
+
261
+ export const text = style({ color: 'content.secondary', margin: '0', typography: 'body.b2' })
262
+ }
263
+ ```
264
+
265
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/PaymentsSend.tsx)
266
+
267
+ ## Enter the Amount
268
+
269
+ Place the [AmountInput](/docs/components/amount-input) first, in a [Field](/docs/components/field), with the `Field.Label` and `Field.Description` inside `AmountInput.Root`.
270
+
271
+ * **Unit:** Name the token beside the amount with a [Token](/docs/components/token), such as PathUSD.
272
+ * **Balance:** Show the available balance in `Field.Description`, such as Balance: 1,240.50 PathUSD.
273
+ * **Limit:** Check the amount against the balance on blur and on submit, and name the limit in the error, such as Enter an amount up to 1,240.50 PathUSD.
274
+ * **Several tokens:** Put a [Select](/docs/components/select) above the amount, with a [TokenLogo](/docs/components/token-logo) icon in `Select.ItemPrefix` and the balance in `Select.ItemSuffix`. Default to the token used last.
275
+
276
+ ## Enter the Recipient
277
+
278
+ * **Address input:** Use a [TextInput](/docs/components/text-input) labelled Recipient, with `autoComplete="off"`, spell check off, and a `0x…` placeholder.
279
+ * **Format check:** On blur, trim pasted spaces and check for `0x` plus 40 hex characters. Explain a bad address under the field.
280
+ * **Saved contacts:** If the address matches a saved contact, show the name beside it, as [Blockchain Values](/docs/patterns/blockchain-values#pair-addresses-with-names) describes.
281
+ * **New addresses:** If the account has never sent to the address, show a `warning` [Alert](/docs/components/alert), such as You haven't sent to this address before.
282
+
283
+ ## Review Before Confirming
284
+
285
+ The review step repeats every detail of the payment in [ListGroup](/docs/components/list-group), with a `ListGroup.Label` for each.
286
+
287
+ * **Amount (size=lg):** The amount and token, shown large as the main value of the review.
288
+ * **To (Full address):** The full address and any known contact name. Ask people to check all of it against its source.
289
+ * **Network Fee (Paid by):** The exact fee in the token that pays it. If a fee payer covers it, name the sponsor.
290
+ * **Total:** The amount plus the fee, if both use the same token. Otherwise, list them apart.
291
+ * **Memo:** The reference of the transfer, such as an invoice number, if one exists.
292
+
293
+ **Do:** Show the fee as its own row and the total beneath it.
294
+
295
+ ```tsx
296
+ /** Do example for "Payments" on the Payments page: show the fee as its own row and the total beneath it. @module */
297
+ import { Amount, ListGroup, style, Surface } from '@tempoxyz/ds'
298
+
299
+ /** Renders the static preview, which the guideline card shows inert. */
300
+ export function Demo() {
301
+ return (
302
+ <Surface size="small" {...styles.card()}>
303
+ <ListGroup.Root>
304
+ <ListGroup.Row>
305
+ <ListGroup.Label>Amount</ListGroup.Label>
306
+ <Amount symbol="PathUSD" value="250.00" />
307
+ </ListGroup.Row>
308
+ <ListGroup.Row>
309
+ <ListGroup.Label>Network fee</ListGroup.Label>
310
+ <Amount symbol="PathUSD" value="0.00003" />
311
+ </ListGroup.Row>
312
+ <ListGroup.Row>
313
+ <ListGroup.Label>Total</ListGroup.Label>
314
+ <Amount symbol="PathUSD" value="250.00003" />
315
+ </ListGroup.Row>
316
+ </ListGroup.Root>
317
+ </Surface>
318
+ )
319
+ }
320
+
321
+ namespace styles {
322
+ export const card = style({
323
+ boxShadow: '0 0 0 1px light-dark(#0000001a, #ffffff1f) !custom',
324
+ width: '17rem !custom',
325
+ })
326
+ }
327
+ ```
328
+
329
+ **Don't:** Fold the fee into a total without showing it.
330
+
331
+ ```tsx
332
+ /** Don't example for "Payments" on the Payments page. Don't fold the fee into a total without showing it. @module */
333
+ import { Amount, ListGroup, style, Surface } from '@tempoxyz/ds'
334
+
335
+ /** Renders the static preview, which the guideline card shows inert. */
336
+ export function Demo() {
337
+ return (
338
+ <Surface size="small" {...styles.card()}>
339
+ <ListGroup.Root>
340
+ <ListGroup.Row>
341
+ <ListGroup.Label>Total</ListGroup.Label>
342
+ <Amount symbol="PathUSD" value="250.00003" />
343
+ </ListGroup.Row>
344
+ </ListGroup.Root>
345
+ </Surface>
346
+ )
347
+ }
348
+
349
+ namespace styles {
350
+ export const card = style({
351
+ boxShadow: '0 0 0 1px light-dark(#0000001a, #ffffff1f) !custom',
352
+ width: '17rem !custom',
353
+ })
354
+ }
355
+ ```
356
+
357
+ ### Confirm With One Large Action
358
+
359
+ End the review with one full-width `primary` [Button](/docs/components/button) with `size="large"`, labelled with the verb and the amount, such as Send 250.00 PathUSD or Confirm and send. Put a `text` Edit payment action beneath it. Set `loading` while the wallet signs and submits, so the button shows a spinner and blocks a second press.
360
+
361
+ **Do:** Use one large, specific confirm action with a quiet way back.
362
+
363
+ ```tsx
364
+ /**
365
+ * Do example for "Confirm With One Large Action" on the Payments page.
366
+ * Use one large, specific confirm action with a quiet way back.
367
+ * @module
368
+ */
369
+ import { Button, style } from '@tempoxyz/ds'
370
+
371
+ /** Renders the static preview, which the guideline card shows inert. */
372
+ export function Demo() {
373
+ return (
374
+ <div {...styles.footer()}>
375
+ <Button size="large">Confirm and send</Button>
376
+ <Button variant="tertiary">Edit payment</Button>
377
+ </div>
378
+ )
379
+ }
380
+
381
+ namespace styles {
382
+ export const footer = style({
383
+ display: 'flex',
384
+ flexDirection: 'column',
385
+ rowGap: '8',
386
+ width: '16rem !custom',
387
+ })
388
+ }
389
+ ```
390
+
391
+ **Don't:** Place a small, generic confirm among other actions.
392
+
393
+ ```tsx
394
+ /**
395
+ * Don't example for "Confirm With One Large Action" on the Payments page.
396
+ * Don't place a small, generic confirm among other actions.
397
+ * @module
398
+ */
399
+ import { Button, style } from '@tempoxyz/ds'
400
+
401
+ /** Renders the static preview, which the guideline card shows inert. */
402
+ export function Demo() {
403
+ return (
404
+ <div {...styles.footer()}>
405
+ <Button size="small" variant="secondary">
406
+ Edit
407
+ </Button>
408
+ <Button size="small" variant="secondary">
409
+ Save
410
+ </Button>
411
+ <Button size="small">OK</Button>
412
+ </div>
413
+ )
414
+ }
415
+
416
+ namespace styles {
417
+ export const footer = style({
418
+ display: 'flex',
419
+ gap: '8',
420
+ justifyContent: 'flex-end',
421
+ width: '16rem !custom',
422
+ })
423
+ }
424
+ ```
425
+
426
+ ## Report the Outcome
427
+
428
+ ```tsx
429
+ import { Amount, Button, ChoiceGroup, Frame, Spinner, Stack, style, Surface } from '@tempoxyz/ds'
430
+ import { AlertCircle, CheckCircle } from '@tempoxyz/ds/icons'
431
+ import { useState } from 'react'
432
+
433
+ export const fill = true
434
+ type State = 'failed' | 'pending' | 'sent'
435
+
436
+ const states = {
437
+ failed: {
438
+ description: 'Your balance hasn’t changed. Check the recipient and amount, then try again.',
439
+ primary: 'Try again',
440
+ secondary: 'Edit payment',
441
+ title: 'Payment failed',
442
+ },
443
+ pending: {
444
+ description: 'Sending to 0x71C7…976F. You can close this screen.',
445
+ primary: undefined,
446
+ secondary: undefined,
447
+ title: 'Sending payment',
448
+ },
449
+ sent: {
450
+ description: 'Sent to 0x71C7…976F.',
451
+ primary: 'Done',
452
+ secondary: 'View receipt',
453
+ title: 'Payment sent',
454
+ },
455
+ }
456
+
457
+ export function Demo() {
458
+ const [state, setState] = useState<State>('pending')
459
+ const content = states[state]
460
+
461
+ return (
462
+ <Stack gap="16">
463
+ <ChoiceGroup.Root
464
+ aria-label="Payment state"
465
+ onValueChange={(value) => setState(value as State)}
466
+ size="small"
467
+ value={state}
468
+ >
469
+ <ChoiceGroup.Item value="pending">Pending</ChoiceGroup.Item>
470
+ <ChoiceGroup.Item value="sent">Sent</ChoiceGroup.Item>
471
+ <ChoiceGroup.Item value="failed">Failed</ChoiceGroup.Item>
472
+ </ChoiceGroup.Root>
473
+ <Surface {...styles.card()}>
474
+ <Frame.Root>
475
+ {/* One live region holds the outcome, so each change is announced once. */}
476
+ <div aria-live="polite" {...styles.result()}>
477
+ {state === 'pending' && <Spinner aria-label="Sending" {...styles.icon()} />}
478
+ {state === 'sent' && <CheckCircle aria-hidden="true" {...styles.positive()} />}
479
+ {state === 'failed' && <AlertCircle aria-hidden="true" {...styles.negative()} />}
480
+ <Frame.Title>{content.title}</Frame.Title>
481
+ <Amount size="md" symbol="PathUSD" value="250.00" />
482
+ <p {...styles.text()}>{content.description}</p>
483
+ </div>
484
+ {content.primary && (
485
+ <Frame.Footer {...styles.footer()}>
486
+ <Button size="large">{content.primary}</Button>
487
+ <Button variant="tertiary">{content.secondary}</Button>
488
+ </Frame.Footer>
489
+ )}
490
+ </Frame.Root>
491
+ </Surface>
492
+ </Stack>
493
+ )
494
+ }
495
+
496
+ namespace styles {
497
+ export const card = style({
498
+ boxSizing: 'border-box',
499
+ marginInline: 'auto !custom',
500
+ maxWidth: '448',
501
+ width: '100% !custom',
502
+ })
503
+
504
+ export const footer = style({ rowGap: '8' })
505
+
506
+ export const icon = style({ marginBlockEnd: '8' })
507
+
508
+ export const negative = style({ color: 'content.negative', fontSize: '36', marginBlockEnd: '8' })
509
+
510
+ export const positive = style({ color: 'content.positive', fontSize: '36', marginBlockEnd: '8' })
511
+
512
+ export const result = style({
513
+ alignItems: 'center',
514
+ display: 'flex',
515
+ flexDirection: 'column',
516
+ minHeight: '200',
517
+ rowGap: '8',
518
+ textAlign: 'center',
519
+ })
520
+
521
+ export const text = style({ color: 'content.secondary', margin: '0', typography: 'body.b2' })
522
+ }
523
+ ```
524
+
525
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/PaymentsStatus.tsx)
526
+
527
+ * **Pending (Spinner):** Keep the amount and recipient on screen. State whether leaving is safe, such as You can close this screen.
528
+ * **Sent (Toast):** Show a check, the amount, the recipient, Done, and View receipt. If the person already left, confirm with a toast.
529
+ * **Failed:** State the result, the cause if known, and whether funds moved. Offer Try again with the same details, and Edit payment.
530
+
531
+ Announce the outcome once through a polite live region, separate from the busy confirm button. Each title states the result, such as Sent or Payment failed, so the color of the icon is not the only signal.
532
+
533
+ ## Keep a Receipt
534
+
535
+ A receipt records the payment in full, including values that the review step shortened: amount, fee, total, sender, recipient, the date with seconds and time zone, block, and transaction hash. Show the status in a [Badge](/docs/components/badge), such as Confirmed. [Blockchain Values](/docs/patterns/blockchain-values) covers the copy buttons and explorer links on each value.
536
+
537
+ ```tsx
538
+ import { Amount, Badge, Button, Hex, ListGroup, style, Surface } from '@tempoxyz/ds'
539
+ import { ExternalLink } from '@tempoxyz/ds/icons'
540
+
541
+ export const fill = true
542
+ export function Demo() {
543
+ return (
544
+ <Surface render={<section aria-labelledby="receipt-title" />} {...styles.card()}>
545
+ <Surface.Header>
546
+ <Surface.Title id="receipt-title">Receipt</Surface.Title>
547
+ <Badge>Confirmed</Badge>
548
+ </Surface.Header>
549
+ <Amount size="lg" symbol="PathUSD" value="250.00" />
550
+ <div {...styles.rows()}>
551
+ <ListGroup.Root>
552
+ <ListGroup.Row>
553
+ <ListGroup.Label>From</ListGroup.Label>
554
+ <Hex copy value="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" />
555
+ </ListGroup.Row>
556
+ <ListGroup.Row>
557
+ <ListGroup.Label>To</ListGroup.Label>
558
+ <Hex copy value="0x71C7656EC7ab88b098defB751B7401B5f6d8976F" />
559
+ </ListGroup.Row>
560
+ <ListGroup.Row>
561
+ <ListGroup.Label>Network fee</ListGroup.Label>
562
+ <Amount symbol="PathUSD" value="0.00003" />
563
+ </ListGroup.Row>
564
+ <ListGroup.Row>
565
+ <ListGroup.Label>Total</ListGroup.Label>
566
+ <Amount symbol="PathUSD" value="250.00003" />
567
+ </ListGroup.Row>
568
+ <ListGroup.Row>
569
+ <ListGroup.Label>Date</ListGroup.Label>
570
+ <time dateTime="2026-10-20T09:41:07Z">Oct 20, 2026, 09:41:07 UTC</time>
571
+ </ListGroup.Row>
572
+ <ListGroup.Row>
573
+ <ListGroup.Label>Block</ListGroup.Label>
574
+ <span {...styles.number()}>18,204,551</span>
575
+ </ListGroup.Row>
576
+ <ListGroup.Row>
577
+ <ListGroup.Label>Transaction</ListGroup.Label>
578
+ <Hex copy value="0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060" />
579
+ </ListGroup.Row>
580
+ </ListGroup.Root>
581
+ </div>
582
+ <div {...styles.actions()}>
583
+ <Button.Link
584
+ href="https://explore.tempo.xyz"
585
+ rel="noreferrer"
586
+ target="_blank"
587
+ variant="secondary"
588
+ >
589
+ View on explorer
590
+ <ExternalLink aria-hidden="true" />
591
+ </Button.Link>
592
+ </div>
593
+ </Surface>
594
+ )
595
+ }
596
+
597
+ namespace styles {
598
+ export const actions = style({ display: 'flex', marginBlockStart: '16' })
599
+
600
+ export const card = style({
601
+ boxSizing: 'border-box',
602
+ marginInline: 'auto !custom',
603
+ maxWidth: '448',
604
+ width: '100% !custom',
605
+ })
606
+
607
+ export const number = style({ fontVariantNumeric: 'tabular-nums !custom' })
608
+
609
+ // Surface pads 24px, so the rows reach its edges and keep their own 20px inset. A direct child
610
+ // `ListGroup` would bleed on its own, but this one follows the amount, so a wrapper sets the margins.
611
+ export const rows = style({ marginBlockStart: '16', marginInline: '-24px !custom' })
612
+ }
613
+ ```
614
+
615
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/PaymentsReceipt.tsx)
616
+
617
+ ## Receive and Deposit
618
+
619
+ Receive shows the address two ways: a [Qr](/docs/components/qr) for scanning, and a [Hex](/docs/components/hex) with `variant="filled"` across the full width for copying. Encode exactly the visible address. Name the code by purpose, such as Wallet address QR code, and state what the address accepts, such as Send stablecoins on Tempo to this address.
620
+
621
+ ```tsx
622
+ import { Alert, Hex, Qr, style, Surface } from '@tempoxyz/ds'
623
+
624
+ export const fill = true
625
+ const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
626
+
627
+ export function Demo() {
628
+ return (
629
+ <Surface render={<section aria-labelledby="receive-title" />} {...styles.card()}>
630
+ <Surface.Header>
631
+ <Surface.Title id="receive-title">Receive</Surface.Title>
632
+ </Surface.Header>
633
+ <div {...styles.body()}>
634
+ <Qr aria-label="Wallet address QR code" size="medium" value={address} />
635
+ {/* A filled Hex spans the card, so it truncates as little as possible and the whole row copies. */}
636
+ <div {...styles.address()}>
637
+ <Hex.Root size="large" value={address} variant="filled">
638
+ <Hex.Text />
639
+ <Hex.Copy aria-label="Copy address" />
640
+ </Hex.Root>
641
+ </div>
642
+ <Alert role="note">
643
+ <Alert.Icon />
644
+ <Alert.Content>Send stablecoins on Tempo to this address.</Alert.Content>
645
+ </Alert>
646
+ </div>
647
+ </Surface>
648
+ )
649
+ }
650
+
651
+ namespace styles {
652
+ export const address = style({ alignSelf: 'stretch' })
653
+
654
+ export const body = style({
655
+ alignItems: 'center',
656
+ display: 'flex',
657
+ flexDirection: 'column',
658
+ rowGap: '20',
659
+ })
660
+
661
+ export const card = style({
662
+ boxSizing: 'border-box',
663
+ marginInline: 'auto !custom',
664
+ maxWidth: '360',
665
+ width: '100% !custom',
666
+ })
667
+ }
668
+ ```
669
+
670
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/PaymentsReceive.tsx)
671
+
672
+ **Do:** Pair the QR code with the address and a copy action.
673
+
674
+ ```tsx
675
+ /**
676
+ * Do example for "Confirm With One Large Action" on the Payments page.
677
+ * Pair the QR code with the address and a copy action.
678
+ * @module
679
+ */
680
+ import { Hex, Qr, style } from '@tempoxyz/ds'
681
+
682
+ const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
683
+
684
+ /** Renders the static preview, which the guideline card shows inert. */
685
+ export function Demo() {
686
+ return (
687
+ <div {...styles.stack()}>
688
+ <Qr aria-label="Wallet address QR code" size="small" value={address} />
689
+ <Hex.Root size="small" value={address} variant="filled">
690
+ <Hex.Text />
691
+ <Hex.Copy aria-label="Copy address" />
692
+ </Hex.Root>
693
+ </div>
694
+ )
695
+ }
696
+
697
+ namespace styles {
698
+ export const stack = style({
699
+ alignItems: 'stretch',
700
+ display: 'flex',
701
+ flexDirection: 'column',
702
+ rowGap: '12',
703
+ width: '15rem !custom',
704
+ })
705
+ }
706
+ ```
707
+
708
+ **Don't:** Show a QR code alone. Many people cannot scan it.
709
+
710
+ ```tsx
711
+ /**
712
+ * Don't example for "Confirm With One Large Action" on the Payments page.
713
+ * Don't show a QR code alone. Many people cannot scan it.
714
+ * @module
715
+ */
716
+ import { Qr } from '@tempoxyz/ds'
717
+
718
+ /** Renders the static preview, which the guideline card shows inert. */
719
+ export function Demo() {
720
+ return <Qr size="small" value="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" />
721
+ }
722
+ ```
723
+
724
+ Add funds starts with the method. Use `Choicebox.RadioGroup` from [Choicebox](/docs/components/choicebox), with a title and arrival time for each option, such as Bank transfer or Debit card. A crypto deposit then shows the receive layout, with the minimum deposit and the accepted tokens.
725
+
726
+ ## Content
727
+
728
+ * **Name the movement:** Use verbs that name it: Send, Receive, Add funds, Withdraw.
729
+ * **Repeat the amount:** Show the amount with its token in every step. No screen reads only Confirm.
730
+ * **Name the cause:** If the cause of a failure is known, name it, such as Not enough PathUSD for the fee.
731
+
732
+ ## Accessibility
733
+
734
+ * **Move Focus:** After a failed submit, focus the first invalid field. On a step change, focus the new step's heading.
735
+ * **Busy Button (aria-busy):** A loading button is marked busy. Announce the outcome separately, such as in a toast.
736
+
737
+ ## Related
738
+
739
+ [AmountInput](/docs/components/amount-input)
740
+
741
+ Use for entering amounts.
742
+
743
+ [ListGroup](/docs/components/list-group)
744
+
745
+ Use for the review and receipt.
746
+
747
+ [Qr](/docs/components/qr)
748
+
749
+ Use for receiving funds.
750
+
751
+ [Blockchain Values](/docs/patterns/blockchain-values)
752
+
753
+ Covers addresses, hashes, and precision.
754
+
755
+ [Errors and Validation](/docs/patterns/errors-and-validation)
756
+
757
+ Covers error messages.
758
+
759
+ [Destructive Actions](/docs/patterns/destructive-actions)
760
+
761
+ Covers irreversible actions.
762
+
763
+ [Tempo Wallet](/docs/products/tempo-wallet)
764
+
765
+ Covers the wallet's flows.