@nxgt/material 2.16.0 → 2.17.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 (106) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +9 -4
  3. package/dist/{_Uint8Array-CjLUgWgf.js → _Uint8Array-C7apDrb7.js} +1 -1
  4. package/dist/{_overRest-HfSwfmH1.js → _overRest-CCAbV-eT.js} +1 -1
  5. package/dist/async-slice-Cuekts5f.js +459 -0
  6. package/dist/{blockDiagram-BEXU5L5S-DSNWJcbS.js → blockDiagram-BEXU5L5S-K2MjXCm9.js} +2 -2
  7. package/dist/{chunk-3FUC2YCW-CcZvlyvW.js → chunk-3FUC2YCW-6KyD6CAZ.js} +2 -2
  8. package/dist/{chunk-7M6MHVWA-21OdxoBq.js → chunk-7M6MHVWA-CNWcBB_6.js} +1 -1
  9. package/dist/{chunk-GNY47TPC-BZdgq2LF.js → chunk-GNY47TPC-DdqgSBxM.js} +4 -4
  10. package/dist/{chunk-NETBCI7D-CAgvlb0w.js → chunk-NETBCI7D-ZnunhYtw.js} +1 -1
  11. package/dist/{chunk-Z7XXMR3K-BSXPgLtN.js → chunk-Z7XXMR3K-C8MhuVIi.js} +1 -1
  12. package/dist/{classDiagram-v2-NBCMYWYE-D_BHO1iT.js → classDiagram-v2-NBCMYWYE-ByUAGkfa.js} +2 -2
  13. package/dist/components/components.css +1 -0
  14. package/dist/components/print-only.css +44 -0
  15. package/dist/{components-Bkdm6jn6.js → components-CIYQrI45.js} +59379 -58878
  16. package/dist/components.js +6 -6
  17. package/dist/{dagre-6A5THRUB-CsnftAPx.js → dagre-6A5THRUB-C2vz5UY9.js} +64 -64
  18. package/dist/{diagram-22UHCM2B-D0yPa8vo.js → diagram-22UHCM2B-DREk9ku4.js} +2 -2
  19. package/dist/{diagram-3UASUU5V-f8LIX2sj.js → diagram-3UASUU5V-CYkPaos5.js} +1 -1
  20. package/dist/dnd.js +1 -1
  21. package/dist/{elk-276RUBZZ-DI8bDQAL.js → elk-276RUBZZ-CK4KoCtK.js} +3 -3
  22. package/dist/{erDiagram-OPXOYQCR-BGHFtxmB.js → erDiagram-OPXOYQCR-CTfn8QUl.js} +2 -2
  23. package/dist/{field-blur-C-5K6U1F.js → field-blur-3RW0mJb2.js} +2 -2
  24. package/dist/{flowDiagram-KWPJA3E3-CnFLTW-3.js → flowDiagram-KWPJA3E3-CDzuzre6.js} +3 -3
  25. package/dist/{ganttDiagram-FUAMR5RP-B5uyVKE4.js → ganttDiagram-FUAMR5RP-Ci-9ZJz5.js} +1 -1
  26. package/dist/{graphlib-BqO9Go2u.js → graphlib-DxjKaXeR.js} +42 -42
  27. package/dist/hooks.js +38 -26
  28. package/dist/{i18n-provider-C3SlA4J0.js → i18n-provider-Dv9ZSmgO.js} +1 -1
  29. package/dist/i18n.js +6 -6
  30. package/dist/{isArrayLikeObject-83P1fvEs.js → isArrayLikeObject-H8k6W51O.js} +3 -3
  31. package/dist/{isEmpty-C2z3Hypl.js → isEmpty-B0Bavg-e.js} +9 -9
  32. package/dist/{journeyDiagram-ZHPQQLJL-CKtbGcVT.js → journeyDiagram-ZHPQQLJL-CwFYd51O.js} +1 -1
  33. package/dist/lib/components/ui/actions/copy-button/use-copy-status.d.ts +13 -0
  34. package/dist/lib/components/ui/display/enrolment-secret/enrolment-secret.d.ts +30 -0
  35. package/dist/lib/components/ui/display/enrolment-secret/index.d.ts +2 -0
  36. package/dist/lib/components/ui/display/enrolment-secret/use-enrolment-secret-labels.d.ts +11 -0
  37. package/dist/lib/components/ui/display/index.d.ts +2 -0
  38. package/dist/lib/components/ui/display/recovery-codes/index.d.ts +4 -0
  39. package/dist/lib/components/ui/display/recovery-codes/partials/recovery-codes-actions.d.ts +14 -0
  40. package/dist/lib/components/ui/display/recovery-codes/partials/recovery-codes-confirm.d.ts +13 -0
  41. package/dist/lib/components/ui/display/recovery-codes/partials/recovery-codes-list.d.ts +5 -0
  42. package/dist/lib/components/ui/display/recovery-codes/recovery-codes.d.ts +8 -0
  43. package/dist/lib/components/ui/display/recovery-codes/types.d.ts +64 -0
  44. package/dist/lib/components/ui/display/recovery-codes/use-recovery-codes-labels.d.ts +7 -0
  45. package/dist/lib/components/ui/fields/form-section/form-section-classes.d.ts +30 -0
  46. package/dist/lib/components/ui/fields/form-section/form-section.d.ts +25 -0
  47. package/dist/lib/components/ui/fields/form-section/index.d.ts +1 -0
  48. package/dist/lib/components/ui/fields/index.d.ts +1 -0
  49. package/dist/lib/components/ui/forms/index.d.ts +1 -0
  50. package/dist/lib/components/ui/forms/use-code-resolver.d.ts +42 -0
  51. package/dist/lib/components/ui/layout/auth-card/auth-card-notice.d.ts +18 -0
  52. package/dist/lib/components/ui/layout/auth-card/auth-card.d.ts +35 -0
  53. package/dist/lib/components/ui/layout/auth-card/index.d.ts +1 -0
  54. package/dist/lib/components/ui/layout/index.d.ts +1 -0
  55. package/dist/lib/hooks/index.d.ts +2 -0
  56. package/dist/lib/hooks/use-focus-on-mount.d.ts +24 -0
  57. package/dist/lib/hooks/use-submit-once.d.ts +32 -0
  58. package/dist/lib/i18n/resources/en/en.d.ts +21 -0
  59. package/dist/lib/i18n/resources/en/material.json.d.ts +21 -0
  60. package/dist/lib/i18n/resources/fr/fr.d.ts +21 -0
  61. package/dist/lib/i18n/resources/fr/material.json.d.ts +21 -0
  62. package/dist/lib/i18n/resources/resources.d.ts +42 -0
  63. package/dist/lib/i18n/translator.d.ts +1 -1
  64. package/dist/lib/lib/index.d.ts +1 -0
  65. package/dist/lib.js +58 -102
  66. package/dist/main.js +16 -16
  67. package/dist/map.js +22 -22
  68. package/dist/material.css +1 -1
  69. package/dist/{merge-CZIEHTTe.js → merge-Js8537dg.js} +4 -4
  70. package/dist/{mermaid.core-Cp75fWA9.js → mermaid.core-DW7UlwJu.js} +21 -21
  71. package/dist/{mindmap-definition-NLK3R4M7-CFYIlEfW.js → mindmap-definition-NLK3R4M7-CsLyIyEF.js} +2 -2
  72. package/dist/{pieDiagram-5QR66LMP-CjvtBYDr.js → pieDiagram-5QR66LMP-C-dfffj5.js} +1 -1
  73. package/dist/print.js +2 -2
  74. package/dist/{quadrantDiagram-O4NWA36T-DPj8ndqA.js → quadrantDiagram-O4NWA36T-DZ0TivLD.js} +1 -1
  75. package/dist/{requirementDiagram-PLB6GJNP-DjVFwRJg.js → requirementDiagram-PLB6GJNP-3brF-h1L.js} +2 -2
  76. package/dist/{sankeyDiagram-IPEJSGJF-63wsgHUY.js → sankeyDiagram-IPEJSGJF-BY-LLyiZ.js} +1 -1
  77. package/dist/{sortable-provider-Chbn1x1i.js → sortable-provider-CUT5Wfdl.js} +1 -1
  78. package/dist/{stateDiagram-v2-GCMORJYK-DNc8m371.js → stateDiagram-v2-GCMORJYK-BGavr7Nm.js} +2 -2
  79. package/dist/{stored-language-DPqzxRmf.js → stored-language-BKoohv2_.js} +42 -0
  80. package/dist/{swimlanes-2SLR337P-B7_gOJxx.js → swimlanes-2SLR337P-B6Fkr7-U.js} +3 -3
  81. package/dist/{swimlanesDiagram-TC7HE7FX-tqlBrGuu.js → swimlanesDiagram-TC7HE7FX-Dt6Tqj4h.js} +3 -3
  82. package/dist/{timeline-definition-EJHVYXUP-DIA9CBZK.js → timeline-definition-EJHVYXUP-CE-3KwYd.js} +1 -1
  83. package/dist/{translate-material-CqB-j9WD.js → translate-material-CCJg3yfc.js} +2 -2
  84. package/dist/{translator-DTl9h__E.js → translator-DEde7BKz.js} +1 -1
  85. package/dist/{use-heading-spy-C1KA3PrF.js → use-heading-spy-XkSExwoT.js} +67 -59
  86. package/dist/{use-material-translation-DMa4Xrnd.js → use-material-translation-Cndoq_iK.js} +2 -2
  87. package/dist/{usecaseDiagram-POWQR4AR-FMjCkoxs.js → usecaseDiagram-POWQR4AR-CPOCe5We.js} +2 -2
  88. package/dist/{xychartDiagram-PMCCYNJV-B76HOkCh.js → xychartDiagram-PMCCYNJV-Cleaihn8.js} +1 -1
  89. package/docs/components/README.md +3 -3
  90. package/docs/components/data-display.md +101 -0
  91. package/docs/components/form-fields.md +78 -0
  92. package/docs/components/navigation-and-layout.md +51 -0
  93. package/docs/guides/forms.md +111 -0
  94. package/docs/guides/helpers.md +81 -0
  95. package/docs/guides/i18n.md +17 -0
  96. package/docs/guides/printing.md +27 -0
  97. package/docs/previews/data-display-enrolmentsecret.webp +0 -0
  98. package/docs/previews/data-display-recoverycodes.webp +0 -0
  99. package/docs/previews/form-fields-formsection.webp +0 -0
  100. package/docs/previews/navigation-layout-authcard.webp +0 -0
  101. package/docs/roadmap.md +15 -0
  102. package/docs/troubleshooting.md +72 -0
  103. package/lib/components/components.css +1 -0
  104. package/lib/components/print-only.css +44 -0
  105. package/package.json +2 -2
  106. package/dist/async-slice-CsE3hkCD.js +0 -411
@@ -238,6 +238,38 @@ export function SubscribeForm({
238
238
  - Default values must have the type the field emits. See
239
239
  [What each field emits](#what-each-field-emits).
240
240
 
241
+ ## A long form in sections
242
+
243
+ A long form reads better in titled sections. Lay it out in the form grid
244
+ (`.form-grid-layout`, its fields in `[data-slot="form-fields"]`, its buttons
245
+ in `[data-slot="form-actions"]`) and put each group of fields in a
246
+ [`FormSection`](../components/form-fields.md#formsection):
247
+
248
+ ```tsx
249
+ <form className="form-grid-layout" onSubmit={form.handleSubmit(onSave)}>
250
+ <div data-slot="form-fields">
251
+ <FormSection title="Profile">
252
+ <TextFormField control={form.control} name="firstName" label="First name" />
253
+ <TextFormField control={form.control} name="lastName" label="Last name" />
254
+ </FormSection>
255
+ <FormSection title="Notifications" description="You can change these at any time.">
256
+ <SwitchFormField control={form.control} name="digest" label="Digest" placeholder="Email me a weekly digest" />
257
+ </FormSection>
258
+ </div>
259
+ <div data-slot="form-actions">
260
+ <Button type="submit" loading={form.formState.isSubmitting}>Save</Button>
261
+ </div>
262
+ </form>
263
+ ```
264
+
265
+ - Each section's title is an `h2` (pass `titleAs="h3"` when the form sits
266
+ under an `h2` of your own), so a screen reader moves from section to
267
+ section, and each section is a group named by its title.
268
+ - The sections take a whole row of the grid and lay their fields out in the
269
+ grid's columns, one column on a phone; switches, textareas, chips and file
270
+ fields still take a row of their own. A rule separates each section from the
271
+ one before it (`separated={false}` drops it).
272
+
241
273
  ## Validation messages
242
274
 
243
275
  `zodLocaleError()` from `@nxgt/material/lib` is a Zod v4 error map that
@@ -302,6 +334,85 @@ How it picks the language, and what that implies:
302
334
  `This value must be a number.`); a type with no string of its own says
303
335
  `This value has the wrong type.`
304
336
 
337
+ ### Wording errors by code
338
+
339
+ `zodLocaleError` words zod's own issues; a rule of your own — a name already
340
+ taken, a code the API sends back — has words only your app knows.
341
+ `useCodeResolver(schema, options?)` (`@nxgt/material/components`) is a
342
+ react-hook-form resolver that words both in the page's language: a
343
+ refinement carrying a code in `params.code` reads as your translator's words
344
+ for that code, every other issue as `zodLocaleError`'s. The language is
345
+ `I18nProvider`'s (or the `language` option); the resolver is made once per
346
+ schema and language.
347
+
348
+ ```tsx
349
+ // i18n.ts — the app's translator, over the package's bundle
350
+ import { createTranslator, mergeResources, resources } from '@nxgt/material/i18n';
351
+
352
+ export const appResources = mergeResources(resources, {
353
+ en: { errors: { taken: 'This name is already taken.', tooShort: 'At least {min} characters.' } },
354
+ fr: { errors: { taken: 'Ce nom est déjà pris.', tooShort: 'Au moins {min} caractères.' } },
355
+ });
356
+ export const translate = createTranslator<string>(appResources);
357
+ ```
358
+
359
+ ```tsx
360
+ // sign-up-form.tsx
361
+ import { useForm } from 'react-hook-form';
362
+ import { z } from 'zod';
363
+ import { Button, TextFormField, useCodeResolver } from '@nxgt/material/components';
364
+ import { translate } from './i18n';
365
+
366
+ const TAKEN = ['admin', 'root'];
367
+
368
+ const schema = z.object({
369
+ // Worded by material's zod messages: "Invalid email address."
370
+ email: z.email(),
371
+ username: z
372
+ .string()
373
+ .refine((name) => name.length >= 3, { params: { code: 'errors.tooShort', min: 3 } })
374
+ .refine((name) => !TAKEN.includes(name), { params: { code: 'errors.taken' } }),
375
+ });
376
+
377
+ export function SignUpForm({ save }: { save: (values: z.output<typeof schema>) => Promise<void> }) {
378
+ const resolver = useCodeResolver(schema, { translate });
379
+ const { control, handleSubmit } = useForm({ resolver, defaultValues: { email: '', username: '' } });
380
+ return (
381
+ <form noValidate onSubmit={handleSubmit(save)}>
382
+ <TextFormField control={control} name="email" label="Email" />
383
+ <TextFormField control={control} name="username" label="Username" />
384
+ <Button type="submit">Sign up</Button>
385
+ </form>
386
+ );
387
+ }
388
+ ```
389
+
390
+ - **The codes.** `translate(code, params, language)`: `params` is the
391
+ refinement's whole `params`, so `{ code, min: 3 }` fills `{min}`. A key
392
+ your translator returns verbatim (what `createTranslator` does with a key
393
+ it lacks) has no words: the issue falls back to `zodLocaleError`.
394
+ - **The rest.** The same `translate` is handed to `zodLocaleError`, so your
395
+ `material.zod.*` overrides apply. Without `translate`, every issue is
396
+ worded by the package's own messages.
397
+ - **Another code.** `codeOf` picks the code an issue carries. Its default is
398
+ `defaultCodeOf` (a custom issue's `params.code`); to word every empty
399
+ required field by one code of yours, use `isEmptyRequired` (both, and
400
+ the type `CodeIssue`, are exported beside `useCodeResolver` from
401
+ `@nxgt/material/components`, and from `@nxgt/material-core/code-errors`):
402
+ `codeOf: (issue) => (isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue))`.
403
+ - **Stable options.** Pass `translate` and `codeOf` from module scope: a new
404
+ function each render makes a new resolver.
405
+ - **After a language switch** an error already on screen keeps its words
406
+ until the field is validated again: call `trigger()` when the language
407
+ changes and the form was submitted (see
408
+ [troubleshooting](../troubleshooting.md#errors-keep-their-old-words-after-a-language-switch)).
409
+ - Outside a hook, `createCodeErrorMap({ translate, codeOf?, fallback })` (also
410
+ exported beside `useCodeResolver`) is the error map itself, for
411
+ `schema.safeParse(values, { error })`.
412
+
413
+ Storybook: `Guides/Forms/useCodeResolver`. Vue: the same name in
414
+ `@nxgt/material-vue/components`, returning a `validationSchema` for vee-validate.
415
+
305
416
  ## What each field emits
306
417
 
307
418
  The value a wrapper writes into the form, read from each field's `onChange`
@@ -15,6 +15,7 @@ differences are listed under each table.
15
15
  | `cn(...classes)` | `clsx` + `tailwind-merge`: joins conditional classes, last Tailwind utility wins. |
16
16
  | `clientOnly(Component)` | Wraps a component so it renders nothing on the server and mounts only on the client. See [SSR](ssr.md). |
17
17
  | `getCspNonce(doc?)`, `setCspNonce(nonce)` | The page's Content Security Policy nonce, from `@nxgt/material-core/csp-nonce`: `getCspNonce` reads what `setCspNonce` kept, else the `csp-nonce` meta tag, a `<script nonce>` or `<style nonce>`, else `__webpack_nonce__`; `undefined` on a server, where `setCspNonce` does nothing. See [CSP](csp.md#nonceprovider). |
18
+ | `printOnly(element, name?)`, `PRINT_ATTRIBUTE`, `PRINT_TARGET_ATTRIBUTE` | Opens the print dialog for one element of the page instead of the whole page (the marks are removed once printing is over), from `@nxgt/material-core/print-only`; the `@media print` rules are in the package's `styles.css`. See [printing](printing.md#printing-one-element-of-the-page). |
18
19
  | `renderSlot(slot, context)` | Resolves a `ContentSlot`: a `ReactNode` or a `(context) => ReactNode`. |
19
20
  | `DATE_UTILS` | `format(iso, 'PP', locale?)` (`'-'` when empty), `parseFromTimestring('HH:mm')`, `parseFromISOString`, `formatDateRangeFilter`. |
20
21
  | `formatDateRangeFilter(range)` | Turns a day-picker range into `{ from, to }` ISO strings at start / end of day; `undefined` when empty. |
@@ -131,6 +132,9 @@ Vue: the Redux helpers are Pinia store factories: `defineLocalStore`
131
132
  | `useSearchParam(name)` | One query-string value from the router (`react-router`). |
132
133
  | `useNavigationHandler(to, options?)`, `useNavigationHandlers(routes)` | Stable click handlers that navigate (`react-router`). |
133
134
  | `useAllowedTransitions(status, transitions, configs)` | Status-machine actions (icon, colour, label, dialog copy) allowed from the current status. |
135
+ | `useFocusOnMount<T>(focus = true)` | A ref whose element takes focus once mounted, and again whenever `focus` turns true: the heading of what replaced the content the user acted on. See [below](#usefocusonmount). |
136
+ | `useSubmitOnce(busy?)` | One submit in flight: wraps a submit handler so a double click or a second Enter is dropped until the request ends (the handler's promise, or `busy` back to false). See [below](#usesubmitonce). |
137
+ | `createSubmitGuard()`, `busySettler(guard)` | The framework-free guard behind `useSubmitOnce` (`@nxgt/material-core/submit-guard`): `{ once(run, { settleSync? }), settle(run?), pendingRun() }`, and the function that settles it from a busy flag. |
134
138
 
135
139
  ```tsx
136
140
  import { useState } from 'react';
@@ -168,6 +172,83 @@ the component, or with `useCallback`) when the list is long.
168
172
 
169
173
  Vue: `@nxgt/material-vue/composables`, same names.
170
174
 
175
+ ### `useFocusOnMount`
176
+
177
+ When what the user acted on is replaced — a wizard's next step, an end state
178
+ such as "Message sent" — the button that had focus is gone, and focus falls
179
+ back to the top of the page. Put the ref on the new content's heading: it
180
+ takes focus once mounted, a screen reader reads it, and Tab goes on from
181
+ there. A heading is not focusable by itself: give it `tabIndex={-1}` (a
182
+ button, a link or a field needs nothing).
183
+
184
+ ```tsx
185
+ import { useFocusOnMount } from '@nxgt/material/hooks';
186
+
187
+ export function Sent({ email }: { email: string }) {
188
+ const title = useFocusOnMount<HTMLHeadingElement>();
189
+ return (
190
+ <section aria-labelledby="sent-title">
191
+ <h2 id="sent-title" ref={title} tabIndex={-1}>
192
+ Message sent
193
+ </h2>
194
+ <p>We replied to {email}.</p>
195
+ </section>
196
+ );
197
+ }
198
+ ```
199
+
200
+ `useFocusOnMount(open)` focuses again whenever `open` turns true, for content
201
+ that stays mounted and is only shown. Storybook: `Guides/Hooks/useFocusOnMount`.
202
+
203
+ ### `useSubmitOnce`
204
+
205
+ A disabled or `loading` button only stops clicks once it has rendered, and
206
+ never stops Enter in a field: a fast double click, or Enter twice, posts the
207
+ form twice — and a second request can abort the first mid-call. The hook
208
+ returns a wrapper that lets one call through until the request ends; a
209
+ dropped call's event has its default prevented, so it never falls back to
210
+ the browser's own submission.
211
+
212
+ ```tsx
213
+ import { useForm } from 'react-hook-form';
214
+ import { Button, TextFormField } from '@nxgt/material/components';
215
+ import { useSubmitOnce } from '@nxgt/material/hooks';
216
+
217
+ type Values = { email: string };
218
+
219
+ export function NewsletterForm({ save }: { save: (values: Values) => Promise<void> }) {
220
+ const { control, handleSubmit, formState } = useForm<Values>({ defaultValues: { email: '' } });
221
+ const submitOnce = useSubmitOnce();
222
+ return (
223
+ // handleSubmit returns a promise: the guard opens again when `save` ends.
224
+ <form onSubmit={submitOnce(handleSubmit(save))}>
225
+ <TextFormField control={control} name="email" label="Email" />
226
+ <Button type="submit" loading={formState.isSubmitting}>
227
+ Subscribe
228
+ </Button>
229
+ </form>
230
+ );
231
+ }
232
+ ```
233
+
234
+ **Return the request's promise, or pass `busy`.** The guard opens again:
235
+
236
+ - when the handler's promise ends, fulfilled or rejected (a rejection still
237
+ reaches the caller);
238
+ - with `busy` passed (a router's form submission, followed by its navigation
239
+ state: `useSubmitOnce(navigation.state !== 'idle')`), once it was true and
240
+ is false again. It settles the submit that was on its way when `busy`
241
+ rose, never a newer one, and a handler that returns no promise waits for
242
+ it;
243
+ - without `busy`, as soon as a handler returns something that is not a
244
+ promise: one that returned early (`if (!valid) return;`) or did its work
245
+ synchronously does not lock the form.
246
+
247
+ Outside React, `createSubmitGuard()` from `@nxgt/material-core/submit-guard`
248
+ (also re-exported here) is the same guard: `once(run)` wraps a handler, `settle()` opens it by hand, and
249
+ `busySettler(guard)` returns the function to call with each value of a busy
250
+ flag. Storybook: `Guides/Hooks/useSubmitOnce`.
251
+
171
252
  ## `@nxgt/material/i18n`
172
253
 
173
254
  | Export | What it does |
@@ -299,6 +299,19 @@ fichier.") when it cannot show it, `material.file-preview.download`
299
299
  "Seuls les premiers {size} de ce fichier sont affichés.") under a text cut
300
300
  by the size cap; the text or Markdown it shows scrolls in a region named by
301
301
  `material.file-preview.content` ("Contents of {name}", "Contenu de {name}").
302
+ `EnrolmentSecret` reads `material.enrolment-secret.qr-label` ("QR code to
303
+ scan with your authenticator app"), `open-app` ("Open in your authenticator
304
+ app", "Ouvrir dans votre application d'authentification"), `manual` ("Or
305
+ type this key in the app:"), `secret-label` ("Setup key", "Clé de
306
+ configuration"), `copy` ("Copy the key") and `copied` ("Key copied", "Clé
307
+ copiée"); each has a prop that overrides it. `RecoveryCodes` reads
308
+ `material.recovery-codes.title` and `list` ("Recovery codes", "Codes de
309
+ récupération"), `warning` ("Each code works once. Keep them somewhere safe:
310
+ they are shown only now."), `copy`, `copied` and `copy-failed` ("Copy codes",
311
+ "Codes copied", "Copy failed"), `download`, `print`, `saved` ("I have saved
312
+ these codes", "J'ai enregistré ces codes"), `saved-required` ("Tick the box
313
+ once your codes are safe") and `done`; `title`, `warning` and `labels`
314
+ override them. `AuthCard` has no copy of its own.
302
315
  `material.carousel.roledescription` /
303
316
  `slide` and `material.attributes.buttons.add-option` are there for
304
317
  `@nxgt/material-vue`'s carousel and attribute options, so the bundles hold
@@ -407,6 +420,10 @@ z.config({
407
420
 
408
421
  Examples and per-field overrides are in [forms](forms.md#validation-messages).
409
422
 
423
+ Your own rules' messages — a refinement carrying an error code in
424
+ `params.code` — are worded by your translator, in the provider's language,
425
+ with `useCodeResolver`: see [forms](forms.md#wording-errors-by-code).
426
+
410
427
  ## Server rendering
411
428
 
412
429
  On the server there is no `localStorage`, so without help the server renders
@@ -1397,3 +1397,30 @@ Under pnpm the glob is
1397
1397
  `pdfkitStandardFonts()` is a Vite plugin and does not apply to Next.js.
1398
1398
 
1399
1399
  More on what a server bundle evaluates in [SSR](ssr.md).
1400
+
1401
+ ## Printing one element of the page
1402
+
1403
+ `RecoveryCodes`' "Print" button prints its card and nothing else, with
1404
+ `window.print()`, no inline style and no new window. The same helper prints
1405
+ any element of yours: `printOnly(element, name?)` from
1406
+ `@nxgt/material/lib` marks `<html data-nxgt-print="<name>">` and
1407
+ the element (`data-nxgt-print-target`), opens the print dialog, and removes
1408
+ both once printing is over (on `afterprint`, or as soon as `print()` returns
1409
+ where it blocks). The rules that hide the rest of the page are in the
1410
+ package's `styles.css` (`print-only.css`), under `@media print`: everything but the element,
1411
+ what holds it and what it holds is taken out of the flow, and the element
1412
+ starts at the top left of the first sheet.
1413
+
1414
+ ```tsx
1415
+ import { printOnly } from '@nxgt/material/lib';
1416
+
1417
+ const stop = printOnly(document.getElementById('ticket') as HTMLElement, 'ticket');
1418
+ // When the element unmounts before the dialog closed (a browser whose
1419
+ // print() returns at once and never fires afterprint):
1420
+ stop();
1421
+ ```
1422
+
1423
+ Hide the controls inside the element with Tailwind's `print:hidden`. If the
1424
+ rest of the page still prints, an `!important` print rule of the app's wins
1425
+ over the package's: see
1426
+ [troubleshooting](../troubleshooting.md#recoverycodes-print-button-prints-the-whole-page).
package/docs/roadmap.md CHANGED
@@ -157,6 +157,21 @@ version they shipped in, and planned entries may name the major they target.
157
157
 
158
158
  ## Shipped
159
159
 
160
+ - **Signed-out pages and two-step verification** — `AuthCard`, the frame of a
161
+ sign-in page (one `main`, the card's title its `h1`, a status or alert
162
+ notice, safe-area padding); `EnrolmentSecret`, a TOTP enrolment's QR code,
163
+ app link and key grouped by four; `RecoveryCodes`, one-time codes to copy,
164
+ download or print alone, with an "I have saved these codes" confirmation
165
+ (`saved` / `onSavedChange`) an app's router guard can follow. 2.17.0.
166
+ - **Focus after a change, one submit at a time, errors worded by code** —
167
+ `useFocusOnMount` focuses the heading of what replaced the content the user
168
+ acted on; `useSubmitOnce` drops a double click or a second Enter until the
169
+ request ends; `useCodeResolver` words a zod schema's errors in the page's
170
+ language, a refinement's `params.code` in the app's own words. 2.17.0.
171
+ - **Long forms in titled sections** — `FormSection`: a heading (an `h2`, or
172
+ the level `titleAs` names), a description and actions, its fields in the form
173
+ grid's columns and one column on a phone, a rule between sections, and a
174
+ group a screen reader announces by its title. 2.17.0.
160
175
  - **A strict Content Security Policy** — a page that sends `script-src` with a
161
176
  nonce and no `'unsafe-eval'`, and `style-src-elem` with a nonce and no
162
177
  `'unsafe-inline'`, runs every component with no violation report:
@@ -62,6 +62,7 @@ words in your error.
62
62
 
63
63
  **Printing**
64
64
 
65
+ - [`RecoveryCodes`' Print button prints the whole page](#recoverycodes-print-button-prints-the-whole-page)
65
66
  - [`Cannot find module '#standard-fonts/Helvetica'`](#cannot-find-module-standard-fontshelvetica)
66
67
  - [`still references '#standard-fonts/' after the transform`](#still-references-standard-fonts-after-the-transform)
67
68
  - [`TypeError: (0 , t.createContext) is not a function` in a Next.js route handler](#typeerror-0--tcreatecontext-is-not-a-function-in-a-nextjs-route-handler)
@@ -105,6 +106,8 @@ words in your error.
105
106
  - [A test asserting the old Zod string length text fails](#a-test-asserting-the-old-zod-string-length-text-fails)
106
107
  - [A calendar's weekday row is in English on a French page](#a-calendars-weekday-row-is-in-english-on-a-french-page)
107
108
  - [An accessibility audit reports low contrast on a calendar's weekday row or its days](#an-accessibility-audit-reports-low-contrast-on-a-calendars-weekday-row-or-its-days)
109
+ - [Errors keep their old words after a language switch](#errors-keep-their-old-words-after-a-language-switch)
110
+ - [`useCodeResolver` re-validates or resets the form on every render](#usecoderesolver-re-validates-or-resets-the-form-on-every-render)
108
111
 
109
112
  **Runtime**
110
113
 
@@ -1105,6 +1108,29 @@ dependencies: `--external @nxgt/material --external @nxgt/material-core`.
1105
1108
 
1106
1109
  ## Printing
1107
1110
 
1111
+ ### `RecoveryCodes`' Print button prints the whole page
1112
+
1113
+ **When:** "Print" on `RecoveryCodes` prints the rest of the page with the
1114
+ codes, or the codes away from the top of the sheet.
1115
+ **Why:** the button only marks the page (`<html data-nxgt-print>`) and the
1116
+ card (`data-nxgt-print-target`); hiding the rest is a `@media print` rule in
1117
+ the package's stylesheet (`print-only.css`). A page that does not load
1118
+ `@nxgt/material/styles.css` (or `dist/material.css`) has no such rule, and an app's own
1119
+ print rule that is `!important` (or a `position` on an ancestor set with
1120
+ `!important`) wins over it.
1121
+ **Fix:** load the stylesheet the setup guide names, and keep your print
1122
+ rules off the marked page:
1123
+
1124
+ ```css
1125
+ /* your app's print rules, everywhere but while one element is printed */
1126
+ @media print {
1127
+ html:not([data-nxgt-print]) .page-header { position: static !important; }
1128
+ }
1129
+ ```
1130
+
1131
+ To print an element of your own the same way, call `printOnly(element)`
1132
+ from `@nxgt/material/lib`; the same rules apply to it.
1133
+
1108
1134
  ### `Cannot find module '#standard-fonts/Helvetica'`
1109
1135
 
1110
1136
  **When:** a built SSR server answers nothing on every route, `/health`
@@ -1839,6 +1865,52 @@ react-day-picker hides the weekday row from assistive tech
1839
1865
  (`<thead aria-hidden>`); in a test, find a day by its full date instead
1840
1866
  (`getByRole('gridcell', { name: 'jeudi 1 octobre 2026' })`).
1841
1867
 
1868
+ ### Errors keep their old words after a language switch
1869
+
1870
+ **When:** a form validated with `useCodeResolver` shows an error, the user
1871
+ switches the language, and the error stays in the old one (a code's words
1872
+ or a zod message alike).
1873
+ **Why:** the resolver follows the new language, but react-hook-form keeps
1874
+ the message it computed until the field is validated again.
1875
+ **Fix:** validate again when the language changes, once the form was
1876
+ submitted:
1877
+
1878
+ ```tsx
1879
+ const resolver = useCodeResolver(schema, { translate });
1880
+ const { trigger, formState } = useForm({ resolver });
1881
+ const { isSubmitted } = formState;
1882
+ useEffect(() => {
1883
+ if (isSubmitted) trigger();
1884
+ }, [resolver, isSubmitted, trigger]); // a new resolver is a new language
1885
+ ```
1886
+
1887
+ Storybook: `Guides/Forms/useCodeResolver`. See
1888
+ [forms](guides/forms.md#wording-errors-by-code).
1889
+
1890
+ ### `useCodeResolver` re-validates or resets the form on every render
1891
+
1892
+ **When:** `translate` or `codeOf` is written inline in the component
1893
+ (`useCodeResolver(schema, { codeOf: (issue) => … })`), or the schema is built
1894
+ in the component body.
1895
+ **Why:** the resolver is memoised on the schema, the language, `translate`
1896
+ and `codeOf`: a new function or schema each render makes a new resolver
1897
+ each render.
1898
+ **Fix:** define the schema, the translator and `codeOf` at module scope (or
1899
+ with `useMemo` / `useCallback` when they depend on props):
1900
+
1901
+ ```tsx
1902
+ import { type CodeIssue, defaultCodeOf, isEmptyRequired, useCodeResolver } from '@nxgt/material/components';
1903
+
1904
+ const schema = z.object({ username: z.string().min(3) });
1905
+ const codeOf = (issue: CodeIssue) =>
1906
+ isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue);
1907
+
1908
+ export function SignUpForm() {
1909
+ const resolver = useCodeResolver(schema, { translate, codeOf });
1910
+ // …
1911
+ }
1912
+ ```
1913
+
1842
1914
  ## Runtime
1843
1915
 
1844
1916
  ### A test asserting the old Zod string length text fails
@@ -29,6 +29,7 @@
29
29
  @import "./ui/layout/app-bar/bottom-app-bar.css";
30
30
  @import "./ui/primitives/label/label.css";
31
31
  @import "./ui/feedback/toast/toast.css";
32
+ @import "./print-only.css";
32
33
  @import "./ui/fields/contacts/contact-item.css";
33
34
  @import "./ui/fields/opening-hours/opening-hours-item.css";
34
35
  @import "./ui/fields/attributes/attribute-form/attribute-form.css";
@@ -0,0 +1,44 @@
1
+ /* Printing one element of the page (`printOnly`, from `/lib` or
2
+ * @nxgt/material-core/print-only; RecoveryCodes' Print button): while
3
+ * <html data-nxgt-print> is set, print media shows the element marked
4
+ * data-nxgt-print-target and nothing else. No inline style: only these rules,
5
+ * the same in @nxgt/material and @nxgt/material-vue.
6
+ *
7
+ * Everything that neither is, holds nor sits inside the target is taken out
8
+ * of the flow, so the page prints no blank sheets after it; where :has() is
9
+ * not supported, that rule is dropped and `visibility` alone hides the rest.
10
+ * The target's ancestors stop positioning it, so it starts at the top left of
11
+ * the first page. Unlayered, these rules beat the components' Tailwind
12
+ * utilities (layered) with no `!important`. */
13
+ @media print {
14
+ html[data-nxgt-print]
15
+ body
16
+ *:not(
17
+ [data-nxgt-print-target],
18
+ [data-nxgt-print-target] *,
19
+ :has([data-nxgt-print-target])
20
+ ) {
21
+ display: none;
22
+ }
23
+
24
+ html[data-nxgt-print] body * {
25
+ visibility: hidden;
26
+ }
27
+
28
+ html[data-nxgt-print] :has([data-nxgt-print-target]) {
29
+ position: static;
30
+ transform: none;
31
+ }
32
+
33
+ html[data-nxgt-print] [data-nxgt-print-target],
34
+ html[data-nxgt-print] [data-nxgt-print-target] * {
35
+ visibility: visible;
36
+ }
37
+
38
+ html[data-nxgt-print] [data-nxgt-print-target] {
39
+ position: absolute;
40
+ inset: 0 0 auto 0;
41
+ margin: 0;
42
+ box-shadow: none;
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"
@@ -119,7 +119,7 @@
119
119
  "docs:previews": "vitest run --project previews"
120
120
  },
121
121
  "dependencies": {
122
- "@nxgt/material-core": "^0.9.1",
122
+ "@nxgt/material-core": "^0.10.0",
123
123
  "@base-ui/react": "^1.6.0",
124
124
  "@dnd-kit/core": "^6.3.1",
125
125
  "@dnd-kit/sortable": "^10.0.0",