@nxgt/material 3.45.0 → 3.46.0
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/CHANGELOG.md +37 -0
- package/README.md +8 -1
- package/dist/{async-slice-D9LAUgth.js → async-slice-DPM9l8dd.js} +2 -2
- package/dist/{button-b_kruV_L.js → button-DR_EdcfV.js} +1 -1
- package/dist/charts.js +3 -3
- package/dist/{chip-6O3t9GXt.js → chip-olzYjs30.js} +1 -1
- package/dist/{code-editor-BtDOgUET.js → code-editor-CCc-piPv.js} +4 -4
- package/dist/code-editor.js +4 -4
- package/dist/{code-language-items-374m-OK8.js → code-language-items-mjN3a33n.js} +2 -2
- package/dist/{components-BG-BTfJP.js → components-CfttRxBs.js} +15688 -14567
- package/dist/components.js +26 -26
- package/dist/{dialog-8Ua9uuJW.js → dialog-DOhEoEP2.js} +4 -4
- package/dist/dnd.js +1 -1
- package/dist/{dropdown-menu-DJEeT1XT.js → dropdown-menu-CFO3E-rO.js} +1 -1
- package/dist/editor.js +7 -7
- package/dist/{empty-state-CrhrmNqn.js → empty-state-CTX0gKR3.js} +1 -1
- package/dist/hooks.js +28 -40
- package/dist/{i18n-provider-hrtFm9jD.js → i18n-provider-BnVQVqAF.js} +1 -1
- package/dist/i18n.js +5 -5
- package/dist/{icon-badge-Cpke1tCf.js → icon-badge-BRys8Tuo.js} +1 -1
- package/dist/lib/components/ui/display/enrolment-card/enrolment-card.d.ts +12 -0
- package/dist/lib/components/ui/display/enrolment-card/index.d.ts +2 -0
- package/dist/lib/components/ui/display/enrolment-card/partials/enrolment-header.d.ts +14 -0
- package/dist/lib/components/ui/display/enrolment-card/partials/enrolment-verification.d.ts +15 -0
- package/dist/lib/components/ui/display/enrolment-card/types.d.ts +49 -0
- package/dist/lib/components/ui/display/enrolment-card/use-enrolment-card-labels.d.ts +3 -0
- package/dist/lib/components/ui/display/index.d.ts +2 -0
- package/dist/lib/components/ui/display/recovery-codes-status/index.d.ts +2 -0
- package/dist/lib/components/ui/display/recovery-codes-status/partials/recovery-count.d.ts +11 -0
- package/dist/lib/components/ui/display/recovery-codes-status/recovery-codes-status.d.ts +12 -0
- package/dist/lib/components/ui/display/recovery-codes-status/types.d.ts +44 -0
- package/dist/lib/components/ui/display/recovery-codes-status/use-recovery-codes-status-labels.d.ts +3 -0
- package/dist/lib/components/ui/feedback/alert/alert.d.ts +7 -1
- package/dist/lib/components/ui/feedback/index.d.ts +1 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/index.d.ts +2 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/partials/step-up-body.d.ts +15 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/partials/step-up-footer.d.ts +22 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/partials/step-up-header.d.ts +10 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/step-up-dialog.d.ts +16 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/types.d.ts +55 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/use-step-up-labels.d.ts +3 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/use-step-up-state.d.ts +12 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/use-title-focus.d.ts +5 -0
- package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
- package/dist/lib/components/ui/fields/index.d.ts +1 -0
- package/dist/lib/components/ui/fields/verification-code/describe-verification.d.ts +53 -0
- package/dist/lib/components/ui/fields/verification-code/index.d.ts +4 -0
- package/dist/lib/components/ui/fields/verification-code/partials/code-input.d.ts +12 -0
- package/dist/lib/components/ui/fields/verification-code/partials/code-message.d.ts +11 -0
- package/dist/lib/components/ui/fields/verification-code/partials/code-slots.d.ts +18 -0
- package/dist/lib/components/ui/fields/verification-code/partials/code-surface.d.ts +12 -0
- package/dist/lib/components/ui/fields/verification-code/partials/form-actions.d.ts +14 -0
- package/dist/lib/components/ui/fields/verification-code/partials/label-row.d.ts +10 -0
- package/dist/lib/components/ui/fields/verification-code/partials/locked-alert.d.ts +17 -0
- package/dist/lib/components/ui/fields/verification-code/partials/method-switch.d.ts +8 -0
- package/dist/lib/components/ui/fields/verification-code/partials/recovery-input.d.ts +7 -0
- package/dist/lib/components/ui/fields/verification-code/partials/resend-line.d.ts +10 -0
- package/dist/lib/components/ui/fields/verification-code/partials/status-line.d.ts +10 -0
- package/dist/lib/components/ui/fields/verification-code/throttle.d.ts +10 -0
- package/dist/lib/components/ui/fields/verification-code/types.d.ts +138 -0
- package/dist/lib/components/ui/fields/verification-code/use-answer-focus.d.ts +32 -0
- package/dist/lib/components/ui/fields/verification-code/use-code-entry.d.ts +43 -0
- package/dist/lib/components/ui/fields/verification-code/use-code-lock.d.ts +36 -0
- package/dist/lib/components/ui/fields/verification-code/use-code-resend.d.ts +29 -0
- package/dist/lib/components/ui/fields/verification-code/use-code-submit.d.ts +23 -0
- package/dist/lib/components/ui/fields/verification-code/use-entry-anchors.d.ts +6 -0
- package/dist/lib/components/ui/fields/verification-code/use-focus-on-switch.d.ts +7 -0
- package/dist/lib/components/ui/fields/verification-code/use-resend-focus.d.ts +20 -0
- package/dist/lib/components/ui/fields/verification-code/use-resend.d.ts +11 -0
- package/dist/lib/components/ui/fields/verification-code/use-short-reminder.d.ts +24 -0
- package/dist/lib/components/ui/fields/verification-code/use-stale-answer.d.ts +52 -0
- package/dist/lib/components/ui/fields/verification-code/use-throttle-focus.d.ts +16 -0
- package/dist/lib/components/ui/fields/verification-code/use-throttle.d.ts +10 -0
- package/dist/lib/components/ui/fields/verification-code/use-typed-value.d.ts +18 -0
- package/dist/lib/components/ui/fields/verification-code/use-verification-labels.d.ts +3 -0
- package/dist/lib/components/ui/fields/verification-code/verification-code.d.ts +11 -0
- package/dist/lib/components/ui/fields/verification-code/verification-lock-provider.d.ts +54 -0
- package/dist/lib/components/ui/overlays/dialog/dialog-parts.d.ts +8 -1
- package/dist/lib/i18n/resources/en/en.d.ts +52 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +52 -0
- package/dist/lib/i18n/resources/fr/fr.d.ts +52 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +52 -0
- package/dist/lib/i18n/resources/resources.d.ts +104 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib.js +1 -1
- package/dist/main.js +43 -43
- package/dist/map.js +4 -4
- package/dist/material.css +1 -1
- package/dist/pdf-reader.js +1 -1
- package/dist/print.js +2 -2
- package/dist/{public-DPPmNtZB.js → public-BmA6r0EK.js} +6 -6
- package/dist/{rich-code-block-KQYR0qt3.js → rich-code-block-BgPtzSnv.js} +4 -4
- package/dist/{sortable-provider-hPPTMtJA.js → sortable-provider-CceARBYP.js} +1 -1
- package/dist/{spinner-Dd0os4cb.js → spinner-7orqNAEy.js} +1 -1
- package/dist/{stored-language-Blyb68vN.js → stored-language-BHmxU75w.js} +104 -0
- package/dist/theme-picker.js +2 -2
- package/dist/{theme-toggle-BDRRkkiA.js → theme-toggle-_RJgLHs9.js} +1 -1
- package/dist/{toolbar-items-D3hw7XZE.js → toolbar-items-BCy6DMzb.js} +2 -2
- package/dist/{translate-material-CdJBbURx.js → translate-material-n0YBXLGp.js} +2 -2
- package/dist/{translator-38JuN_VD.js → translator-B_ue2XEg.js} +1 -1
- package/dist/{use-chat-composer-utGkZQ6X.js → use-chat-composer-FDmwZSua.js} +12 -12
- package/dist/{use-heading-spy-bTccmEMF.js → use-heading-spy-O6_EATtA.js} +43 -31
- package/dist/{use-material-translation-DLsfkMxp.js → use-material-translation-CkPcqI6q.js} +2 -2
- package/docs/README.md +1 -0
- package/docs/components/README.md +1 -1
- package/docs/components/buttons-and-actions.md +2 -1
- package/docs/components/data-display.md +90 -0
- package/docs/components/feedback-and-status.md +113 -1
- package/docs/components/form-fields.md +163 -0
- package/docs/components/overlays-and-menus.md +3 -1
- package/docs/guides/two-factor.md +78 -0
- package/docs/previews/data-display-enrolmentcard.webp +0 -0
- package/docs/previews/data-display-recoverycodesstatus.webp +0 -0
- package/docs/previews/feedback-status-stepupdialog.webp +0 -0
- package/docs/previews/form-fields-verificationcode.webp +0 -0
- package/docs/roadmap.md +16 -0
- package/package.json +2 -2
|
@@ -36,8 +36,10 @@ from `@nxgt/material/components`.
|
|
|
36
36
|
| <img src="../previews/data-display-paramtable.webp" width="96" alt=""> | [ParamTable](#paramtable) | Name / type / default / description table |
|
|
37
37
|
| <img src="../previews/data-display-qrcode.webp" width="96" alt=""> | [QRCode](#qrcode) | QR code on a padded tile |
|
|
38
38
|
| <img src="../previews/data-display-barecode.webp" width="96" alt=""> | [Barecode](#barecode) | 1D barcode |
|
|
39
|
+
| <img src="../previews/data-display-enrolmentcard.webp" width="96" alt=""> | [EnrolmentCard](#enrolmentcard) | Turning on an authenticator app: the key, a notice and the first code to check, driven by props |
|
|
39
40
|
| <img src="../previews/data-display-enrolmentsecret.webp" width="96" alt=""> | [EnrolmentSecret](#enrolmentsecret) | A TOTP enrolment: QR code, app link, the key grouped by four |
|
|
40
41
|
| <img src="../previews/data-display-recoverycodes.webp" width="96" alt=""> | [RecoveryCodes](#recoverycodes) | One-time recovery codes: copy, download, print, confirm |
|
|
42
|
+
| <img src="../previews/data-display-recoverycodesstatus.webp" width="96" alt=""> | [RecoveryCodesStatus](#recoverycodesstatus) | How many recovery codes are left, and a new set behind a code |
|
|
41
43
|
|
|
42
44
|
## Avatar
|
|
43
45
|
|
|
@@ -1375,6 +1377,56 @@ not that span.
|
|
|
1375
1377
|
|
|
1376
1378
|
---
|
|
1377
1379
|
|
|
1380
|
+
## EnrolmentCard
|
|
1381
|
+
|
|
1382
|
+

|
|
1383
|
+
|
|
1384
|
+
Turning on an authenticator app, in one card: the [EnrolmentSecret](#enrolmentsecret) (QR code, app link, the key) on a tinted surface, a notice that the other devices are signed out, and the first code to prove the app works, with "Activate" and a tonal Cancel. It is **driven by props**, like the [VerificationCode](form-fields.md#verificationcode) it holds: `attemptsLeft`, `locked`, `error` and `pending` are your answer to the last code, `onSubmit` your call. It fetches nothing.
|
|
1385
|
+
|
|
1386
|
+
```tsx
|
|
1387
|
+
import { EnrolmentCard, VerificationLockProvider } from '@nxgt/material/components';
|
|
1388
|
+
|
|
1389
|
+
<VerificationLockProvider>
|
|
1390
|
+
<EnrolmentCard
|
|
1391
|
+
uri={enrolment.uri}
|
|
1392
|
+
secret={enrolment.secret}
|
|
1393
|
+
onSubmit={(code) => activate(code)} // the code is an app's code
|
|
1394
|
+
onCancel={() => navigate('/settings/security')}
|
|
1395
|
+
error={answer.error}
|
|
1396
|
+
attemptsLeft={answer.attemptsLeft}
|
|
1397
|
+
locked={answer.locked}
|
|
1398
|
+
pending={activating}
|
|
1399
|
+
/>
|
|
1400
|
+
</VerificationLockProvider>
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
| Prop | Type | Default | Effect |
|
|
1404
|
+
| --- | --- | --- | --- |
|
|
1405
|
+
| `uri`, `secret` | `string` | — | passed to `EnrolmentSecret` |
|
|
1406
|
+
| `onSubmit` | `(code, { kind }) => unknown` | — | called with the six digits; return a promise to hold a second submit |
|
|
1407
|
+
| `onCancel` | `() => void` | — | draws a Cancel beside Activate; left out, no Cancel |
|
|
1408
|
+
| `cancelVariant` | `ButtonVariant` | `'tonal'` | the Cancel button's variant; `'outlined'` or `'ghost'` restores an earlier look |
|
|
1409
|
+
| `attemptsLeft`, `locked`, `error`, `pending` | as `VerificationCode` | — | the answer to the last code: a wrong code empties the field and focuses it with the tries left; no tries left, or `locked`, locks the form and the page-wide lock. `error="throttled"` shows the general wait line only (no Resend, no `retryAfter` here). |
|
|
1410
|
+
| `title`, `titleAs`, `description` | `ReactNode`, `'h2'…`, `ReactNode \| false` | built-in, `'h2'` | the heading ("Set up your authenticator app"), its level, and the line under it |
|
|
1411
|
+
| `focusTitle` | `boolean` | `true` | the title takes the focus once mounted |
|
|
1412
|
+
| `length` | `number` | `6` | the slots in the code field, as `VerificationCode` |
|
|
1413
|
+
| `sessionsNotice` | `ReactNode \| false` | "Turning this on signs you out on your other devices." | the notice that the other devices are (or were) signed out; pass the after-the-fact wording once they were, `false` for none |
|
|
1414
|
+
| `status` | `ReactNode` | — | a notice line over the field ("Your session is confirmed. Type the next code.") |
|
|
1415
|
+
| `secretProps` | `Partial<EnrolmentSecretProps>` | — | the QR size, labels and copy icon |
|
|
1416
|
+
| `submitLabel`, `labels`, `icons` | — | "Activate" | the button, every string (`title`, `description`, `activate`, `cancel`, `sessionsNotice`, and those of `VerificationCode`), the locked alert's icon |
|
|
1417
|
+
|
|
1418
|
+
There is no recovery-code switch: no recovery code exists yet. The card reads
|
|
1419
|
+
the lock of the nearest `VerificationLockProvider` (or the document's), so a
|
|
1420
|
+
lock set by another code form disables its field and leaves Cancel usable.
|
|
1421
|
+
|
|
1422
|
+
**In a test**, find the field by `getByRole('textbox', { name: 'Code' })`, the
|
|
1423
|
+
button by `getByRole('button', { name: 'Activate' })` and the lock by
|
|
1424
|
+
`getByRole('alert')`.
|
|
1425
|
+
|
|
1426
|
+
**Storybook:** `Data display/EnrolmentCard`, `…/Shared lock`, `Guides/Accessibility/Touch targets/Verification code` · **Vue:** not yet
|
|
1427
|
+
|
|
1428
|
+
---
|
|
1429
|
+
|
|
1378
1430
|
## EnrolmentSecret
|
|
1379
1431
|
|
|
1380
1432
|

|
|
@@ -1477,3 +1529,41 @@ useEffect(() => {
|
|
|
1477
1529
|
```
|
|
1478
1530
|
|
|
1479
1531
|
**Storybook:** `Data display/RecoveryCodes` · **Vue:** `RecoveryCodes` (`v-model:saved`, `@done`, `@copy`; slots `title`, `description`, `warning`, `copy-icon`, `download-icon`, `print-icon`)
|
|
1532
|
+
|
|
1533
|
+
---
|
|
1534
|
+
|
|
1535
|
+
## RecoveryCodesStatus
|
|
1536
|
+
|
|
1537
|
+

|
|
1538
|
+
|
|
1539
|
+
Where an account stands on its recovery codes: how many are left, a warning when none is, and the way to make a new set. It is **driven by props**; show the new codes with [RecoveryCodes](#recoverycodes) once `onRegenerate` succeeds.
|
|
1540
|
+
|
|
1541
|
+
```tsx
|
|
1542
|
+
import { RecoveryCodesStatus, VerificationLockProvider } from '@nxgt/material/components';
|
|
1543
|
+
|
|
1544
|
+
<VerificationLockProvider>
|
|
1545
|
+
<RecoveryCodesStatus
|
|
1546
|
+
remaining={account.recoveryCodesLeft}
|
|
1547
|
+
onRegenerate={(code, options) => regenerate(code, options?.kind)}
|
|
1548
|
+
error={answer.error}
|
|
1549
|
+
attemptsLeft={answer.attemptsLeft}
|
|
1550
|
+
locked={answer.locked}
|
|
1551
|
+
pending={regenerating}
|
|
1552
|
+
/>
|
|
1553
|
+
</VerificationLockProvider>
|
|
1554
|
+
```
|
|
1555
|
+
|
|
1556
|
+
| Prop | Type | Default | Effect |
|
|
1557
|
+
| --- | --- | --- | --- |
|
|
1558
|
+
| `remaining` | `number \| null` | — | the count, an ICU plural ("6 recovery codes left"); `0` turns it red and adds a warning; `null` or left out shows no count |
|
|
1559
|
+
| `onRegenerate` | `(code?, { kind }?) => unknown` | — | with `requireCode`, called with the code and its kind (`'code'` or `'recovery'`); without it, called with no argument |
|
|
1560
|
+
| `requireCode` | `boolean` | `true` | `true`: a `VerificationCode` whose button is "New codes"; `false`: a plain button |
|
|
1561
|
+
| `allowRecovery` | `boolean` | `true` | offers "Use a recovery code" in the form |
|
|
1562
|
+
| `length` | `number` | `6` | the slots in the code field, as `VerificationCode` |
|
|
1563
|
+
| `attemptsLeft`, `locked`, `error`, `pending` | as `VerificationCode` | — | the answer to the last code; the same locked state. `error="throttled"` shows the general wait line only (no Resend, no `retryAfter` here) |
|
|
1564
|
+
| `title`, `titleAs`, `description` | `ReactNode`, `'h2'…`, `ReactNode \| false` | built-in, `'h2'` | the heading ("Recovery codes"), its level, the line under it |
|
|
1565
|
+
| `labels`, `icons` | partial objects | built-in | `title`, `description`, `remaining` (`(count) => string`), `noneLeft`, `noneLeftHint`, `newCodes`, `newCodesHint`, and those of `VerificationCode` |
|
|
1566
|
+
|
|
1567
|
+
It reads the page-wide lock like [EnrolmentCard](#enrolmentcard) does. With `requireCode={false}` there is no code form, so `error` and `attemptsLeft` are not shown: the plain button follows `pending`, and is disabled by `locked` and by the page-wide lock; then the lock's text (the message of the answer that locked, or "Too many wrong codes. Try again later.") shows above it as a muted line that describes the button.
|
|
1568
|
+
|
|
1569
|
+
**Storybook:** `Data display/RecoveryCodesStatus`, `Data display/EnrolmentCard/Shared lock` · **Vue:** not yet
|
|
@@ -11,6 +11,7 @@ centre.
|
|
|
11
11
|
| <img src="../previews/feedback-status-alertdialog.webp" width="96" alt=""> | [AlertDialog](#alertdialog) | Parts for a blocking confirmation dialog |
|
|
12
12
|
| <img src="../previews/feedback-status-confirmdialog.webp" width="96" alt=""> | [ConfirmDialog](#confirmdialog) | Confirm / cancel dialog, `ConfirmationDialog`, `CustomAlertDialog` |
|
|
13
13
|
| <img src="../previews/feedback-status-customdialog.webp" width="96" alt=""> | [CustomDialog](#customdialog) | Ready-made dialog with trigger, icon, title, body and footer |
|
|
14
|
+
| <img src="../previews/feedback-status-stepupdialog.webp" width="96" alt=""> | [StepUpDialog](#stepupdialog) | "Confirm it's you": a dialog that asks for a one-time code, driven by props |
|
|
14
15
|
| <img src="../previews/feedback-status-emptystate.webp" width="96" alt=""> | [EmptyState](#emptystate) | Illustration, title, description and action, by kind (empty, search, not-found, unauthorized) |
|
|
15
16
|
| <img src="../previews/feedback-status-errorstate.webp" width="96" alt=""> | [ErrorState](#errorstate) | Failure block with Retry and technical details |
|
|
16
17
|
| <img src="../previews/feedback-status-progress.webp" width="96" alt=""> | [Progress](#progress) | Material 3 Expressive linear bar, circular ring and labelled progress field, flat or wavy |
|
|
@@ -56,9 +57,25 @@ keep the old look.
|
|
|
56
57
|
| `icon` | `ReactNode` | the tone's icon | shown before the text; `null` (or `false`) shows none, any node replaces the tone's icon |
|
|
57
58
|
| `title` | `ReactNode` | — | bold first line |
|
|
58
59
|
| `description` | `ReactNode` | — | muted body text |
|
|
60
|
+
| `actions` | `ReactNode` | — | buttons or links under the text, wrapping on a narrow screen (`data-slot="alert-actions"`); a `Button` or `LinkButton` keeps its 44 px target on a coarse pointer |
|
|
59
61
|
|
|
60
62
|
The root carries `data-slot="alert"`, the title `data-slot="alert-title"` and the description `data-slot="alert-description"`, for tests and selectors.
|
|
61
63
|
|
|
64
|
+
**Actions.** Give an Alert a way out with `actions`, such as a link to the next step:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<Alert
|
|
68
|
+
variant="warning"
|
|
69
|
+
title="A second step is required"
|
|
70
|
+
description="Turn on two-step sign-in to continue."
|
|
71
|
+
actions={
|
|
72
|
+
<Button asChild size="sm">
|
|
73
|
+
<a href="/settings/security">Continue</a>
|
|
74
|
+
</Button>
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
62
79
|
**In a form grid.** Inside `.form-grid-layout`'s `[data-slot="form-fields"]` or a [FormSection](form-fields.md#formsection)'s fields, an `Alert` or a `Banner` placed directly in the grid takes the whole row (`col-span-full w-full`) without any class from you; a utility class of your own on it still wins.
|
|
63
80
|
|
|
64
81
|
```tsx
|
|
@@ -71,7 +88,7 @@ The root carries `data-slot="alert"`, the title `data-slot="alert-title"` and th
|
|
|
71
88
|
</form>
|
|
72
89
|
```
|
|
73
90
|
|
|
74
|
-
**Storybook:** `Feedback & status/Alert/Tone icon` (`ToneIcon`), `Feedback & status/Alert/In a form grid` (`InFormGrid`) · **Vue:** `Alert` (the default icon and the form-grid row are React only for now)
|
|
91
|
+
**Storybook:** `Feedback & status/Alert/Tone icon` (`ToneIcon`), `Feedback & status/Alert/In a form grid` (`InFormGrid`), `Feedback & status/Alert` (`WithActions`) · **Vue:** `Alert` (the default icon and the form-grid row are React only for now)
|
|
75
92
|
|
|
76
93
|
---
|
|
77
94
|
|
|
@@ -336,6 +353,101 @@ import { Atom } from 'lucide-react';
|
|
|
336
353
|
|
|
337
354
|
---
|
|
338
355
|
|
|
356
|
+
## StepUpDialog
|
|
357
|
+
|
|
358
|
+

|
|
359
|
+
|
|
360
|
+
"Confirm it's you": a dialog that asks for a one-time code before the app goes
|
|
361
|
+
on with a sensitive action. It is **presentational and props-driven**, built on
|
|
362
|
+
`Dialog` and `VerificationCode`: it holds no state machine and fetches nothing,
|
|
363
|
+
so you wire the model of your own (a session, a store, a loader) to its props.
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
import { StepUpDialog, VerificationLockProvider } from '@nxgt/material/components';
|
|
367
|
+
|
|
368
|
+
type Step = {
|
|
369
|
+
open: boolean;
|
|
370
|
+
via?: 'email' | 'secondFactor';
|
|
371
|
+
address?: string;
|
|
372
|
+
requestError?: string;
|
|
373
|
+
attemptsLeft?: number;
|
|
374
|
+
error?: 'invalid' | 'expired' | 'failed' | 'throttled';
|
|
375
|
+
retryAfter?: number;
|
|
376
|
+
pending: boolean;
|
|
377
|
+
resent: boolean;
|
|
378
|
+
accepted: boolean;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
export function ConfirmItsYou({
|
|
382
|
+
step,
|
|
383
|
+
close,
|
|
384
|
+
check,
|
|
385
|
+
sendAgain,
|
|
386
|
+
}: {
|
|
387
|
+
step: Step;
|
|
388
|
+
close: () => void;
|
|
389
|
+
check: (code: string, kind: 'code' | 'recovery') => Promise<void>;
|
|
390
|
+
sendAgain: () => Promise<void>;
|
|
391
|
+
}) {
|
|
392
|
+
return (
|
|
393
|
+
<VerificationLockProvider>
|
|
394
|
+
<StepUpDialog
|
|
395
|
+
open={step.open}
|
|
396
|
+
onCancel={close}
|
|
397
|
+
via={step.via} // left out while the code is being requested
|
|
398
|
+
destination={step.address}
|
|
399
|
+
requestError={step.requestError}
|
|
400
|
+
attemptsLeft={step.attemptsLeft}
|
|
401
|
+
error={step.error}
|
|
402
|
+
retryAfter={step.retryAfter}
|
|
403
|
+
pending={step.pending}
|
|
404
|
+
onSubmit={(code, { kind }) => check(code, kind)}
|
|
405
|
+
onResend={sendAgain}
|
|
406
|
+
resent={step.resent}
|
|
407
|
+
confirming={step.accepted}
|
|
408
|
+
/>
|
|
409
|
+
</VerificationLockProvider>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
| Prop | Type | Default | Effect |
|
|
415
|
+
| --- | --- | --- | --- |
|
|
416
|
+
| `open`, `onCancel` | `boolean`, `() => void` | — | Escape, a click outside and Cancel call `onCancel`, except while `confirming`; you close the dialog |
|
|
417
|
+
| `cancelVariant` | `ButtonVariant` | `'tonal'` | the Cancel button's variant; `'outlined'` or `'ghost'` restores an earlier look |
|
|
418
|
+
| `via` | `'email' \| 'secondFactor'` | — | how the code was issued; left out, the code is still being requested: no field, a status line. Changing it starts the form again (typed value, recovery switch) |
|
|
419
|
+
| `destination` | `ReactNode` | — | where an e-mailed code went; named in the description |
|
|
420
|
+
| `requestError` | `ReactNode` | — | the request for a code failed: an alert in place of the field |
|
|
421
|
+
| `attemptsLeft`, `locked`, `error`, `retryAfter`, `pending` | as `VerificationCode` | — | the answer to the last code (`error="throttled"` with `retryAfter` seconds disables "Resend the code" for that long and never locks; the footer is unchanged). No tries left: an e-mailed code reads "expired, ask for a new code"; an app or recovery code shows the locked alert and writes the page-wide lock |
|
|
422
|
+
| `length`, `allowRecovery` | `number`, `boolean` | `6`, per `via` | the slots in the code field, and "Use a recovery code" (on for `via="secondFactor"`), passed to the `VerificationCode` |
|
|
423
|
+
| `onSubmit`, `onResend`, `resent` | as `VerificationCode` | — | your calls; "Resend the code" sits alone on its own line |
|
|
424
|
+
| `confirming`, `confirmedLabel` | `boolean`, `ReactNode` | — | the code was accepted and you re-run the action: the field goes, a status line shows, Cancel is disabled and Escape does nothing until you close it |
|
|
425
|
+
| `title`, `description` | `ReactNode` | built-in | the title ("Confirm it's you") and the line under it, which follows `via` |
|
|
426
|
+
| `labels`, `icons` | partial objects | built-in | every string of the dialog (`title`, `description`, `descriptionApp`, `descriptionEmail`, `descriptionEmailTo`, `requesting`, `cancel`, `confirmed`) and of `VerificationCode` (`incomplete`, `incompleteRecovery` among them) |
|
|
427
|
+
|
|
428
|
+
Confirm is a footer button that submits the form. Pressed with too few digits
|
|
429
|
+
(or an empty recovery code) it focuses the field and says "Enter all 6 digits."
|
|
430
|
+
instead of calling `onSubmit`.
|
|
431
|
+
|
|
432
|
+
**Focus.** The title takes the focus on open and whenever the field goes away
|
|
433
|
+
(a code being requested, a code accepted); the field takes it after a refusal;
|
|
434
|
+
on close it goes back to the button that opened the dialog. A locked code
|
|
435
|
+
leaves Cancel usable.
|
|
436
|
+
|
|
437
|
+
**On a phone** the footer stays one row, Cancel on the left and Confirm on the
|
|
438
|
+
right: `DialogFooter` takes `stackOnPhone={false}` (`true` by default, the
|
|
439
|
+
buttons stacked full width). Every control keeps a 44 px target on a coarse
|
|
440
|
+
pointer.
|
|
441
|
+
|
|
442
|
+
**In a test**, find the field by `getByRole('textbox', { name: 'Code' })`, the
|
|
443
|
+
dialog by `getByRole('dialog', { name: "Confirm it's you" })` and the lock by
|
|
444
|
+
`getByRole('alert')`.
|
|
445
|
+
|
|
446
|
+
**Storybook:** `Feedback & status/StepUpDialog`, `…/Locks`, `…/Throttled`, `…/Layout`, `Guides/Accessibility/Touch targets/Step-up dialog` ·
|
|
447
|
+
**Vue:** not yet
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
339
451
|
## EmptyState
|
|
340
452
|
|
|
341
453
|

|
|
@@ -24,6 +24,7 @@ for the value type each one writes. Everything is imported from
|
|
|
24
24
|
| <img src="../previews/form-fields-searchfield.webp" width="96" alt=""> | [SearchField](#searchfield) | Clear button, spinner, debounced value |
|
|
25
25
|
| <img src="../previews/form-fields-colorfield.webp" width="96" alt=""> | [ColorField](#colorfield) | Hex input with a picker |
|
|
26
26
|
| <img src="../previews/form-fields-otpfield.webp" width="96" alt=""> | [OtpField](#otpfield) | One-time code input |
|
|
27
|
+
| <img src="../previews/form-fields-verificationcode.webp" width="96" alt=""> | [VerificationCode](#verificationcode) | The form that checks a one-time code, with a page-wide lock |
|
|
27
28
|
| <img src="../previews/form-fields-inputgroupfield.webp" width="96" alt=""> | [InputGroupField](#inputgroupfield) | Labelled `InputGroup` with addons |
|
|
28
29
|
| <img src="../previews/form-fields-localizedtextfields.webp" width="96" alt=""> | [LocalizedTextFields](#localizedtextfields) | One text in several languages, with defaults and a reset |
|
|
29
30
|
| <img src="../previews/form-fields-inlineedit.webp" width="96" alt=""> | [InlineEdit](#inlineedit) | Click-to-edit text |
|
|
@@ -742,6 +743,168 @@ carries `data-invalid`, not `aria-invalid`.
|
|
|
742
743
|
|
|
743
744
|
---
|
|
744
745
|
|
|
746
|
+
## VerificationCode
|
|
747
|
+
|
|
748
|
+

|
|
749
|
+
|
|
750
|
+
A `<form>` that asks for a one-time code, **presentational and controlled**: it
|
|
751
|
+
keeps the typed value, the app or recovery switch, the focus and the submit
|
|
752
|
+
guard, and shows the answer you give it. It fetches nothing and holds no state
|
|
753
|
+
machine; your state (a session, a store, a loader) says what happened, and
|
|
754
|
+
`onSubmit` and `onResend` are yours. Every string is a label.
|
|
755
|
+
|
|
756
|
+
The slots are tinted boxes inside a hairline ring that holds 3:1 against the
|
|
757
|
+
card in light and dark, with a dot while empty, a dash between the halves, 20px
|
|
758
|
+
semibold tabular digits, and a soft caret and a 2px primary ring on the active
|
|
759
|
+
one (the caret stands still under `prefers-reduced-motion`). A wrong code tints
|
|
760
|
+
the slots and digits and shows its message with an icon; a full code is tinted with
|
|
761
|
+
the primary colour (the slots pop in once, only where motion is welcome); a locked form is
|
|
762
|
+
muted, with a "Locked" pill in the label row (`labels.lockedBadge`) and the lock
|
|
763
|
+
message under the slots. The recovery code's input shares the same fill and ring.
|
|
764
|
+
|
|
765
|
+
```tsx
|
|
766
|
+
import { VerificationCode, VerificationLockProvider } from '@nxgt/material/components';
|
|
767
|
+
|
|
768
|
+
export function SecondStep({ answer, pending, send }: Props) {
|
|
769
|
+
return (
|
|
770
|
+
<VerificationLockProvider>
|
|
771
|
+
<VerificationCode
|
|
772
|
+
via="secondFactor"
|
|
773
|
+
attemptsLeft={answer?.attemptsLeft}
|
|
774
|
+
error={answer?.error}
|
|
775
|
+
pending={pending}
|
|
776
|
+
onSubmit={(code, { kind }) => send(code, kind)}
|
|
777
|
+
/>
|
|
778
|
+
</VerificationLockProvider>
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
| Prop | Type | Default | Effect |
|
|
784
|
+
| --- | --- | --- | --- |
|
|
785
|
+
| `via` | `'email' \| 'secondFactor'` | — | how the code was issued: sent to the person, or from an authenticator app |
|
|
786
|
+
| `attemptsLeft` | `number` | — | the tries left, said under a wrong code; at `0` see below |
|
|
787
|
+
| `error` | `'invalid' \| 'expired' \| 'failed' \| 'throttled' \| ReactNode` | — | the last refusal; a node is shown as a wrong code's message. `'throttled'` is not a refusal of the code: see below. Leave it out when a new code is typed or accepted |
|
|
788
|
+
| `retryAfter` | `number` | — | with `error="throttled"`, the seconds to wait before asking for a new code |
|
|
789
|
+
| `locked` | `boolean` | — | locks this form, and the page-wide lock, for an app or recovery code; ignored for an e-mailed code, which is never locked unless a recovery code is typed in its form |
|
|
790
|
+
| `pending` | `boolean` | — | a request is in flight: field disabled, Confirm busy, a second press dropped |
|
|
791
|
+
| `onSubmit` | `(code, { kind: 'code' \| 'recovery' }) => unknown` | — | the code as typed (a recovery code is trimmed and cut at 40) and what it is (`'code'` is the issued code, from an app or an e-mail). Return a promise to hold a second submit |
|
|
792
|
+
| `onResend`, `resent` | `() => unknown`, `boolean` | — | "Resend the code" alone on its own line (an e-mailed code), "New code" / "Try again" once a code expired; `resent` says a new code was sent |
|
|
793
|
+
| `allowRecovery` | `boolean` | `via === 'secondFactor'` | "Use a recovery code", which swaps the field for a text field and back |
|
|
794
|
+
| `length` | `number` | `6` | slots in the code field |
|
|
795
|
+
| `id` | `string` | generated | the form's id; the buttons of `actions="none"` point at it with `form={id}`. Other `<form>` props pass through |
|
|
796
|
+
| `actions` | `'inline' \| 'none'` | `'inline'` | `'none'` leaves the buttons to you; point them at the form with `form={id}` |
|
|
797
|
+
| `submitLabel`, `secondaryAction` | `ReactNode` | "Confirm" | the button, and what sits beside it (Cancel), which stays usable when locked |
|
|
798
|
+
| `destructive` | `boolean` | — | the button is red (turning protection off) |
|
|
799
|
+
| `status` | `ReactNode` | — | a notice line over the field |
|
|
800
|
+
| `labels`, `icons` | partial objects | built-in | every string (`triesLeft`, `incomplete`, `throttledMinutes` and `throttledSeconds` are `(count) => string`; `incompleteRecovery` is the empty recovery code's message); the locked alert's icon |
|
|
801
|
+
|
|
802
|
+
Submitting too few digits (the inline Confirm, which stays enabled until the
|
|
803
|
+
form is locked, Enter, or a Confirm button of your own pointed at the form)
|
|
804
|
+
does not call `onSubmit`: it focuses the field and says "Enter all 6 digits."
|
|
805
|
+
(an empty recovery code, "Enter your recovery code."), announced once.
|
|
806
|
+
|
|
807
|
+
A wrong code empties the field and focuses it once your answer arrives, and
|
|
808
|
+
says "Wrong code." with the tries left (an ICU plural). `expired` and `failed`
|
|
809
|
+
say their message. One polite live region announces what the focus does not
|
|
810
|
+
read: "A new code was sent.", and a refusal you set without a submit. A refusal
|
|
811
|
+
that follows a submit sends the focus to the field, which is described by the
|
|
812
|
+
message, so the live region leaves it out rather than say it twice. A rejected
|
|
813
|
+
`onSubmit` is swallowed: map the failure to `error="failed"`.
|
|
814
|
+
|
|
815
|
+
**Throttled.** `error="throttled"` says too many new codes were asked for.
|
|
816
|
+
It is not a refusal of the code: the field stays usable, nothing locks (for an
|
|
817
|
+
app or recovery code, `locked` or `attemptsLeft: 0` still does and overrides
|
|
818
|
+
it; an e-mailed code never locks), no try is counted, and an entry switch does
|
|
819
|
+
not clear it. It speaks before "expired": an e-mailed code with no tries left
|
|
820
|
+
that is throttled when "New code" is pressed says the wait, not "This code has
|
|
821
|
+
expired.", and the button keeps the "New code" label. The line stays until you
|
|
822
|
+
clear `error` (the end of the wait only re-enables Resend). With `retryAfter` (seconds) the line reads "Wait 2
|
|
823
|
+
min before asking for a new code." (minutes rounded up, "Wait 45 s…" under a
|
|
824
|
+
minute; on an authenticator or recovery code, `via="secondFactor"`, "Wait 2 min
|
|
825
|
+
before trying again.") and "Resend the code" is disabled, described by that line, until the
|
|
826
|
+
wait elapsed, counted from when the answer arrived: one timer, no countdown,
|
|
827
|
+
so the text does not tick. A new `retryAfter` starts the clock again, and so does a
|
|
828
|
+
resend that settles while `error` is still `"throttled"`, even with the same
|
|
829
|
+
`retryAfter`. Resend
|
|
830
|
+
is disabled once the client has hydrated, not in the server HTML. Without `retryAfter` the line is general ("Too many
|
|
831
|
+
requests. Wait a moment and try again.") and Resend stays on. `EnrolmentCard` and `RecoveryCodesStatus` take `error="throttled"` too, with no Resend and no
|
|
832
|
+
`retryAfter`: they show that general line only.
|
|
833
|
+
It is announced once by the polite live region; when it answers a press of
|
|
834
|
+
Resend, the focus goes to the field (the pressed button is now disabled), which
|
|
835
|
+
is described by the line, and the live region leaves it out. The words are
|
|
836
|
+
`labels.throttled`, `throttledMinutes(n)` and `throttledSeconds(n)` (`throttledMinutesApp(n)` and `throttledSecondsApp(n)` for `via="secondFactor"`). On the
|
|
837
|
+
server nothing waits and no timer runs.
|
|
838
|
+
|
|
839
|
+
**With no tries left** (`attemptsLeft === 0`) the form reads the answer by what
|
|
840
|
+
the code was: an e-mailed code shows "expired, ask for a new code" (nothing is
|
|
841
|
+
held against it), and an app or recovery code shows the locked alert, because
|
|
842
|
+
that lock belongs to the account, so it also writes the page-wide lock below.
|
|
843
|
+
What is typed decides what is held: an e-mailed form in recovery mode is held
|
|
844
|
+
like an app's. Switching the entry clears the held decision of the answer you
|
|
845
|
+
gave for the other kind of code: a stale `attemptsLeft: 0` for the e-mailed
|
|
846
|
+
code does not lock the recovery field, only a refused recovery code does, and
|
|
847
|
+
a lock already written stays written. The stale answer is released once a
|
|
848
|
+
changed answer arrives, or a submit made since the switch ends with `pending`
|
|
849
|
+
down, so raise `pending` in the same tick as your `onSubmit` (or return its
|
|
850
|
+
promise); an `error` node rebuilt on each render reads as a changed answer. The locked message disables the field and Confirm and describes
|
|
851
|
+
them (`aria-describedby`); it takes the focus after a submit that locked it.
|
|
852
|
+
One lock is one alert: only the first form to show it has `role="alert"`, and
|
|
853
|
+
the other forms under the same lock show the same words as a plain element.
|
|
854
|
+
With no provider every form whose own answer locked it is an alert (the server
|
|
855
|
+
and the hydrated page must agree), and the forms the document lock holds are
|
|
856
|
+
plain elements.
|
|
857
|
+
|
|
858
|
+
**Wiring your state.** Map the answer your session or API gives to the props;
|
|
859
|
+
nothing else is needed:
|
|
860
|
+
|
|
861
|
+
```tsx
|
|
862
|
+
type Answer = {
|
|
863
|
+
via: 'email' | 'secondFactor';
|
|
864
|
+
attemptsLeft?: number;
|
|
865
|
+
error?: 'invalid' | 'expired' | 'failed' | 'throttled';
|
|
866
|
+
retryAfter?: number; // seconds, with 'throttled'
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
function CodeStep({ answer, pending, send, sendAgain }: StepProps<Answer>) {
|
|
870
|
+
return (
|
|
871
|
+
<VerificationCode
|
|
872
|
+
via={answer.via}
|
|
873
|
+
attemptsLeft={answer.attemptsLeft}
|
|
874
|
+
error={answer.error}
|
|
875
|
+
retryAfter={answer.retryAfter}
|
|
876
|
+
pending={pending}
|
|
877
|
+
onSubmit={(code, { kind }) => send({ code, recovery: kind === 'recovery' })}
|
|
878
|
+
onResend={answer.via === 'email' ? sendAgain : undefined}
|
|
879
|
+
/>
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
**The lock.** An app or recovery code that is `locked`, or has no tries left,
|
|
885
|
+
writes a page-wide lock: every app or recovery field under the same
|
|
886
|
+
`VerificationLockProvider` stays locked until the page is loaded again, whether
|
|
887
|
+
it remounts, the step changes or a dialog closes. Without a provider the lock
|
|
888
|
+
covers the whole document. A code sent by e-mail is never held by it, unless a recovery code is typed in its form.
|
|
889
|
+
`useVerificationLock()` returns `{ locked, message, lock(message?) }` for the
|
|
890
|
+
rest of your app; on the server it is always unlocked, so hydration cannot
|
|
891
|
+
mismatch. `createVerificationLock` and `normalizeRecoveryCode` are in
|
|
892
|
+
`@nxgt/material-core/two-factor`.
|
|
893
|
+
|
|
894
|
+
For a layout of your own use `useVerificationLabels`.
|
|
895
|
+
|
|
896
|
+
**On a phone** the buttons stay on one row, the slots shrink to fit 320 px, and
|
|
897
|
+
every control keeps a 44 px target on a coarse pointer.
|
|
898
|
+
|
|
899
|
+
**In a test**, find the field by `getByRole('textbox', { name: 'Code' })`, the
|
|
900
|
+
refusal as the field's accessible description (`toHaveAccessibleDescription`; the `status` region carries only what the focus does not read: after a click on Confirm it stays empty), and the lock by `getByRole('alert')` (one per lock).
|
|
901
|
+
|
|
902
|
+
**Storybook:** `Form fields/VerificationCode`, `…/States`, `…/Lock`, `…/Lock scope`, `…/Answers`, `…/Failures`, `…/Recovery switch`, `…/Throttled`, `…/Throttled expired`, `…/Entry switch`, `…/Layout`, `…/Refined`, `…/Refined checks`,
|
|
903
|
+
`Navigation & layout/AuthCard/Second factor` · **Vue:** not yet (the framework-free
|
|
904
|
+
part is `@nxgt/material-core/two-factor`)
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
745
908
|
## InputGroupField
|
|
746
909
|
|
|
747
910
|

|
|
@@ -139,7 +139,9 @@ utilities: a class you pass wins. The flat look of 2.x before it is
|
|
|
139
139
|
`className="rounded-lg border bg-background shadow-lg"` on `DialogContent`
|
|
140
140
|
(and `className="bg-black/50"` on a `DialogOverlay` of your own).
|
|
141
141
|
`DialogFooter` stacks its actions full width on a phone, the main one at the
|
|
142
|
-
bottom.
|
|
142
|
+
bottom. `stackOnPhone={false}` keeps one row instead, the first
|
|
143
|
+
action on the left and the last on the right, for two short actions such as
|
|
144
|
+
Cancel and Confirm. With `prefers-reduced-motion`, nothing moves: it appears and goes.
|
|
143
145
|
|
|
144
146
|
`DialogContent` and `AlertDialogContent` are capped at `100dvh - 2rem` (`100vh`
|
|
145
147
|
where `dvh` is unsupported) and scroll inside themselves, so a long body never
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Two-step verification
|
|
2
|
+
|
|
3
|
+
A one-time code is asked for at five moments of an account's life: signing in,
|
|
4
|
+
confirming a sensitive action ("step-up"), turning an authenticator app on,
|
|
5
|
+
making new recovery codes and turning it off. The components below draw all of
|
|
6
|
+
them. They are **driven by props**: your server, session or store says what
|
|
7
|
+
happened (`attemptsLeft`, `error`, `locked`, `pending`), and the components
|
|
8
|
+
keep only the typed value, the focus and the submit guard. Nothing is fetched
|
|
9
|
+
inside them.
|
|
10
|
+
|
|
11
|
+
| Moment | Component | Page |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| The code field and its states (wrong, expired, busy, locked, resent) | `VerificationCode` | [Form fields](../components/form-fields.md#verificationcode) |
|
|
14
|
+
| Sign-in's second step | `AuthCard` holding a `VerificationCode` | [Navigation and layout](../components/navigation-and-layout.md#authcard) |
|
|
15
|
+
| "Confirm it's you" before a sensitive action | `StepUpDialog` | [Feedback and status](../components/feedback-and-status.md#stepupdialog) |
|
|
16
|
+
| Turning an authenticator app on | `EnrolmentCard` (over `EnrolmentSecret`) | [Data display](../components/data-display.md#enrolmentcard) |
|
|
17
|
+
| Recovery codes: how many are left, a new set | `RecoveryCodesStatus`, then `RecoveryCodes` | [Data display](../components/data-display.md#recoverycodesstatus) |
|
|
18
|
+
| A notice with a way out ("Continue", "Retry") | `Alert` with `actions` | [Feedback and status](../components/feedback-and-status.md#alert) |
|
|
19
|
+
| The page-wide lock | `VerificationLockProvider`, `useVerificationLock` | [Form fields](../components/form-fields.md#verificationcode) |
|
|
20
|
+
|
|
21
|
+
## Wire it once
|
|
22
|
+
|
|
23
|
+
Put one `VerificationLockProvider` around the screens that ask for a code. An
|
|
24
|
+
authenticator or recovery code that is `locked`, or has `attemptsLeft: 0`,
|
|
25
|
+
locks every such field under the provider until the page is loaded again: a
|
|
26
|
+
remount, a step change or closing the dialog does not lift it. An e-mailed
|
|
27
|
+
code is never held by it. Without a provider the lock covers the document.
|
|
28
|
+
|
|
29
|
+
Then map your answer to props, in one place:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { VerificationCode } from '@nxgt/material/components';
|
|
33
|
+
|
|
34
|
+
<VerificationCode
|
|
35
|
+
via="secondFactor" // or 'email'
|
|
36
|
+
attemptsLeft={answer?.attemptsLeft}
|
|
37
|
+
error={answer?.error} // 'invalid' | 'expired' | 'failed' | 'throttled' | a node
|
|
38
|
+
retryAfter={answer?.retryAfter} // seconds, with 'throttled'
|
|
39
|
+
locked={answer?.locked}
|
|
40
|
+
pending={pending}
|
|
41
|
+
onSubmit={(code, { kind }) => send(code, kind)} // 'code' | 'recovery'
|
|
42
|
+
onResend={via === 'email' ? sendAgain : undefined}
|
|
43
|
+
/>;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The framework-free part (`createVerificationLock`, `normalizeRecoveryCode`) is
|
|
47
|
+
in `@nxgt/material-core/two-factor`.
|
|
48
|
+
|
|
49
|
+
## Replacing what an app wrote itself
|
|
50
|
+
|
|
51
|
+
| An app's own | Replace with | Notes |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| A "confirm it's you" dialog with a state machine (asking, failed, code, expired, confirmed) | `StepUpDialog` | Keep your state; map it to `via`, `requestError`, `error`, `attemptsLeft`, `pending`, `confirming`. Leave `via` out while the code is requested. The replay of the blocked action stays yours |
|
|
54
|
+
| A code input with a "use a recovery code" switch, in a sign-in step, an activation or a turn-off | `VerificationCode` (`allowRecovery` by default for `via="secondFactor"`; `destructive` for turning off) | The recovery code is trimmed and cut at 40; `kind` tells you which it is |
|
|
55
|
+
| A card with the QR code, the key, a sessions note and a first code | `EnrolmentCard` | `sessionsNotice` is a default, your wording or `false`; `status` is a line over the field |
|
|
56
|
+
| A card with the codes left and a "new codes" action | `RecoveryCodesStatus`, then the existing `RecoveryCodes` for the new set | `remaining` is `null` when not known; `requireCode={false}` is a plain button |
|
|
57
|
+
| A "this is required" notice with a link | `Alert` with `actions` (a `LinkButton`) | The wording stays yours |
|
|
58
|
+
| A "3 tries left" or "locked" message the app kept in its own state | `attemptsLeft` and `locked` | A wrong code empties and focuses the field; no tries left locks an app code and expires an e-mailed one |
|
|
59
|
+
| A hand-made lock that survives a remount or a closed dialog | `VerificationLockProvider` | `useVerificationLock()` reads it from the rest of the app |
|
|
60
|
+
| Cancel buttons drawn `outlined` or `ghost` | nothing: they are tonal | `cancelVariant` on `StepUpDialog` and `EnrolmentCard` restores another look |
|
|
61
|
+
|
|
62
|
+
What stays in the app: every request, the mapping of a server error to the
|
|
63
|
+
props above, where the challenge is kept, the replay of the action a step-up
|
|
64
|
+
unblocked, the leave guard around the new recovery codes, and all wording
|
|
65
|
+
(`labels`, or your translations under `material.*`).
|
|
66
|
+
|
|
67
|
+
## On a phone and for assistive technology
|
|
68
|
+
|
|
69
|
+
Every control has a 44 px target on a coarse pointer, the slots shrink to fit
|
|
70
|
+
320 px, and the dialog's footer stays one row (`DialogFooter`'s
|
|
71
|
+
`stackOnPhone={false}`). A refusal after a submit is the field's accessible description (the focus goes
|
|
72
|
+
there); one polite live region announces what the focus does not read, such
|
|
73
|
+
as a new code; the lock is a `role="alert"` that takes the focus when the
|
|
74
|
+
form's own answer locked it, and describes the disabled field. The code field
|
|
75
|
+
is `autocomplete="one-time-code"`. In a test, find it with
|
|
76
|
+
`getByRole('textbox', { name: 'Code' })`.
|
|
77
|
+
|
|
78
|
+
React only for now; the Vue twin is a `vue` issue.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/roadmap.md
CHANGED
|
@@ -330,6 +330,22 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
330
330
|
|
|
331
331
|
## Shipped
|
|
332
332
|
|
|
333
|
+
- **Two-step verification** — the pieces every app rebuilt for a one-time
|
|
334
|
+
code. `VerificationCode` is the form (app or e-mailed code, "Use a recovery
|
|
335
|
+
code", resend, wrong with the tries left, expired, busy, locked, throttled
|
|
336
|
+
with `retryAfter`: Resend waits, and waits again if it is answered throttled
|
|
337
|
+
again), and
|
|
338
|
+
`VerificationLockProvider` keeps a locked app code locked page-wide.
|
|
339
|
+
`StepUpDialog` is "Confirm it's you"; `EnrolmentCard` turns an authenticator
|
|
340
|
+
app on; `RecoveryCodesStatus` shows the codes left and makes a new set.
|
|
341
|
+
`Alert` takes `actions` and `DialogFooter` `stackOnPhone`; the new Cancel
|
|
342
|
+
buttons are tonal and take `cancelVariant`. The framework-free part is in
|
|
343
|
+
`@nxgt/material-core/two-factor`. React only for now; the Vue port is a
|
|
344
|
+
`vue` issue.
|
|
345
|
+
[Two-step verification guide](guides/two-factor.md) ·
|
|
346
|
+
[VerificationCode](components/form-fields.md#verificationcode) ·
|
|
347
|
+
[StepUpDialog](components/feedback-and-status.md#stepupdialog) ·
|
|
348
|
+
[EnrolmentCard](components/data-display.md#enrolmentcard)
|
|
333
349
|
- **One filter model: defaults, a scope, a refusal, `FilterBar`** — a filter
|
|
334
350
|
field says what an absent value means and what role it plays, so a list can
|
|
335
351
|
start narrowed and a page can keep a value the user never clears.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/material",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.46.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"email": "stevegires@gmail.com"
|
|
@@ -362,7 +362,7 @@
|
|
|
362
362
|
"docs:previews": "rm -rf node_modules/.cache/nxgt-preview-free/missing && NXGT_PREVIEW_FREE=1 vitest run --project previews && bun .storybook/preview-free-report.ts"
|
|
363
363
|
},
|
|
364
364
|
"dependencies": {
|
|
365
|
-
"@nxgt/material-core": "^0.
|
|
365
|
+
"@nxgt/material-core": "^0.41.0",
|
|
366
366
|
"@base-ui/react": "^1.6.0",
|
|
367
367
|
"@dnd-kit/core": "^6.3.1",
|
|
368
368
|
"@dnd-kit/sortable": "^10.0.0",
|