@tempoxyz/ds 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/AGENTS.md +9 -0
- package/dist/docs/components/accordion.md +489 -0
- package/dist/docs/components/address-card.md +147 -0
- package/dist/docs/components/agent-logo.md +194 -0
- package/dist/docs/components/alert-dialog.md +631 -0
- package/dist/docs/components/alert.md +448 -0
- package/dist/docs/components/amount-input.md +305 -0
- package/dist/docs/components/amount.md +794 -0
- package/dist/docs/components/app-shell.md +559 -0
- package/dist/docs/components/asset-chip.md +294 -0
- package/dist/docs/components/autocomplete.md +619 -0
- package/dist/docs/components/avatar.md +321 -0
- package/dist/docs/components/badge.md +339 -0
- package/dist/docs/components/badged-logo.md +116 -0
- package/dist/docs/components/banner.md +379 -0
- package/dist/docs/components/bar-chart.md +208 -0
- package/dist/docs/components/breadcrumbs.md +380 -0
- package/dist/docs/components/button-group.md +286 -0
- package/dist/docs/components/button.md +650 -0
- package/dist/docs/components/calendar.md +375 -0
- package/dist/docs/components/callout.md +96 -0
- package/dist/docs/components/checkbox-group.md +521 -0
- package/dist/docs/components/checkbox.md +467 -0
- package/dist/docs/components/choice-group.md +516 -0
- package/dist/docs/components/choicebox.md +641 -0
- package/dist/docs/components/code.md +100 -0
- package/dist/docs/components/combobox.md +939 -0
- package/dist/docs/components/container.md +257 -0
- package/dist/docs/components/copy-button.md +642 -0
- package/dist/docs/components/date-picker.md +514 -0
- package/dist/docs/components/dialog.md +1332 -0
- package/dist/docs/components/drawer.md +966 -0
- package/dist/docs/components/empty.md +340 -0
- package/dist/docs/components/field.md +570 -0
- package/dist/docs/components/fieldset.md +398 -0
- package/dist/docs/components/frame.md +434 -0
- package/dist/docs/components/gauge.md +299 -0
- package/dist/docs/components/get-started-visual.md +117 -0
- package/dist/docs/components/grid.md +288 -0
- package/dist/docs/components/hero-visual.md +155 -0
- package/dist/docs/components/hex.md +487 -0
- package/dist/docs/components/icon-tile.md +292 -0
- package/dist/docs/components/identicon.md +371 -0
- package/dist/docs/components/inline.md +233 -0
- package/dist/docs/components/inset.md +225 -0
- package/dist/docs/components/kbd.md +423 -0
- package/dist/docs/components/list-group.md +981 -0
- package/dist/docs/components/loading-dots.md +237 -0
- package/dist/docs/components/menu.md +934 -0
- package/dist/docs/components/native-select.md +204 -0
- package/dist/docs/components/network-logo.md +248 -0
- package/dist/docs/components/number-field.md +543 -0
- package/dist/docs/components/otp-field.md +616 -0
- package/dist/docs/components/pagination.md +516 -0
- package/dist/docs/components/popover.md +544 -0
- package/dist/docs/components/progress.md +388 -0
- package/dist/docs/components/qr.md +314 -0
- package/dist/docs/components/radio-group.md +528 -0
- package/dist/docs/components/secret-value.md +406 -0
- package/dist/docs/components/select.md +751 -0
- package/dist/docs/components/separator.md +379 -0
- package/dist/docs/components/sidebar-menu.md +922 -0
- package/dist/docs/components/skeleton.md +517 -0
- package/dist/docs/components/slider.md +539 -0
- package/dist/docs/components/snippet.md +424 -0
- package/dist/docs/components/spinner.md +216 -0
- package/dist/docs/components/split-bar.md +139 -0
- package/dist/docs/components/split-button.md +464 -0
- package/dist/docs/components/stack.md +299 -0
- package/dist/docs/components/status-chip.md +226 -0
- package/dist/docs/components/status-indicator.md +119 -0
- package/dist/docs/components/status-row.md +174 -0
- package/dist/docs/components/stepper.md +451 -0
- package/dist/docs/components/success-hero.md +157 -0
- package/dist/docs/components/surface.md +639 -0
- package/dist/docs/components/switch.md +472 -0
- package/dist/docs/components/table.md +967 -0
- package/dist/docs/components/tabs.md +627 -0
- package/dist/docs/components/tempo-logo.md +185 -0
- package/dist/docs/components/text-input.md +624 -0
- package/dist/docs/components/textarea.md +352 -0
- package/dist/docs/components/toast.md +535 -0
- package/dist/docs/components/token-logo.md +371 -0
- package/dist/docs/components/token.md +247 -0
- package/dist/docs/components/tooltip.md +490 -0
- package/dist/docs/components/verification-code.md +116 -0
- package/dist/docs/components/visual.md +249 -0
- package/dist/docs/components.md +203 -0
- package/dist/docs/foundations/accessibility.md +377 -0
- package/dist/docs/foundations/agents.md +150 -0
- package/dist/docs/foundations/color.md +522 -0
- package/dist/docs/foundations/content/tone.md +194 -0
- package/dist/docs/foundations/content/voice.md +194 -0
- package/dist/docs/foundations/content/writing-style.md +406 -0
- package/dist/docs/foundations/iconography.md +282 -0
- package/dist/docs/foundations/intent.md +360 -0
- package/dist/docs/foundations/layout.md +508 -0
- package/dist/docs/foundations/linter.md +296 -0
- package/dist/docs/foundations/motion.md +151 -0
- package/dist/docs/foundations/radius-and-padding.md +86 -0
- package/dist/docs/foundations/typography.md +266 -0
- package/dist/docs/index.md +137 -0
- package/dist/docs/introduction/faq.md +132 -0
- package/dist/docs/introduction/getting-started.md +377 -0
- package/dist/docs/patterns/blockchain-values.md +709 -0
- package/dist/docs/patterns/dashboards.md +638 -0
- package/dist/docs/patterns/destructive-actions.md +626 -0
- package/dist/docs/patterns/empty-states.md +612 -0
- package/dist/docs/patterns/errors-and-validation.md +808 -0
- package/dist/docs/patterns/feedback-and-status.md +518 -0
- package/dist/docs/patterns/forms.md +679 -0
- package/dist/docs/patterns/loading.md +630 -0
- package/dist/docs/patterns/navigation.md +397 -0
- package/dist/docs/patterns/payments.md +765 -0
- package/dist/docs/patterns/responsive-layout.md +997 -0
- package/dist/docs/patterns/tables-and-lists.md +964 -0
- package/dist/docs/products/tempo-console.md +671 -0
- package/dist/docs/products/tempo-performance.md +437 -0
- package/dist/docs/products/tempo-wallet.md +438 -0
- package/dist/web/components/OtpField.css +1 -1
- package/dist/web/components/OtpField.d.ts +1 -1
- package/dist/web/components/OtpField.d.ts.map +1 -1
- package/dist/web/components/OtpField.js +3 -4
- package/dist/web/components/OtpField.js.map +1 -1
- package/dist/web/components/OtpField.js.zyzz.json +1 -1
- package/dist/web/index.js.zyzz.json +1 -1
- package/package.json +1 -1
- package/src/web/components/OtpField.tsx +2 -24
|
@@ -0,0 +1,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.
|