@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,626 @@
1
+ # Destructive Actions
2
+
3
+ Match the safeguard on a destructive action to its impact and its reversibility.
4
+
5
+ ## Overview
6
+
7
+ A destructive action removes something the reader may want back, such as a webhook, an API key, a team member, or a pending payment. Weigh each action on two axes:
8
+
9
+ * **Reversible or Not:** Can the reader undo it or restore what was lost?
10
+ * **Impact:** Does it affect one item, other people, or funds?
11
+
12
+ Add friction in proportion to the risk. Too few safeguards lose data. Too many train readers to confirm without reading.
13
+
14
+ ## Structure
15
+
16
+ 1. **Trigger:** Names the action, such as Delete webhook, after the safe actions or at the end of a [Menu](/docs/components/menu).
17
+ 2. **Safeguard:** Undo, a confirmation, or a typed confirmation, chosen by risk.
18
+ 3. **Confirmation:** An [AlertDialog](/docs/components/alert-dialog) names the object, states the consequence, and repeats the verb on its button.
19
+ 4. **Result:** The item disappears, a [Toast](/docs/components/toast) confirms it, and focus moves to the list.
20
+
21
+ ```tsx
22
+ import { AlertDialog, Button, ButtonGroup } from '@tempoxyz/ds'
23
+
24
+ export function Demo() {
25
+ return (
26
+ <AlertDialog.Root>
27
+ <AlertDialog.Trigger render={<Button />}>Delete webhook</AlertDialog.Trigger>
28
+ <AlertDialog.Popup>
29
+ <AlertDialog.Header>
30
+ <AlertDialog.Title>Delete this webhook?</AlertDialog.Title>
31
+ <AlertDialog.Description>
32
+ Tempo stops sending payment events to this endpoint. You can add it again later.
33
+ </AlertDialog.Description>
34
+ </AlertDialog.Header>
35
+ {/* Cancel comes first, so it takes focus on open and Enter by habit keeps the webhook. */}
36
+ <ButtonGroup>
37
+ <AlertDialog.Close render={<Button size="large" variant="secondary" />}>
38
+ Cancel
39
+ </AlertDialog.Close>
40
+ <AlertDialog.Close render={<Button size="large" />}>Delete webhook</AlertDialog.Close>
41
+ </ButtonGroup>
42
+ </AlertDialog.Popup>
43
+ </AlertDialog.Root>
44
+ )
45
+ }
46
+ ```
47
+
48
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/DeleteWebhook.tsx)
49
+
50
+ This reference layout uses [AlertDialog](/docs/components/alert-dialog) and [Button](/docs/components/button). Cancel comes first in the markup, so it takes focus when the dialog opens.
51
+
52
+ ## Guidelines
53
+
54
+ ### Match the Safeguard to the Risk
55
+
56
+ Prefer Undo to confirmation wherever you can build it.
57
+
58
+ * **Easy to Reverse (Toast):** Act at once and offer Undo, such as for a removed contact or an archived notification.
59
+ * **Recoverable With Effort (AlertDialog):** Confirm first, and state how to restore it, such as for a webhook or a team member.
60
+ * **Irreversible (AlertDialog):** Always confirm, and state that the action cannot be undone.
61
+ * **Irreversible and Wide-Reaching (Type to confirm):** Ask the reader to type the object's name or authenticate again.
62
+
63
+ Actions that move funds, such as a payment or a swap, use the review step as the confirmation (see [Payments](/docs/patterns/payments)). Do not add an [AlertDialog](/docs/components/alert-dialog) after a payment review, since a second prompt trains readers to dismiss prompts.
64
+
65
+ ### Name the Action and Its Consequence
66
+
67
+ * **Title (Question):** Name the verb and the object, such as Revoke the Production key?
68
+ * **Description (1 to 2 sentences):** State the consequence, including whether it is reversible. Do not soften it.
69
+ * **Actions (secondary + primary):** Repeat the title's verb on the confirm button, in the [action order](/docs/patterns/forms#order-actions).
70
+
71
+ **Do:** Name the object and the consequence, and repeat the verb on the button.
72
+
73
+ ```tsx
74
+ /**
75
+ * Do example for "Name the Action and Its Consequence" on the Destructive Actions page.
76
+ * Name the object and the consequence, and repeat the verb on the button.
77
+ * @module
78
+ */
79
+ import { Button, style, Surface } from '@tempoxyz/ds'
80
+
81
+ /** Renders the static preview, which the guideline card shows inert. */
82
+ export function Demo() {
83
+ return (
84
+ <Surface size="medium" {...styles.fill()}>
85
+ <div {...styles.copy()}>
86
+ <p {...styles.title()}>Revoke the Production key?</p>
87
+ <p {...styles.text()}>
88
+ Apps that use this key stop working immediately. You can't undo this.
89
+ </p>
90
+ </div>
91
+ <div {...styles.actions()}>
92
+ <Button size="small" variant="secondary">
93
+ Cancel
94
+ </Button>
95
+ <Button size="small">Revoke key</Button>
96
+ </div>
97
+ </Surface>
98
+ )
99
+ }
100
+
101
+ namespace styles {
102
+ export const actions = style({
103
+ columnGap: '8',
104
+ display: 'flex',
105
+ justifyContent: 'flex-end',
106
+ marginTop: '16',
107
+ })
108
+
109
+ export const copy = style({ display: 'grid', gap: '4' })
110
+
111
+ export const fill = style({ width: '100% !custom' })
112
+
113
+ export const text = style({ color: 'content.secondary', margin: '0', typography: 'body.b2' })
114
+
115
+ export const title = style({ margin: '0', typography: 'body.b1Strong' })
116
+ }
117
+ ```
118
+
119
+ **Don't:** Ask a bare Are you sure? with Yes and No buttons.
120
+
121
+ ```tsx
122
+ /**
123
+ * Don't example for "Name the Action and Its Consequence" on the Destructive Actions page.
124
+ * Don't ask a bare Are you sure? with Yes and No buttons.
125
+ * @module
126
+ */
127
+ import { Button, style, Surface } from '@tempoxyz/ds'
128
+
129
+ /** Renders the static preview, which the guideline card shows inert. */
130
+ export function Demo() {
131
+ return (
132
+ <Surface size="medium" {...styles.fill()}>
133
+ <div {...styles.copy()}>
134
+ <p {...styles.title()}>Are you sure?</p>
135
+ <p {...styles.text()}>This action can't be undone.</p>
136
+ </div>
137
+ <div {...styles.actions()}>
138
+ <Button size="small" variant="secondary">
139
+ No
140
+ </Button>
141
+ <Button size="small">Yes</Button>
142
+ </div>
143
+ </Surface>
144
+ )
145
+ }
146
+
147
+ namespace styles {
148
+ export const actions = style({
149
+ columnGap: '8',
150
+ display: 'flex',
151
+ justifyContent: 'flex-end',
152
+ marginTop: '16',
153
+ })
154
+
155
+ export const copy = style({ display: 'grid', gap: '4' })
156
+
157
+ export const fill = style({ width: '100% !custom' })
158
+
159
+ export const text = style({ color: 'content.secondary', margin: '0', typography: 'body.b2' })
160
+
161
+ export const title = style({ margin: '0', typography: 'body.b1Strong' })
162
+ }
163
+ ```
164
+
165
+ ### Label Buttons With the Verb and Object
166
+
167
+ Use the same specific verb in the trigger, the dialog title, and the confirm button. Show the amount for any action that moves funds.
168
+
169
+ * **Delete:** The item is gone for good, and no one can restore it later.
170
+ * **Remove:** The item still exists elsewhere, such as a member removed from a team.
171
+ * **Revoke:** Access ends, such as an access key or an app's permission.
172
+ * **Cancel Payment:** A pending transfer stops. Never write only "Cancel", the label that closes the dialog.
173
+
174
+ ### Start Focus on the Safe Choice
175
+
176
+ `AlertDialog.Popup` focuses its first tabbable element on open. If that is the destructive button, pass a ref to the Cancel button as `initialFocus`. If the item and its trigger disappear, pass `finalFocus`.
177
+
178
+ * **On Open (initialFocus):** Focus Cancel, so a habitual Enter keeps the data.
179
+ * **After Removal (finalFocus):** Focus the next item or the list heading, not the body.
180
+
181
+ ### Type to Confirm Irreversible Actions
182
+
183
+ ```tsx
184
+ import { AlertDialog, Badge, Button, ButtonGroup, Field, ListGroup, style, Surface, TextInput, Toast } from '@tempoxyz/ds'
185
+ import { useState } from 'react'
186
+
187
+ export const fill = true
188
+ const key = 'Production'
189
+
190
+ function KeyRow() {
191
+ const toast = Toast.useToast()
192
+ const [invalid, setInvalid] = useState(false)
193
+ const [open, setOpen] = useState(false)
194
+ const [pending, setPending] = useState(false)
195
+ const [revoked, setRevoked] = useState(false)
196
+ const [typed, setTyped] = useState('')
197
+
198
+ return (
199
+ <ListGroup.Root dividers={false}>
200
+ <ListGroup.Row>
201
+ <ListGroup.Info>
202
+ <ListGroup.Title>{key}</ListGroup.Title>
203
+ <ListGroup.Description>Last used 2 minutes ago</ListGroup.Description>
204
+ </ListGroup.Info>
205
+ {revoked ? (
206
+ <Badge>Revoked</Badge>
207
+ ) : (
208
+ <AlertDialog.Root
209
+ onOpenChange={(next) => {
210
+ setOpen(next)
211
+ setInvalid(false)
212
+ setTyped('')
213
+ }}
214
+ open={open}
215
+ >
216
+ <AlertDialog.Trigger render={<Button size="small" />}>Revoke key</AlertDialog.Trigger>
217
+ <AlertDialog.Popup size="medium">
218
+ <form
219
+ noValidate
220
+ onSubmit={(event) => {
221
+ event.preventDefault()
222
+ if (typed !== key) return setInvalid(true)
223
+
224
+ setPending(true)
225
+ setTimeout(() => {
226
+ setPending(false)
227
+ setOpen(false)
228
+ setRevoked(true)
229
+ toast.add({ intent: 'positive', title: `${key} key revoked` })
230
+ }, 1000)
231
+ }}
232
+ >
233
+ <AlertDialog.Header>
234
+ <AlertDialog.Title>Revoke the {key} key?</AlertDialog.Title>
235
+ <AlertDialog.Description>
236
+ Apps that use this key stop working immediately. You can't undo this.
237
+ </AlertDialog.Description>
238
+ </AlertDialog.Header>
239
+ <AlertDialog.Body>
240
+ <Field.Root disabled={pending} invalid={invalid}>
241
+ <Field.Label>Type {key} to confirm</Field.Label>
242
+ <TextInput.Group>
243
+ <TextInput.Control
244
+ autoComplete="off"
245
+ onValueChange={(value) => {
246
+ setTyped(value)
247
+ if (invalid && value === key) setInvalid(false)
248
+ }}
249
+ spellCheck={false}
250
+ value={typed}
251
+ />
252
+ </TextInput.Group>
253
+ <Field.Error match={invalid}>Type {key} exactly as shown.</Field.Error>
254
+ </Field.Root>
255
+ </AlertDialog.Body>
256
+ <ButtonGroup>
257
+ <AlertDialog.Close
258
+ disabled={pending}
259
+ render={<Button size="large" variant="secondary" />}
260
+ >
261
+ Cancel
262
+ </AlertDialog.Close>
263
+ <Button loading={pending} size="large" type="submit">
264
+ Revoke key
265
+ </Button>
266
+ </ButtonGroup>
267
+ </form>
268
+ </AlertDialog.Popup>
269
+ </AlertDialog.Root>
270
+ )}
271
+ </ListGroup.Row>
272
+ </ListGroup.Root>
273
+ )
274
+ }
275
+
276
+ export function Demo() {
277
+ return (
278
+ <div {...styles.page()}>
279
+ <Toast.Provider>
280
+ <Surface {...styles.card()}>
281
+ <Surface.Header>
282
+ <Surface.Title>API keys</Surface.Title>
283
+ </Surface.Header>
284
+ <KeyRow />
285
+ </Surface>
286
+ <Toast.Viewport />
287
+ </Toast.Provider>
288
+ </div>
289
+ )
290
+ }
291
+
292
+ namespace styles {
293
+ export const card = style({
294
+ boxSizing: 'border-box',
295
+ maxWidth: '26rem !custom',
296
+ width: '100% !custom',
297
+ })
298
+
299
+ export const page = style({
300
+ backgroundColor: 'background.primary',
301
+ display: 'grid',
302
+ justifyItems: 'center',
303
+ padding: '16',
304
+ })
305
+ }
306
+ ```
307
+
308
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/RevokeKey.tsx)
309
+
310
+ Any text other than Production shows an error.
311
+
312
+ * **Ask for the Name (Field.Label):** Ask for the key's name, not DELETE, in a visible label.
313
+ * **Keep the Button Enabled (Field.Error):** If the text does not match, show an error.
314
+
315
+ **Do:** Ask for typed confirmation only for irreversible actions that affect other people or systems.
316
+
317
+ ```tsx
318
+ /**
319
+ * Do example for "Type to Confirm Irreversible Actions" on the Destructive Actions page.
320
+ * Ask for typed confirmation only for irreversible actions that affect other people or systems.
321
+ * @module
322
+ */
323
+ import { Field, style, TextInput } from '@tempoxyz/ds'
324
+
325
+ /** Renders the static preview, which the guideline card shows inert. */
326
+ export function Demo() {
327
+ return (
328
+ <Field.Root {...styles.fill()}>
329
+ <Field.Label>Type Production to confirm</Field.Label>
330
+ <TextInput.Group>
331
+ <TextInput.Control defaultValue="Produc" />
332
+ </TextInput.Group>
333
+ <Field.Description>Revoking this key stops every app that uses it.</Field.Description>
334
+ </Field.Root>
335
+ )
336
+ }
337
+
338
+ namespace styles {
339
+ export const fill = style({ width: '100% !custom' })
340
+ }
341
+ ```
342
+
343
+ **Don't:** Ask for typed confirmation on reversible actions, such as removing a contact.
344
+
345
+ ```tsx
346
+ /**
347
+ * Don't example for "Type to Confirm Irreversible Actions" on the Destructive Actions page.
348
+ * Don't ask for typed confirmation on reversible actions, such as removing a contact.
349
+ * @module
350
+ */
351
+ import { Button, Field, style, Surface, TextInput } from '@tempoxyz/ds'
352
+
353
+ /** Renders the static preview, which the guideline card shows inert. */
354
+ export function Demo() {
355
+ return (
356
+ <Surface size="medium" {...styles.fill()}>
357
+ <div {...styles.copy()}>
358
+ <p {...styles.title()}>Remove Ada from contacts?</p>
359
+ <p {...styles.text()}>You can add Ada again later.</p>
360
+ </div>
361
+ <Field.Root {...styles.field()}>
362
+ <Field.Label>Type "Ada" to confirm</Field.Label>
363
+ <TextInput.Group>
364
+ <TextInput.Control />
365
+ </TextInput.Group>
366
+ </Field.Root>
367
+ <div {...styles.actions()}>
368
+ <Button size="small" variant="secondary">
369
+ Cancel
370
+ </Button>
371
+ <Button size="small">Remove contact</Button>
372
+ </div>
373
+ </Surface>
374
+ )
375
+ }
376
+
377
+ namespace styles {
378
+ export const actions = style({
379
+ columnGap: '8',
380
+ display: 'flex',
381
+ justifyContent: 'flex-end',
382
+ marginTop: '16',
383
+ })
384
+
385
+ export const copy = style({ display: 'grid', gap: '4' })
386
+
387
+ export const field = style({ marginTop: '16' })
388
+
389
+ export const fill = style({ width: '100% !custom' })
390
+
391
+ export const text = style({ color: 'content.secondary', margin: '0', typography: 'body.b2' })
392
+
393
+ export const title = style({ margin: '0', typography: 'body.b1Strong' })
394
+ }
395
+ ```
396
+
397
+ ### Offer Undo When You Can
398
+
399
+ ```tsx
400
+ import { Button, Identicon, ListGroup, style, Surface, Toast } from '@tempoxyz/ds'
401
+ import { useState } from 'react'
402
+
403
+ export const fill = true
404
+ type Contact = { address: string; name: string }
405
+
406
+ const initial: readonly Contact[] = [
407
+ { address: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F', name: 'Ada' },
408
+ { address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', name: 'Payroll' },
409
+ { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', name: 'Treasury' },
410
+ ]
411
+
412
+ function Contacts() {
413
+ const toast = Toast.useToast()
414
+ const [contacts, setContacts] = useState(initial)
415
+
416
+ return (
417
+ <Surface {...styles.card()}>
418
+ <Surface.Header>
419
+ <Surface.Title>Contacts</Surface.Title>
420
+ </Surface.Header>
421
+ <ListGroup.Root>
422
+ {contacts.map((contact) => (
423
+ <ListGroup.Row key={contact.address}>
424
+ <Identicon address={contact.address} aria-hidden size="24" />
425
+ <ListGroup.Info>
426
+ <ListGroup.Title>{contact.name}</ListGroup.Title>
427
+ <ListGroup.Description>
428
+ {contact.address.slice(0, 6)}…{contact.address.slice(-4)}
429
+ </ListGroup.Description>
430
+ </ListGroup.Info>
431
+ <Button
432
+ aria-label={`Remove ${contact.name}`}
433
+ onClick={() => {
434
+ // Removing a contact is easy to reverse, so it happens at once with an Undo.
435
+ const index = contacts.indexOf(contact)
436
+ setContacts((current) => current.filter((item) => item !== contact))
437
+ toast.add({
438
+ action: {
439
+ label: 'Undo',
440
+ onClick: () =>
441
+ setContacts((current) => [
442
+ ...current.slice(0, index),
443
+ contact,
444
+ ...current.slice(index),
445
+ ]),
446
+ },
447
+ title: `${contact.name} removed`,
448
+ })
449
+ }}
450
+ size="small"
451
+ variant="tertiary"
452
+ >
453
+ Remove
454
+ </Button>
455
+ </ListGroup.Row>
456
+ ))}
457
+ </ListGroup.Root>
458
+ </Surface>
459
+ )
460
+ }
461
+
462
+ export function Demo() {
463
+ return (
464
+ <div {...styles.page()}>
465
+ <Toast.Provider>
466
+ <Contacts />
467
+ <Toast.Viewport />
468
+ </Toast.Provider>
469
+ </div>
470
+ )
471
+ }
472
+
473
+ namespace styles {
474
+ export const card = style({
475
+ boxSizing: 'border-box',
476
+ maxWidth: '26rem !custom',
477
+ width: '100% !custom',
478
+ })
479
+
480
+ export const page = style({
481
+ backgroundColor: 'background.primary',
482
+ display: 'grid',
483
+ justifyItems: 'center',
484
+ padding: '16',
485
+ })
486
+ }
487
+ ```
488
+
489
+ [View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/RemoveContact.tsx)
490
+
491
+ Undo in the [Toast](/docs/components/toast) restores the contact in place.
492
+
493
+ * **Skip the Confirmation (action):** If an action is easy to reverse, run it at once and offer Undo in a toast.
494
+ * **Keep Another Way Back:** A toast dismisses itself, so keep another way back, such as adding the contact again.
495
+
496
+ ### Save the Filled Button for the Decision
497
+
498
+ Give a reversible removal in a list a `tertiary` button, and keep the `primary` button for the confirm step. In a [Menu](/docs/components/menu), give destructive items `intent="negative"` and place them last, after a `Menu.Separator`.
499
+
500
+ **Do:** Use `tertiary` buttons for a reversible removal, and keep `primary` for the confirm step.
501
+
502
+ ```tsx
503
+ /**
504
+ * Do example for "Save the Filled Button for the Decision" on the Destructive Actions page.
505
+ * Use `tertiary` buttons for a reversible removal, and keep `primary` for the confirm step.
506
+ * @module
507
+ */
508
+ import { Button, ListGroup, style } from '@tempoxyz/ds'
509
+
510
+ /** Renders the static preview, which the guideline card shows inert. */
511
+ export function Demo() {
512
+ return (
513
+ <ListGroup.Root {...styles.fill()}>
514
+ {['Ada', 'Payroll', 'Treasury'].map((name) => (
515
+ <ListGroup.Row key={name}>
516
+ <ListGroup.Label>{name}</ListGroup.Label>
517
+ <Button size="small" variant="tertiary">
518
+ Remove
519
+ </Button>
520
+ </ListGroup.Row>
521
+ ))}
522
+ </ListGroup.Root>
523
+ )
524
+ }
525
+
526
+ namespace styles {
527
+ export const fill = style({ width: '100% !custom' })
528
+ }
529
+ ```
530
+
531
+ **Don't:** Fill a list with `primary` buttons, which hides the one that matters.
532
+
533
+ ```tsx
534
+ /**
535
+ * Don't example for "Save the Filled Button for the Decision" on the Destructive Actions page.
536
+ * Don't fill a list with `primary` buttons, which hides the one that matters.
537
+ * @module
538
+ */
539
+ import { Button, ListGroup, style } from '@tempoxyz/ds'
540
+
541
+ /** Renders the static preview, which the guideline card shows inert. */
542
+ export function Demo() {
543
+ return (
544
+ <ListGroup.Root {...styles.fill()}>
545
+ {['Ada', 'Payroll', 'Treasury'].map((name) => (
546
+ <ListGroup.Row key={name}>
547
+ <ListGroup.Label>{name}</ListGroup.Label>
548
+ <Button size="small">Delete</Button>
549
+ </ListGroup.Row>
550
+ ))}
551
+ </ListGroup.Root>
552
+ )
553
+ }
554
+
555
+ namespace styles {
556
+ export const fill = style({ width: '100% !custom' })
557
+ }
558
+ ```
559
+
560
+ ### Warn Before the Action
561
+
562
+ If the consequence is not obvious, state it in an [Alert](/docs/components/alert) beside the button. Readers who skim past dialogs still read it.
563
+
564
+ **Do:** State the specific consequence next to the action.
565
+
566
+ ```tsx
567
+ <Alert intent="warning">
568
+ <Alert.Icon />
569
+ <Alert.Content>Revoking this key stops 3 apps from sending payments.</Alert.Content>
570
+ </Alert>
571
+ <Inline>
572
+ <Button>Revoke key</Button>
573
+ </Inline>
574
+ ```
575
+
576
+ **Don't:** Write an alarming warning that names no consequence.
577
+
578
+ ```tsx
579
+ <Alert intent="negative">
580
+ <Alert.Icon />
581
+ <Alert.Content>Warning! Be careful!</Alert.Content>
582
+ </Alert>
583
+ <Inline>
584
+ <Button>Revoke key</Button>
585
+ </Inline>
586
+ ```
587
+
588
+ ## States
589
+
590
+ * **Loading (loading):** Confirm shows progress, Cancel is disabled, and the dialog stays open until the request ends.
591
+ * **Error:** Keep the dialog open and state the failure in it, so the reader can try again.
592
+ * **Empty:** After the last item goes, the list shows its first use [empty state](/docs/patterns/empty-states).
593
+ * **Disabled:** If the reader lacks permission, state who can act instead of a silent disabled button.
594
+
595
+ ## Accessibility
596
+
597
+ * **Named Dialogs (alertdialog):** Always include the title and description, which name the popup.
598
+ * **Escape Cancels (Escape):** Closes the dialog without running the action, as a close button does.
599
+ * **Specific Trigger Names:** Give each repeated trigger a specific name, such as Remove Ada.
600
+ * **Undo Toasts (F6):** Toasts are announced and F6 focuses them. Hover, focus, or a background window pauses timers.
601
+
602
+ ## Related
603
+
604
+ [AlertDialog](/docs/components/alert-dialog)
605
+
606
+ Use for confirmations.
607
+
608
+ [Button](/docs/components/button)
609
+
610
+ Use for the confirm and cancel actions.
611
+
612
+ [Toast](/docs/components/toast)
613
+
614
+ Use for Undo.
615
+
616
+ [Menu](/docs/components/menu)
617
+
618
+ Use for destructive items in a menu.
619
+
620
+ [Forms](/docs/patterns/forms)
621
+
622
+ Covers typed input and action order.
623
+
624
+ [Payments](/docs/patterns/payments)
625
+
626
+ Covers reviewing transfers.