@nxgt/material 3.45.0 → 3.47.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 +63 -0
- package/README.md +10 -3
- package/dist/{async-slice-D9LAUgth.js → async-slice-DlvKCKAa.js} +2 -2
- package/dist/{button-b_kruV_L.js → button-yaltbQmC.js} +1 -1
- package/dist/charts.js +3 -3
- package/dist/{chip-6O3t9GXt.js → chip-BqWbbr1b.js} +1 -1
- package/dist/{code-editor-BtDOgUET.js → code-editor-C9qBNoYb.js} +4 -4
- package/dist/code-editor.js +4 -4
- package/dist/{code-language-items-374m-OK8.js → code-language-items-DAHtfQ7W.js} +2 -2
- package/dist/{components-BG-BTfJP.js → components-DFPy96C3.js} +16282 -14755
- package/dist/components.js +26 -26
- package/dist/{dialog-8Ua9uuJW.js → dialog-CCCcnTIx.js} +4 -4
- package/dist/dnd.js +1 -1
- package/dist/{dropdown-menu-DJEeT1XT.js → dropdown-menu-Df8785nt.js} +1 -1
- package/dist/editor.js +7 -7
- package/dist/{empty-state-CrhrmNqn.js → empty-state-DcF4TBzi.js} +1 -1
- package/dist/hooks.js +28 -40
- package/dist/{i18n-provider-hrtFm9jD.js → i18n-provider-C0fB0sEL.js} +1 -1
- package/dist/i18n.js +5 -5
- package/dist/{icon-badge-Cpke1tCf.js → icon-badge-aJ22mKGh.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/display/timeline/index.d.ts +1 -1
- package/dist/lib/components/ui/display/timeline/partials/timeline-day-heading.d.ts +14 -0
- package/dist/lib/components/ui/display/timeline/partials/timeline-empty.d.ts +8 -0
- package/dist/lib/components/ui/display/timeline/partials/timeline-groups.d.ts +22 -0
- package/dist/lib/components/ui/display/timeline/partials/timeline-item-body.d.ts +13 -0
- package/dist/lib/components/ui/display/timeline/partials/timeline-item-card.d.ts +5 -0
- package/dist/lib/components/ui/display/timeline/partials/timeline-item-rail.d.ts +8 -0
- package/dist/lib/components/ui/display/timeline/partials/timeline-item-time.d.ts +12 -0
- package/dist/lib/components/ui/display/timeline/partials/timeline-skeleton.d.ts +4 -0
- package/dist/lib/components/ui/display/timeline/timeline-item.d.ts +13 -2
- package/dist/lib/components/ui/display/timeline/timeline-marker.d.ts +12 -0
- package/dist/lib/components/ui/display/timeline/timeline-rows.d.ts +23 -0
- package/dist/lib/components/ui/display/timeline/timeline.d.ts +32 -2
- package/dist/lib/components/ui/display/timeline/types.d.ts +6 -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/layout/stepper/partials/header-nav.d.ts +10 -0
- package/dist/lib/components/ui/layout/stepper/partials/header-segments.d.ts +6 -0
- package/dist/lib/components/ui/layout/stepper/partials/header-summary.d.ts +12 -0
- package/dist/lib/components/ui/layout/stepper/partials/stepper-content.d.ts +8 -0
- package/dist/lib/components/ui/layout/stepper/partials/stepper-step-text.d.ts +13 -0
- package/dist/lib/components/ui/layout/stepper/stepper-connector.d.ts +6 -0
- package/dist/lib/components/ui/layout/stepper/stepper-header.d.ts +12 -0
- package/dist/lib/components/ui/layout/stepper/stepper-icon.d.ts +5 -0
- package/dist/lib/components/ui/layout/stepper/stepper-layout.d.ts +25 -0
- package/dist/lib/components/ui/layout/stepper/stepper-step.d.ts +9 -2
- package/dist/lib/components/ui/layout/stepper/stepper.d.ts +13 -3
- package/dist/lib/components/ui/overlays/dialog/dialog-parts.d.ts +8 -1
- package/dist/lib/i18n/resources/en/en.d.ts +59 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +61 -2
- package/dist/lib/i18n/resources/fr/fr.d.ts +59 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +61 -2
- package/dist/lib/i18n/resources/resources.d.ts +118 -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-zVb_Nq_D.js} +6 -6
- package/dist/{rich-code-block-KQYR0qt3.js → rich-code-block-DMWZZJma.js} +4 -4
- package/dist/{sortable-provider-hPPTMtJA.js → sortable-provider-DstbgMVf.js} +1 -1
- package/dist/{spinner-Dd0os4cb.js → spinner-C4CgDEYm.js} +1 -1
- package/dist/{stored-language-Blyb68vN.js → stored-language-IaL1Ftn_.js} +122 -4
- package/dist/styles.css +8 -0
- package/dist/theme-picker.js +2 -2
- package/dist/{theme-toggle-BDRRkkiA.js → theme-toggle-CGW2_K54.js} +1 -1
- package/dist/{toolbar-items-D3hw7XZE.js → toolbar-items-CsLGJMEJ.js} +2 -2
- package/dist/{translate-material-CdJBbURx.js → translate-material-v9u-e7Kd.js} +2 -2
- package/dist/{translator-38JuN_VD.js → translator-Dk4_qE6U.js} +1 -1
- package/dist/{use-chat-composer-utGkZQ6X.js → use-chat-composer-Brcb5f7r.js} +12 -12
- package/dist/{use-heading-spy-bTccmEMF.js → use-heading-spy-DjKixjLm.js} +43 -31
- package/dist/{use-material-translation-DLsfkMxp.js → use-material-translation-DOkTaWK_.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 +133 -4
- package/docs/components/feedback-and-status.md +113 -1
- package/docs/components/form-fields.md +163 -0
- package/docs/components/navigation-and-layout.md +66 -23
- package/docs/components/overlays-and-menus.md +3 -1
- package/docs/guides/i18n.md +4 -1
- package/docs/guides/ssr.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/data-display-timeline.webp +0 -0
- package/docs/previews/feedback-status-stepupdialog.webp +0 -0
- package/docs/previews/form-fields-verificationcode.webp +0 -0
- package/docs/previews/navigation-layout-stepper.webp +0 -0
- package/docs/roadmap.md +30 -0
- package/lib/styles.css +8 -0
- package/package.json +2 -2
|
@@ -24,7 +24,7 @@ from `@nxgt/material/components`.
|
|
|
24
24
|
| <img src="../previews/data-display-entityheader.webp" width="96" alt=""> | [EntityHeader](#entityheader) | Page header with status, metadata and actions |
|
|
25
25
|
| <img src="../previews/data-display-listtile.webp" width="96" alt=""> | [ListTile](#listtile) | Row with leading / trailing slots, row actions, chips and a divided variant |
|
|
26
26
|
| <img src="../previews/data-display-accordioncard.webp" width="96" alt=""> | [AccordionCard](#accordioncard) | Accordion rendered from `items` |
|
|
27
|
-
| <img src="../previews/data-display-timeline.webp" width="96" alt=""> | [Timeline](#timeline) | Vertical event timeline
|
|
27
|
+
| <img src="../previews/data-display-timeline.webp" width="96" alt=""> | [Timeline](#timeline) | Vertical event timeline: shaped markers, cards, alternating sides, day groups (`DaySeparator` headings, words from `material.day-separator.*`), relative times; `layout="auto"` needs a parent with a definite width (`rail` in a `w-fit` parent) |
|
|
28
28
|
| <img src="../previews/data-display-changehistory.webp" width="96" alt=""> | [ChangeHistory](#changehistory) | History of the changes made to anything, as a rail or as cards |
|
|
29
29
|
| <img src="../previews/data-display-zonedtime.webp" width="96" alt=""> | [ZonedTime](#zonedtime) | A moment written in a given time zone, identical on the server and the client |
|
|
30
30
|
| <img src="../previews/data-display-eventchip.webp" width="96" alt=""> | [EventChip](#eventchip) | Calendar event pill, soft, solid or outlined |
|
|
@@ -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
|
|
|
@@ -963,7 +965,12 @@ import { AccordionCard } from '@nxgt/material/components';
|
|
|
963
965
|
|
|
964
966
|

|
|
965
967
|
|
|
966
|
-
Vertical event timeline with toned
|
|
968
|
+
Vertical event timeline with toned markers and relative times. Each marker is
|
|
969
|
+
44px and takes a shape by tone (circle, squircle, cookie, clover, diamond,
|
|
970
|
+
burst) in the tone's container colour; an item with `children` is a rounded
|
|
971
|
+
card with a subtle tone bar. Once the timeline's own container is 48rem wide
|
|
972
|
+
(a container query, not the viewport) the two sides alternate; narrower, it is
|
|
973
|
+
a single rail. `grouped` opens each day with a `DaySeparator` heading.
|
|
967
974
|
|
|
968
975
|
```tsx
|
|
969
976
|
import { Package, Pencil } from 'lucide-react';
|
|
@@ -982,11 +989,45 @@ Parts: `Timeline`, `TimelineItem` (one row, for your own list),
|
|
|
982
989
|
|
|
983
990
|
| Prop | Type | Default | Effect |
|
|
984
991
|
| --- | --- | --- | --- |
|
|
985
|
-
| `items` | `TimelineItemData[]` | — | `title`, `description`, `timestamp`, `icon` or `avatar`, `tone`, `children` |
|
|
992
|
+
| `items` | `TimelineItemData[]` | — | `title`, `description`, `timestamp`, `icon` or `avatar`, `tone`, `children` (rendered as a card) |
|
|
993
|
+
| `grouped` | `boolean` | `false` | a heading per day ("Today", "Yesterday", a weekday or a date: a `DaySeparator`, real `<h3>`, with a `<time dateTime>` and its count); items keep the order given |
|
|
994
|
+
| `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | `3` | the level of the day headings |
|
|
995
|
+
| `layout` | `'auto' \| 'rail'` | `'auto'` | `'auto'` alternates sides in a wide container (it measures its own container: its parent needs a definite width, see below); `'rail'` never does |
|
|
996
|
+
| `timeZone` | `string` | — | the zone the days are cut in |
|
|
997
|
+
| `now` | `Date` | — | the reference instant for "Today" and "Yesterday" |
|
|
986
998
|
| `loading` | `boolean` | `false` | skeleton rows |
|
|
987
999
|
| `empty` | `ReactNode` | "No activity yet" | shown when `items` is empty |
|
|
988
1000
|
|
|
989
|
-
|
|
1001
|
+
```tsx
|
|
1002
|
+
// Grouped on the server too: fix the zone and the reference instant.
|
|
1003
|
+
<Timeline items={items} grouped timeZone="UTC" now={new Date('2026-03-15T12:00:00Z')} aria-label="Order history" />
|
|
1004
|
+
|
|
1005
|
+
// One rail, never alternating sides.
|
|
1006
|
+
<Timeline items={items} layout="rail" aria-label="Order history" />
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
**Server rendering.** Without both `timeZone` and `now`, `grouped` renders no
|
|
1010
|
+
day headings on the server and on the first client render, and adds them once
|
|
1011
|
+
mounted (and logs a development warning once). With both, the headings render
|
|
1012
|
+
on the server too. The list is one flat `<ol>`: each heading sits inside the first `<li>` of
|
|
1013
|
+
its day, ahead of the item's content (a list may only hold `<li>`s, so a
|
|
1014
|
+
standalone heading row fails axe), never around the items, so an item keeps its
|
|
1015
|
+
parent, its key and its state when they appear (a counter inside an item is not reset).
|
|
1016
|
+
**Width.** `layout="auto"` is a container query on the Timeline itself, so its
|
|
1017
|
+
parent needs a definite width (a block, a grid cell, a fixed width). In a
|
|
1018
|
+
shrink-to-fit parent (`w-fit`, an inline flex item) it collapses to 0 wide:
|
|
1019
|
+
use `layout="rail"` there, which sets no container. In right-to-left the wide
|
|
1020
|
+
layout mirrors, time labels included. `now` also fixes the relative times
|
|
1021
|
+
("5 minutes ago") of the items. **Accessibility:** the list keeps its
|
|
1022
|
+
`aria-label`, markers are decorative, each day heading is a real heading whose
|
|
1023
|
+
count reads as "N events", and every item of a day has `aria-describedby`
|
|
1024
|
+
pointing to that heading (a nested list labelled by it would put the items
|
|
1025
|
+
under a new parent). **Migrating:** the new look is the default; pass
|
|
1026
|
+
`layout="rail"` for a single rail. Children now render as a card, not as text
|
|
1027
|
+
under the title. New key: `material.timeline.events`; the day words come from
|
|
1028
|
+
`material.day-separator.today` and `yesterday`.
|
|
1029
|
+
|
|
1030
|
+
**Storybook:** `Data display/Timeline` · **Vue:** `Timeline`, `TimelineItem`, `RelativeTime` (without `now`, `tooltip`, `focusable` and `ref`, React only); the redesign and `grouped`, `layout`, `timeZone`, `now` are React only for now (#1810)
|
|
990
1031
|
|
|
991
1032
|
---
|
|
992
1033
|
|
|
@@ -1375,6 +1416,56 @@ not that span.
|
|
|
1375
1416
|
|
|
1376
1417
|
---
|
|
1377
1418
|
|
|
1419
|
+
## EnrolmentCard
|
|
1420
|
+
|
|
1421
|
+

|
|
1422
|
+
|
|
1423
|
+
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.
|
|
1424
|
+
|
|
1425
|
+
```tsx
|
|
1426
|
+
import { EnrolmentCard, VerificationLockProvider } from '@nxgt/material/components';
|
|
1427
|
+
|
|
1428
|
+
<VerificationLockProvider>
|
|
1429
|
+
<EnrolmentCard
|
|
1430
|
+
uri={enrolment.uri}
|
|
1431
|
+
secret={enrolment.secret}
|
|
1432
|
+
onSubmit={(code) => activate(code)} // the code is an app's code
|
|
1433
|
+
onCancel={() => navigate('/settings/security')}
|
|
1434
|
+
error={answer.error}
|
|
1435
|
+
attemptsLeft={answer.attemptsLeft}
|
|
1436
|
+
locked={answer.locked}
|
|
1437
|
+
pending={activating}
|
|
1438
|
+
/>
|
|
1439
|
+
</VerificationLockProvider>
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
| Prop | Type | Default | Effect |
|
|
1443
|
+
| --- | --- | --- | --- |
|
|
1444
|
+
| `uri`, `secret` | `string` | — | passed to `EnrolmentSecret` |
|
|
1445
|
+
| `onSubmit` | `(code, { kind }) => unknown` | — | called with the six digits; return a promise to hold a second submit |
|
|
1446
|
+
| `onCancel` | `() => void` | — | draws a Cancel beside Activate; left out, no Cancel |
|
|
1447
|
+
| `cancelVariant` | `ButtonVariant` | `'tonal'` | the Cancel button's variant; `'outlined'` or `'ghost'` restores an earlier look |
|
|
1448
|
+
| `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). |
|
|
1449
|
+
| `title`, `titleAs`, `description` | `ReactNode`, `'h2'…`, `ReactNode \| false` | built-in, `'h2'` | the heading ("Set up your authenticator app"), its level, and the line under it |
|
|
1450
|
+
| `focusTitle` | `boolean` | `true` | the title takes the focus once mounted |
|
|
1451
|
+
| `length` | `number` | `6` | the slots in the code field, as `VerificationCode` |
|
|
1452
|
+
| `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 |
|
|
1453
|
+
| `status` | `ReactNode` | — | a notice line over the field ("Your session is confirmed. Type the next code.") |
|
|
1454
|
+
| `secretProps` | `Partial<EnrolmentSecretProps>` | — | the QR size, labels and copy icon |
|
|
1455
|
+
| `submitLabel`, `labels`, `icons` | — | "Activate" | the button, every string (`title`, `description`, `activate`, `cancel`, `sessionsNotice`, and those of `VerificationCode`), the locked alert's icon |
|
|
1456
|
+
|
|
1457
|
+
There is no recovery-code switch: no recovery code exists yet. The card reads
|
|
1458
|
+
the lock of the nearest `VerificationLockProvider` (or the document's), so a
|
|
1459
|
+
lock set by another code form disables its field and leaves Cancel usable.
|
|
1460
|
+
|
|
1461
|
+
**In a test**, find the field by `getByRole('textbox', { name: 'Code' })`, the
|
|
1462
|
+
button by `getByRole('button', { name: 'Activate' })` and the lock by
|
|
1463
|
+
`getByRole('alert')`.
|
|
1464
|
+
|
|
1465
|
+
**Storybook:** `Data display/EnrolmentCard`, `…/Shared lock`, `Guides/Accessibility/Touch targets/Verification code` · **Vue:** not yet
|
|
1466
|
+
|
|
1467
|
+
---
|
|
1468
|
+
|
|
1378
1469
|
## EnrolmentSecret
|
|
1379
1470
|
|
|
1380
1471
|

|
|
@@ -1477,3 +1568,41 @@ useEffect(() => {
|
|
|
1477
1568
|
```
|
|
1478
1569
|
|
|
1479
1570
|
**Storybook:** `Data display/RecoveryCodes` · **Vue:** `RecoveryCodes` (`v-model:saved`, `@done`, `@copy`; slots `title`, `description`, `warning`, `copy-icon`, `download-icon`, `print-icon`)
|
|
1571
|
+
|
|
1572
|
+
---
|
|
1573
|
+
|
|
1574
|
+
## RecoveryCodesStatus
|
|
1575
|
+
|
|
1576
|
+

|
|
1577
|
+
|
|
1578
|
+
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.
|
|
1579
|
+
|
|
1580
|
+
```tsx
|
|
1581
|
+
import { RecoveryCodesStatus, VerificationLockProvider } from '@nxgt/material/components';
|
|
1582
|
+
|
|
1583
|
+
<VerificationLockProvider>
|
|
1584
|
+
<RecoveryCodesStatus
|
|
1585
|
+
remaining={account.recoveryCodesLeft}
|
|
1586
|
+
onRegenerate={(code, options) => regenerate(code, options?.kind)}
|
|
1587
|
+
error={answer.error}
|
|
1588
|
+
attemptsLeft={answer.attemptsLeft}
|
|
1589
|
+
locked={answer.locked}
|
|
1590
|
+
pending={regenerating}
|
|
1591
|
+
/>
|
|
1592
|
+
</VerificationLockProvider>
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
| Prop | Type | Default | Effect |
|
|
1596
|
+
| --- | --- | --- | --- |
|
|
1597
|
+
| `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 |
|
|
1598
|
+
| `onRegenerate` | `(code?, { kind }?) => unknown` | — | with `requireCode`, called with the code and its kind (`'code'` or `'recovery'`); without it, called with no argument |
|
|
1599
|
+
| `requireCode` | `boolean` | `true` | `true`: a `VerificationCode` whose button is "New codes"; `false`: a plain button |
|
|
1600
|
+
| `allowRecovery` | `boolean` | `true` | offers "Use a recovery code" in the form |
|
|
1601
|
+
| `length` | `number` | `6` | the slots in the code field, as `VerificationCode` |
|
|
1602
|
+
| `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) |
|
|
1603
|
+
| `title`, `titleAs`, `description` | `ReactNode`, `'h2'…`, `ReactNode \| false` | built-in, `'h2'` | the heading ("Recovery codes"), its level, the line under it |
|
|
1604
|
+
| `labels`, `icons` | partial objects | built-in | `title`, `description`, `remaining` (`(count) => string`), `noneLeft`, `noneLeftHint`, `newCodes`, `newCodesHint`, and those of `VerificationCode` |
|
|
1605
|
+
|
|
1606
|
+
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.
|
|
1607
|
+
|
|
1608
|
+
**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
|

|
|
@@ -21,7 +21,7 @@ documentation-site shell. Everything is imported from
|
|
|
21
21
|
| <img src="../previews/navigation-layout-pager.webp" width="96" alt=""> | [Pager](#pager) | Previous / next cards |
|
|
22
22
|
| <img src="../previews/navigation-layout-seealso.webp" width="96" alt=""> | [SeeAlso](#seealso) | Related links |
|
|
23
23
|
| <img src="../previews/navigation-layout-toc.webp" width="96" alt=""> | [Toc](#toc) | On-this-page outline |
|
|
24
|
-
| <img src="../previews/navigation-layout-stepper.webp" width="96" alt=""> | [Stepper](#stepper) | Clickable steps with progress |
|
|
24
|
+
| <img src="../previews/navigation-layout-stepper.webp" width="96" alt=""> | [Stepper](#stepper) | Clickable steps with progress: shaped indicators, optional steps, a phone header |
|
|
25
25
|
| <img src="../previews/navigation-layout-steps.webp" width="96" alt=""> | [Steps](#steps) | Numbered editorial steps |
|
|
26
26
|
| <img src="../previews/navigation-layout-hero.webp" width="96" alt=""> | [Hero](#hero) | Landing band |
|
|
27
27
|
| <img src="../previews/navigation-layout-doc.webp" width="96" alt=""> | [Doc](#doc) | Documentation site shell |
|
|
@@ -657,23 +657,41 @@ const items: TocItem[] = [
|
|
|
657
657
|
|
|
658
658
|

|
|
659
659
|
|
|
660
|
-
Clickable horizontal or vertical steps with status and progress
|
|
660
|
+
Clickable horizontal or vertical steps with status and progress. The look is
|
|
661
|
+
expressive: the current step is a cookie, a completed one a filled circle with
|
|
662
|
+
a check, an upcoming one a muted outlined circle and a failed one a cookie in
|
|
663
|
+
the error container, joined by thick pill tracks that fill with the spatial
|
|
664
|
+
spring (no motion under `prefers-reduced-motion`). Below `sm` a header card
|
|
665
|
+
replaces the steps.
|
|
661
666
|
|
|
662
667
|
```tsx
|
|
663
668
|
import { CreditCard, Truck, User } from 'lucide-react';
|
|
664
|
-
import { Stepper
|
|
669
|
+
import { Stepper } from '@nxgt/material/components';
|
|
670
|
+
|
|
671
|
+
<Stepper
|
|
672
|
+
active={active}
|
|
673
|
+
onChange={setActive}
|
|
674
|
+
steps={[
|
|
675
|
+
{ icon: <User />, title: 'Account', description: 'Your profile details.' },
|
|
676
|
+
{ icon: <Truck />, title: 'Shipping', status: 'completed' },
|
|
677
|
+
{ icon: <CreditCard />, title: 'Payment' },
|
|
678
|
+
]}
|
|
679
|
+
/>
|
|
680
|
+
```
|
|
665
681
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
682
|
+
An optional step with a card under it, and Back / Next in the phone header
|
|
683
|
+
(they call `onChange(active ± 1)`):
|
|
684
|
+
|
|
685
|
+
```tsx
|
|
686
|
+
<Stepper
|
|
687
|
+
active={active}
|
|
688
|
+
onChange={setActive}
|
|
689
|
+
steps={[
|
|
690
|
+
{ title: 'Account' },
|
|
691
|
+
{ title: 'Address', optional: true, content: <p>Skip this if you pick up in store.</p> },
|
|
692
|
+
{ title: 'Payment' },
|
|
693
|
+
]}
|
|
694
|
+
/>
|
|
677
695
|
```
|
|
678
696
|
|
|
679
697
|
Parts: `Stepper`, `StepperIndicator` (the progress bar alone: `current`,
|
|
@@ -681,10 +699,10 @@ Parts: `Stepper`, `StepperIndicator` (the progress bar alone: `current`,
|
|
|
681
699
|
|
|
682
700
|
| Prop | Type | Default | Effect |
|
|
683
701
|
| --- | --- | --- | --- |
|
|
684
|
-
| `steps` | `{ icon?; title; description?; status? }[]` | — | `status`: `'completed' \| 'error' \| 'upcoming'`, else derived from `active` |
|
|
685
|
-
| `active` | `number` | `0` | current step; its `description` shows under the
|
|
702
|
+
| `steps` | `{ icon?; title; description?; status?; optional?; content? }[]` | — | `status`: `'completed' \| 'error' \| 'upcoming'`, else derived from `active`. `optional` adds "· Optional" to the label; `content` is a tinted card shown under the current step |
|
|
703
|
+
| `active` | `number` | `0` | current step; its `description` shows under the steps (in the header card on a phone) |
|
|
686
704
|
| `onChange` | `(index: number) => void` | — | makes steps clickable |
|
|
687
|
-
| `orientation` | `'horizontal' \| 'vertical' \| 'responsive'` | `'horizontal'` | `'vertical'`: a column from `
|
|
705
|
+
| `orientation` | `'horizontal' \| 'vertical' \| 'responsive'` | `'horizontal'` | `'vertical'`: a column from `sm` (640px), phones get the header card. `'responsive'`: every step in a column below `sm` (640px), a row from `sm` |
|
|
688
706
|
| `renderStepLabel` | `(step, index) => ReactNode` | — | replaces the "Step n" caption |
|
|
689
707
|
| `name` | `string` | — | the stepper's `data-pw` (`stepper` without it), and the prefix of each step's: `<name>-step-N` instead of `step-N` |
|
|
690
708
|
|
|
@@ -692,12 +710,37 @@ Each step is an `<li>` with `data-pw="step-N"` (`<name>-step-N` with `name`),
|
|
|
692
710
|
N counted from 0, for tests to select; the current step's button carries
|
|
693
711
|
`aria-current="step"`, and a completed or failed step is announced as such.
|
|
694
712
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
713
|
+
Below `sm` a header card shows "Step 3 of 5", the title, the description and a
|
|
714
|
+
segmented progress bar (one segment per step, toned by state); with `onChange`
|
|
715
|
+
it also has Back and Next buttons, which stay focusable but inactive (`aria-disabled`) at the ends. Titles clamp to two
|
|
716
|
+
lines. The info tooltip on the current step is gone: read the description
|
|
717
|
+
under the steps.
|
|
718
|
+
|
|
719
|
+
**Server rendering.** The layout is chosen by CSS breakpoints, not by the
|
|
720
|
+
screen width in JavaScript, so the server and the client render the same
|
|
721
|
+
markup and nothing switches after hydration. Both layouts are in the markup
|
|
722
|
+
and the unused one is hidden; ids stay unique.
|
|
723
|
+
|
|
724
|
+
**Accessibility.** The steps are a list, the current step's button carries
|
|
725
|
+
`aria-current="step"`, a completed or failed step is announced as such, and
|
|
726
|
+
steps are focusable when `onChange` is given. The header's progress bar is
|
|
727
|
+
decorative: the "Step n of N" text carries the meaning. The live header
|
|
728
|
+
announces a failed current step and the number of failed steps; Back and Next
|
|
729
|
+
use `aria-disabled` and stay focusable at the ends.
|
|
730
|
+
|
|
731
|
+
**Migrating.** The new look is the default and the props are unchanged;
|
|
732
|
+
`optional` and `content` are additive. Update any test that selected the old
|
|
733
|
+
info button on the current step. New `material.stepper.*` keys: `step-of`,
|
|
734
|
+
`back`, `next`, `optional`, `attention`, `failed`. The `attention` text ("Needs attention") is shown under the title of a failed step, as a visual cue only: the announced status stays "Error".
|
|
735
|
+
|
|
736
|
+
Migration notes:
|
|
737
|
+
|
|
738
|
+
- `data-orientation` now repeats the `orientation` prop; a `responsive` Stepper no longer reports `vertical` or `horizontal` by width.
|
|
739
|
+
- Below 640px the horizontal step list is hidden and the header card replaces it.
|
|
740
|
+
- `horizontal` and `vertical` switch at `sm` (640px), not `md` (768px).
|
|
741
|
+
- `Stepper` no longer renders `StepperIndicator` (still exported).
|
|
742
|
+
|
|
743
|
+
**Storybook:** `Navigation & layout/Stepper` · **Vue:** `Stepper`, with the earlier look: the redesign, `optional` and `content` are React only for now (#1810)
|
|
701
744
|
|
|
702
745
|
---
|
|
703
746
|
|
|
@@ -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
|