@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +8 -1
  3. package/dist/{async-slice-D9LAUgth.js → async-slice-DPM9l8dd.js} +2 -2
  4. package/dist/{button-b_kruV_L.js → button-DR_EdcfV.js} +1 -1
  5. package/dist/charts.js +3 -3
  6. package/dist/{chip-6O3t9GXt.js → chip-olzYjs30.js} +1 -1
  7. package/dist/{code-editor-BtDOgUET.js → code-editor-CCc-piPv.js} +4 -4
  8. package/dist/code-editor.js +4 -4
  9. package/dist/{code-language-items-374m-OK8.js → code-language-items-mjN3a33n.js} +2 -2
  10. package/dist/{components-BG-BTfJP.js → components-CfttRxBs.js} +15688 -14567
  11. package/dist/components.js +26 -26
  12. package/dist/{dialog-8Ua9uuJW.js → dialog-DOhEoEP2.js} +4 -4
  13. package/dist/dnd.js +1 -1
  14. package/dist/{dropdown-menu-DJEeT1XT.js → dropdown-menu-CFO3E-rO.js} +1 -1
  15. package/dist/editor.js +7 -7
  16. package/dist/{empty-state-CrhrmNqn.js → empty-state-CTX0gKR3.js} +1 -1
  17. package/dist/hooks.js +28 -40
  18. package/dist/{i18n-provider-hrtFm9jD.js → i18n-provider-BnVQVqAF.js} +1 -1
  19. package/dist/i18n.js +5 -5
  20. package/dist/{icon-badge-Cpke1tCf.js → icon-badge-BRys8Tuo.js} +1 -1
  21. package/dist/lib/components/ui/display/enrolment-card/enrolment-card.d.ts +12 -0
  22. package/dist/lib/components/ui/display/enrolment-card/index.d.ts +2 -0
  23. package/dist/lib/components/ui/display/enrolment-card/partials/enrolment-header.d.ts +14 -0
  24. package/dist/lib/components/ui/display/enrolment-card/partials/enrolment-verification.d.ts +15 -0
  25. package/dist/lib/components/ui/display/enrolment-card/types.d.ts +49 -0
  26. package/dist/lib/components/ui/display/enrolment-card/use-enrolment-card-labels.d.ts +3 -0
  27. package/dist/lib/components/ui/display/index.d.ts +2 -0
  28. package/dist/lib/components/ui/display/recovery-codes-status/index.d.ts +2 -0
  29. package/dist/lib/components/ui/display/recovery-codes-status/partials/recovery-count.d.ts +11 -0
  30. package/dist/lib/components/ui/display/recovery-codes-status/recovery-codes-status.d.ts +12 -0
  31. package/dist/lib/components/ui/display/recovery-codes-status/types.d.ts +44 -0
  32. package/dist/lib/components/ui/display/recovery-codes-status/use-recovery-codes-status-labels.d.ts +3 -0
  33. package/dist/lib/components/ui/feedback/alert/alert.d.ts +7 -1
  34. package/dist/lib/components/ui/feedback/index.d.ts +1 -0
  35. package/dist/lib/components/ui/feedback/step-up-dialog/index.d.ts +2 -0
  36. package/dist/lib/components/ui/feedback/step-up-dialog/partials/step-up-body.d.ts +15 -0
  37. package/dist/lib/components/ui/feedback/step-up-dialog/partials/step-up-footer.d.ts +22 -0
  38. package/dist/lib/components/ui/feedback/step-up-dialog/partials/step-up-header.d.ts +10 -0
  39. package/dist/lib/components/ui/feedback/step-up-dialog/step-up-dialog.d.ts +16 -0
  40. package/dist/lib/components/ui/feedback/step-up-dialog/types.d.ts +55 -0
  41. package/dist/lib/components/ui/feedback/step-up-dialog/use-step-up-labels.d.ts +3 -0
  42. package/dist/lib/components/ui/feedback/step-up-dialog/use-step-up-state.d.ts +12 -0
  43. package/dist/lib/components/ui/feedback/step-up-dialog/use-title-focus.d.ts +5 -0
  44. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  45. package/dist/lib/components/ui/fields/index.d.ts +1 -0
  46. package/dist/lib/components/ui/fields/verification-code/describe-verification.d.ts +53 -0
  47. package/dist/lib/components/ui/fields/verification-code/index.d.ts +4 -0
  48. package/dist/lib/components/ui/fields/verification-code/partials/code-input.d.ts +12 -0
  49. package/dist/lib/components/ui/fields/verification-code/partials/code-message.d.ts +11 -0
  50. package/dist/lib/components/ui/fields/verification-code/partials/code-slots.d.ts +18 -0
  51. package/dist/lib/components/ui/fields/verification-code/partials/code-surface.d.ts +12 -0
  52. package/dist/lib/components/ui/fields/verification-code/partials/form-actions.d.ts +14 -0
  53. package/dist/lib/components/ui/fields/verification-code/partials/label-row.d.ts +10 -0
  54. package/dist/lib/components/ui/fields/verification-code/partials/locked-alert.d.ts +17 -0
  55. package/dist/lib/components/ui/fields/verification-code/partials/method-switch.d.ts +8 -0
  56. package/dist/lib/components/ui/fields/verification-code/partials/recovery-input.d.ts +7 -0
  57. package/dist/lib/components/ui/fields/verification-code/partials/resend-line.d.ts +10 -0
  58. package/dist/lib/components/ui/fields/verification-code/partials/status-line.d.ts +10 -0
  59. package/dist/lib/components/ui/fields/verification-code/throttle.d.ts +10 -0
  60. package/dist/lib/components/ui/fields/verification-code/types.d.ts +138 -0
  61. package/dist/lib/components/ui/fields/verification-code/use-answer-focus.d.ts +32 -0
  62. package/dist/lib/components/ui/fields/verification-code/use-code-entry.d.ts +43 -0
  63. package/dist/lib/components/ui/fields/verification-code/use-code-lock.d.ts +36 -0
  64. package/dist/lib/components/ui/fields/verification-code/use-code-resend.d.ts +29 -0
  65. package/dist/lib/components/ui/fields/verification-code/use-code-submit.d.ts +23 -0
  66. package/dist/lib/components/ui/fields/verification-code/use-entry-anchors.d.ts +6 -0
  67. package/dist/lib/components/ui/fields/verification-code/use-focus-on-switch.d.ts +7 -0
  68. package/dist/lib/components/ui/fields/verification-code/use-resend-focus.d.ts +20 -0
  69. package/dist/lib/components/ui/fields/verification-code/use-resend.d.ts +11 -0
  70. package/dist/lib/components/ui/fields/verification-code/use-short-reminder.d.ts +24 -0
  71. package/dist/lib/components/ui/fields/verification-code/use-stale-answer.d.ts +52 -0
  72. package/dist/lib/components/ui/fields/verification-code/use-throttle-focus.d.ts +16 -0
  73. package/dist/lib/components/ui/fields/verification-code/use-throttle.d.ts +10 -0
  74. package/dist/lib/components/ui/fields/verification-code/use-typed-value.d.ts +18 -0
  75. package/dist/lib/components/ui/fields/verification-code/use-verification-labels.d.ts +3 -0
  76. package/dist/lib/components/ui/fields/verification-code/verification-code.d.ts +11 -0
  77. package/dist/lib/components/ui/fields/verification-code/verification-lock-provider.d.ts +54 -0
  78. package/dist/lib/components/ui/overlays/dialog/dialog-parts.d.ts +8 -1
  79. package/dist/lib/i18n/resources/en/en.d.ts +52 -0
  80. package/dist/lib/i18n/resources/en/material.json.d.ts +52 -0
  81. package/dist/lib/i18n/resources/fr/fr.d.ts +52 -0
  82. package/dist/lib/i18n/resources/fr/material.json.d.ts +52 -0
  83. package/dist/lib/i18n/resources/resources.d.ts +104 -0
  84. package/dist/lib/i18n/translator.d.ts +1 -1
  85. package/dist/lib.js +1 -1
  86. package/dist/main.js +43 -43
  87. package/dist/map.js +4 -4
  88. package/dist/material.css +1 -1
  89. package/dist/pdf-reader.js +1 -1
  90. package/dist/print.js +2 -2
  91. package/dist/{public-DPPmNtZB.js → public-BmA6r0EK.js} +6 -6
  92. package/dist/{rich-code-block-KQYR0qt3.js → rich-code-block-BgPtzSnv.js} +4 -4
  93. package/dist/{sortable-provider-hPPTMtJA.js → sortable-provider-CceARBYP.js} +1 -1
  94. package/dist/{spinner-Dd0os4cb.js → spinner-7orqNAEy.js} +1 -1
  95. package/dist/{stored-language-Blyb68vN.js → stored-language-BHmxU75w.js} +104 -0
  96. package/dist/theme-picker.js +2 -2
  97. package/dist/{theme-toggle-BDRRkkiA.js → theme-toggle-_RJgLHs9.js} +1 -1
  98. package/dist/{toolbar-items-D3hw7XZE.js → toolbar-items-BCy6DMzb.js} +2 -2
  99. package/dist/{translate-material-CdJBbURx.js → translate-material-n0YBXLGp.js} +2 -2
  100. package/dist/{translator-38JuN_VD.js → translator-B_ue2XEg.js} +1 -1
  101. package/dist/{use-chat-composer-utGkZQ6X.js → use-chat-composer-FDmwZSua.js} +12 -12
  102. package/dist/{use-heading-spy-bTccmEMF.js → use-heading-spy-O6_EATtA.js} +43 -31
  103. package/dist/{use-material-translation-DLsfkMxp.js → use-material-translation-CkPcqI6q.js} +2 -2
  104. package/docs/README.md +1 -0
  105. package/docs/components/README.md +1 -1
  106. package/docs/components/buttons-and-actions.md +2 -1
  107. package/docs/components/data-display.md +90 -0
  108. package/docs/components/feedback-and-status.md +113 -1
  109. package/docs/components/form-fields.md +163 -0
  110. package/docs/components/overlays-and-menus.md +3 -1
  111. package/docs/guides/two-factor.md +78 -0
  112. package/docs/previews/data-display-enrolmentcard.webp +0 -0
  113. package/docs/previews/data-display-recoverycodesstatus.webp +0 -0
  114. package/docs/previews/feedback-status-stepupdialog.webp +0 -0
  115. package/docs/previews/form-fields-verificationcode.webp +0 -0
  116. package/docs/roadmap.md +16 -0
  117. 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
+ ![EnrolmentCard](../previews/data-display-enrolmentcard.webp)
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
  ![EnrolmentSecret](../previews/data-display-enrolmentsecret.webp)
@@ -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
+ ![RecoveryCodesStatus](../previews/data-display-recoverycodesstatus.webp)
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
+ ![StepUpDialog](../previews/feedback-status-stepupdialog.webp)
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
  ![EmptyState](../previews/feedback-status-emptystate.webp)
@@ -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
+ ![VerificationCode](../previews/form-fields-verificationcode.webp)
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
  ![InputGroupField](../previews/form-fields-inputgroupfield.webp)
@@ -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. With `prefers-reduced-motion`, nothing moves: it appears and goes.
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.
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.45.0",
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.40.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",