@nxgt/material-core 0.9.1 → 0.11.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 (41) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +180 -6
  3. package/dist/chunks/index-15vj9xbx.js +26 -0
  4. package/dist/chunks/index-15vj9xbx.js.map +10 -0
  5. package/dist/chunks/index-407b6zs0.js +41 -0
  6. package/dist/chunks/index-407b6zs0.js.map +10 -0
  7. package/dist/chunks/{index-h3vjydz7.js → index-r93kpbtg.js} +8 -23
  8. package/dist/chunks/index-r93kpbtg.js.map +10 -0
  9. package/dist/index.js +5 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/code-errors.d.ts +75 -0
  12. package/dist/lib/code-errors.d.ts.map +1 -0
  13. package/dist/lib/code-errors.js +37 -0
  14. package/dist/lib/code-errors.js.map +10 -0
  15. package/dist/lib/focus-control.js +7 -39
  16. package/dist/lib/focus-control.js.map +3 -4
  17. package/dist/lib/pdf-download.d.ts +1 -6
  18. package/dist/lib/pdf-download.d.ts.map +1 -1
  19. package/dist/lib/pdf-download.js +5 -3
  20. package/dist/lib/pdf-download.js.map +1 -1
  21. package/dist/lib/print-only.d.ts +29 -0
  22. package/dist/lib/print-only.d.ts.map +1 -0
  23. package/dist/lib/print-only.js +39 -0
  24. package/dist/lib/print-only.js.map +10 -0
  25. package/dist/lib/repeatable-group.d.ts +70 -0
  26. package/dist/lib/repeatable-group.d.ts.map +1 -0
  27. package/dist/lib/repeatable-group.js +59 -0
  28. package/dist/lib/repeatable-group.js.map +10 -0
  29. package/dist/lib/save-blob.d.ts +16 -0
  30. package/dist/lib/save-blob.d.ts.map +1 -0
  31. package/dist/lib/submit-guard.d.ts +72 -0
  32. package/dist/lib/submit-guard.d.ts.map +1 -0
  33. package/dist/lib/submit-guard.js +69 -0
  34. package/dist/lib/submit-guard.js.map +10 -0
  35. package/dist/lib/two-factor.d.ts +37 -0
  36. package/dist/lib/two-factor.d.ts.map +1 -0
  37. package/dist/lib/two-factor.js +38 -0
  38. package/dist/lib/two-factor.js.map +10 -0
  39. package/docs/roadmap.md +13 -0
  40. package/package.json +32 -2
  41. package/dist/chunks/index-h3vjydz7.js.map +0 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,103 @@
1
1
  # @nxgt/material-core
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#574](https://github.com/softistx/nxgt-material/pull/574) [`c00f8c3`](https://github.com/softistx/nxgt-material/commit/c00f8c366e35b7769560bb86b964ae0705bcc60f) Thanks [@SteveGT96](https://github.com/SteveGT96)! - Add `RepeatableGroup` and `RepeatableGroupFormField` (Storybook
8
+ `Form fields/RepeatableGroup`): rows of fields a user adds, removes and, with
9
+ `reorderable`, moves up and down. `RepeatableGroupFormField` wraps the form
10
+ library's `useFieldArray` (react-hook-form in React, vee-validate in Vue) and
11
+ renders each row from `renderRow` (React) or the `row` slot (Vue), whose
12
+ `path` names the row's fields. The rows are a list (`ul` / `li`) in a group
13
+ named by `label`; each row's buttons are named after it ("Remove period 2",
14
+ "Move period 2 up"), from the new `material.repeatable-group.*` keys in
15
+ English and French. An added row's first field takes the focus, a removed row
16
+ hands it to its neighbour or to Add, and a moved row keeps it on its move
17
+ button; each change is announced politely. `max` makes Add `aria-disabled`,
18
+ described by "Maximum reached (n)"; `min` disables Remove; `emptyTitle` /
19
+ `emptyDescription` show an `EmptyState` with no row. The array's own error
20
+ shows under the rows and describes the group; each row's fields show their
21
+ own. One column on a phone, a responsive grid wider up, no inline style.
22
+
23
+ `@nxgt/material-core` gains `/repeatable-group`: the focus rules
24
+ (`focusAfterRemove`, `focusAfterMove`, `focusRowTarget`), the limits
25
+ (`canAddRow`, `canRemoveRow`, `rowMoveTarget`) and the `RowAnnouncement`
26
+ descriptor both libraries use.
27
+
28
+ ```tsx
29
+ <RepeatableGroupFormField
30
+ control={control}
31
+ name="periods"
32
+ label="Periods"
33
+ rowLabel="period"
34
+ max={6}
35
+ reorderable
36
+ newRow={() => ({ name: '' })}
37
+ renderRow={({ index }) => (
38
+ <TextFormField control={control} name={`periods.${index}.name`} label="Name" />
39
+ )}
40
+ />
41
+ ```
42
+
43
+ ```vue
44
+ <RepeatableGroupFormField
45
+ name="periods"
46
+ label="Periods"
47
+ row-label="period"
48
+ :max="6"
49
+ reorderable
50
+ :new-row="() => ({ name: '' })"
51
+ >
52
+ <template #row="{ path }">
53
+ <TextFormField :name="`${path}.name`" label="Name" />
54
+ </template>
55
+ </RepeatableGroupFormField>
56
+ ```
57
+
58
+ Fixes [#516](https://github.com/softistx/nxgt-material/issues/516).
59
+
60
+ Migration: an app with a local field-array list marked temporary for this
61
+ request ([#516](https://github.com/softistx/nxgt-material/issues/516)) — a `useFieldArray` whose rows are mapped by hand, with its own
62
+ Add and per-row Remove buttons — can delete it and render
63
+ `RepeatableGroupFormField` instead: the array's `name`, `newRow` for what Add
64
+ appends, and the row's fields moved into `renderRow` / the `row` slot. Pass no
65
+ `shouldFocus` to `append`: the group moves the focus itself.
66
+
67
+ ## 0.10.0
68
+
69
+ ### Minor Changes
70
+
71
+ - [#535](https://github.com/softistx/nxgt-material/pull/535) [`a088c1c`](https://github.com/softistx/nxgt-material/commit/a088c1c8f08f8c4483fe0ba0e35eedfbe1b193f9) Thanks [@SteveGT96](https://github.com/SteveGT96)! - Three form and focus helpers, the same names in both libraries (softistx/nxgt-material#500). Fixes [#500](https://github.com/softistx/nxgt-material/issues/500).
72
+
73
+ - **`useFocusOnMount<T>(focus?)`** (`@nxgt/material/hooks`, `@nxgt/material-vue/composables`, auto-imported by the Nuxt module): the element takes focus once mounted, and again whenever `focus` turns true. Put it on the heading of what replaced the content the user acted on (a wizard's next step, "Message sent"), with `tabIndex={-1}` / `tabindex="-1"`. React returns a `RefObject`, Vue a `ShallowRef` to bind with `ref="…"`.
74
+ - **`useSubmitOnce(busy?)`** (same entries): one submit in flight. `const submitOnce = useSubmitOnce(); <form onSubmit={submitOnce(handleSubmit(save))}>` (Vue: `const onSubmit = submitOnce(handleSubmit(save))`) drops a double click or a second Enter until `save` ends; a dropped call's event has its default prevented. Return the request's promise, or pass its busy flag (Vue: a ref or getter): the guard then opens once the flag was true and is false again, for the submit that was on its way when it rose. Without `busy`, a handler that returns no promise (an early `return`) opens the guard at once. The framework-free `createSubmitGuard()` and `busySettler(guard)` are re-exported by both from `@nxgt/material-core/submit-guard`.
75
+ - **`useCodeResolver(schema, { translate, codeOf?, language? })`** (`@nxgt/material/components`, `@nxgt/material-vue/components`): a zod 4 schema's errors worded in the page's language. A refinement carrying `params.code` (`.refine(isFree, { params: { code: 'errors.taken' } })`) reads as your translator's words for that code, every other issue as `zodLocaleError`'s, with your `material.zod.*` overrides. `createCodeErrorMap`, `defaultCodeOf`, `isEmptyRequired` and the type `CodeIssue` are exported beside it. React returns a react-hook-form `Resolver` (`useForm({ resolver })`); Vue a computed vee-validate `TypedSchema` (`useForm({ validationSchema })`), so a language switch re-words the errors on screen.
76
+ - `@nxgt/material-core` adds `/submit-guard` (`createSubmitGuard` with `once(run, { settleSync })`, a run-scoped `settle(run)` and `pendingRun()`, and `busySettler`) and `/code-errors` (`createCodeErrorMap`, `defaultCodeOf`, `isEmptyRequired`): the error map is typed by shape, with no zod dependency.
77
+
78
+ An app with a local copy marked `To extract to material (request [#500](https://github.com/softistx/nxgt-material/issues/500))` — `useFocusOnMount`, `createSubmitGuard` / `useSubmitOnce`, `useCodeResolver` and the code error map behind it — can delete it and import the same names from `@nxgt/material` / `@nxgt/material-vue` (`createSubmitGuard` and `createCodeErrorMap` from `@nxgt/material-core/submit-guard` and `/code-errors`). A local `useSubmitOnce(busy)` keeps its call; a local code error map becomes `useCodeResolver(schema, { translate, codeOf })`, with `codeOf: (issue) => (isEmptyRequired(issue) ? '<your required code>' : defaultCodeOf(issue))` where empty required fields had a code of their own.
79
+
80
+ - [#544](https://github.com/softistx/nxgt-material/pull/544) [`e071e15`](https://github.com/softistx/nxgt-material/commit/e071e15a1d3b31c7e8fe7ad83d5a206687d18d9f) Thanks [@SteveGT96](https://github.com/SteveGT96)! - Signed-out pages and two-step verification: `AuthCard`, `EnrolmentSecret` and `RecoveryCodes`, the same names in both libraries (`@nxgt/material/components`, `@nxgt/material-vue/components`, registered by the Nuxt module). Fixes [#498](https://github.com/softistx/nxgt-material/issues/498).
81
+
82
+ - **`AuthCard`** (Storybook `Navigation & layout/AuthCard`): the frame of a sign-in, sign-up or reset page. One `<main tabIndex={-1}>` (focusable by script, never by Tab), a centred card whose `title` is the page's `h1`, a `description`, a `notice` above the form (`noticeTone` `'info'` | `'success'` | `'warning'` are `role="status"`, `'error'` is `role="alert"`), `focusTitle` to move the focus to the `h1` on an end state ("Check your inbox"), a row above the card for `brand` and `toolbar` (rendered only when one is given), a `banner` above it and a `footer` under the card. Padded with `max(1rem, env(safe-area-inset-*))` by classes, no inline style. Vue: the same props, and slots `title`, `description`, `notice`, `brand`, `toolbar`, `banner`, `footer`.
83
+ - **`EnrolmentSecret`** (`Data display/EnrolmentSecret`): a TOTP enrolment — the `otpauth://` URI's QR code (the library's `QRCode`, level M, named "QR code to scan with your authenticator app"), the URI as a link ("Open in your authenticator app": a phone cannot scan its own screen) and the secret in groups of four in a group named "Setup key", with a `CopyButton` that copies it without spaces and announces "Key copied". `uri`, `secret`, `showQrCode` (default `true`), `qrSize` (default `192`) and a prop per label. Side by side once the component is 28rem wide, stacked on a phone, the key wrapping inside the card.
84
+ - **`RecoveryCodes`** (`Data display/RecoveryCodes`): one-time codes shown once, in a card. An ordered list named "Recovery codes"; "Copy codes" (one per line, announced "Codes copied" / "Copy failed" by the component's own polite status region, no `Toaster` needed); "Download" (a text file: `fileHeading`, `fileNote`, a blank line, the codes; `fileName` defaults to `recovery-codes.txt`); "Print" (the card alone, through the new print rules in `styles.css`); and "I have saved these codes", which "Done" requires: pressed before, it moves the focus to the box, marks it `aria-invalid` and says "Tick the box once your codes are safe". `title` / `titleAs` (`h2` by default), `description`, `warning` (`false` hides it), `focusTitle` (default `true`), `onDone` (the "Done" button shows only with it; Vue: `@done`), `saved` / `onSavedChange` (Vue: `v-model:saved`) and `labels`.
85
+
86
+ ```tsx
87
+ <AuthCard title="Two-step verification" brand={<Logo />} toolbar={<LanguageSwitcher />}>
88
+ <EnrolmentSecret uri={enrolment.uri} secret={enrolment.secret} />
89
+ </AuthCard>
90
+ <RecoveryCodes codes={codes} fileHeading={`Recovery codes for ${email}`} saved={saved} onSavedChange={setSaved} onDone={done} />
91
+ ```
92
+
93
+ ```vue
94
+ <RecoveryCodes v-model:saved="saved" :codes="codes" :file-heading="`Recovery codes for ${email}`" @done="done" />
95
+ ```
96
+
97
+ `groupSecret`, `recoveryCodesText` and `downloadText` are re-exported beside them. `@nxgt/material-core` adds `/two-factor`: `groupSecret(secret, size = 4)`, `recoveryCodesText(codes, { heading?, note? })` and `downloadText(text, fileName, { type? })` (an object URL revoked a minute later, as `/pdf-download` does: both now share one helper). It also adds `/print-only`: `printOnly(element, name?)` prints one element of the page through `data-nxgt-print` on `<html>` and `data-nxgt-print-target` on the element, removed on `afterprint` or once a blocking `print()` returns; the rules are the new `print-only.css` in each library's `styles.css`, and both libraries re-export `printOnly`, `PRINT_ATTRIBUTE` and `PRINT_TARGET_ATTRIBUTE` from `/lib`. `CopyButton` and `RecoveryCodes` share the copy-and-announce state; `CopyButton` behaves as before. In `@nxgt/material-vue`, `Checkbox` now shows a `#helper-text` (or `#label`) slot that is added after the first render.
98
+
99
+ Migration: local copies marked `To extract to material (request [#498](https://github.com/softistx/nxgt-material/issues/498))` can be deleted. A local sign-in frame becomes `AuthCard` (its subtitle is `description`; brand, language switcher and banner go in `brand`, `toolbar` and `banner`). A local enrolment block becomes `EnrolmentSecret uri secret`, and a local grouping helper `groupSecret`. A local recovery-codes display becomes `RecoveryCodes`, a local file builder `recoveryCodesText(codes, { heading, note })`, and a local print-only rule `printOnly`; the app keeps its wording (`title`, `description`, `fileHeading`, `fileNote`, `labels`), its data loading and its navigation guard while `saved` is false.
100
+
3
101
  ## 0.9.1
4
102
 
5
103
  ### Patch Changes
package/README.md CHANGED
@@ -8,16 +8,21 @@ half (page geometry, scroll spy, text search), the currency field's decimals,
8
8
  the file-size formatter, the slider's tick marks, the emoji list's groups,
9
9
  the date range presets, a debounce, accent-insensitive text matching and a race-free async search (`/debounce`, `/text-match`, `/async-search`), the file preview's kind detection, the fields' blur
10
10
  rule, a field's first reachable control (`/focus-control`) and its `aria-describedby`
11
- (`/described-by`), the page behind a modal layer made inert
11
+ (`/described-by`), where the focus goes in a repeatable row group
12
+ (`/repeatable-group`), the page behind a modal layer made inert
12
13
  (`/inert-others`), Escape pressed while an overlay closes
13
14
  (`/escape-forward`), the Toaster's guard against a tap's emulated hover
14
15
  (`/touch-hover`), the focus a toast's action hands back (`/toast-focus`), a
15
16
  table row's activation, the zod error map's issue messages
16
- (`/zod-issue-messages`), the page's Content Security Policy nonce
17
- (`/csp-nonce`), an object built on its first use (`/lazy-object`), the Vite
18
- plugin that bundles pdfkit's standard fonts and a `build.assetsInlineLimit`
19
- that keeps fonts out of `data:` URIs, and the Node-only resolution of those
20
- fonts that the Next.js helper and the Nuxt module run at build time.
17
+ (`/zod-issue-messages`), a zod error map that words an issue by its code
18
+ (`/code-errors`), the page's Content Security Policy nonce (`/csp-nonce`), an
19
+ object built on its first use (`/lazy-object`), one submit in flight
20
+ (`/submit-guard`), the two-step verification helpers (`/two-factor`: a TOTP
21
+ secret grouped to be typed, the recovery codes' text file, a text download),
22
+ printing one element of the page (`/print-only`), the Vite plugin that
23
+ bundles pdfkit's standard fonts and a `build.assetsInlineLimit` that keeps
24
+ fonts out of `data:` URIs, and the Node-only resolution of those fonts that
25
+ the Next.js helper and the Nuxt module run at build time.
21
26
 
22
27
  **You usually do not install it yourself.** Both libraries depend on it and
23
28
  re-export what they always exported, at the same subpaths:
@@ -84,6 +89,7 @@ built-in.
84
89
  | `/currency-digits` | `currencyFractionDigits` — the decimals `CurrencyField` shows |
85
90
  | `/field-blur-layers` | `layerOf`, `openerOf`, `FieldLayers` — whose overlay a layer of the page is, for a field's blur |
86
91
  | `/focus-control` | `firstControl`, `focusHandle`, `FocusHandle` — the first control of a field a user can reach, and a stand-in element that focuses it for a form library |
92
+ | `/repeatable-group` | `focusAfterRemove`, `focusAfterMove`, `focusRowTarget`, `rowMoveTarget`, `canAddRow`, `canRemoveRow`, `RowFocusTarget`, `RowMoveDirection`, `RowAnnouncement`, `ROW_SLOT`, `ROW_FIELDS_SLOT`, `ADD_ATTRIBUTE` — where the focus goes after a row of a `RepeatableGroup` is added, removed or moved, and what its live region says |
87
93
  | `/escape-forward` | `forwardEscape` — Escape pressed while an overlay closes, pressed again for the overlay below once it has gone |
88
94
  | `/described-by` | `describedBy` — an `aria-describedby` value from the caller's ids and a field's help and error ids |
89
95
  | `/inert-others` | `inertOthers`, `INERT_MARKER` — the rest of the page `inert` behind a modal layer, live regions kept, nested layers counted |
@@ -100,6 +106,10 @@ built-in.
100
106
  | `/debounce` | `debounce`, the type `Debounced` — a function called once calls pause, with `cancel` and `flush` (`SearchField`'s `debounceMs`) |
101
107
  | `/text-match` | `foldText`, `matchesText` — case- and accent-insensitive matching (`'e'` finds "École"), lowercased for a locale |
102
108
  | `/async-search` | `createAsyncSearch`, `searchAnnouncement`, `DEFAULT_SEARCH_DEBOUNCE_MS`, `DEFAULT_MIN_QUERY_LENGTH` and the types `AsyncSearch`, `AsyncSearchConfig`, `AsyncSearchState`, `AsyncSearchStatus`, `SearchFn`, `SearchContext`, `SearchAnnouncement` — `Autocomplete`'s server search: debounced, each run aborting the last, a stale response dropped |
109
+ | `/submit-guard` | `createSubmitGuard`, `busySettler` and the types `SubmitGuard`, `SubmitOnceOptions` — one submit in flight until its request ends, behind both libraries' `useSubmitOnce` |
110
+ | `/code-errors` | `createCodeErrorMap`, `defaultCodeOf`, `isEmptyRequired` and the types `CodeErrorMap`, `CodeErrorMapOptions`, `CodeIssue` — a zod 4 error map that words an issue by the code it carries (a refinement's `params.code`), behind both libraries' `useCodeResolver` |
111
+ | `/two-factor` | `groupSecret`, `recoveryCodesText`, `downloadText` and the types `RecoveryCodesTextOptions`, `DownloadTextOptions` — a TOTP secret in groups of four, the recovery codes as a text file, and saving a text file, behind both libraries' `EnrolmentSecret` and `RecoveryCodes` |
112
+ | `/print-only` | `printOnly`, `PRINT_ATTRIBUTE`, `PRINT_TARGET_ATTRIBUTE` — printing one element of the page with no inline style, behind `RecoveryCodes`' Print button; re-exported by both libraries' `/lib` |
103
113
  | `/file-preview` | `fileKind`, `describeFile`, `safePreviewUrl`, `readTextPreview`, `FilePreviewHttpError`, `FilePreviewUnsafeUrlError`, `TEXT_PREVIEW_MAX_BYTES`, `fileExtension`, `fileNameFromUrl`, `dataUrlType`, `kindOfMimeType`, `normaliseMimeType`, `isTextFileKind`, `TEXT_FILE_KINDS` and the types `FileKind`, `FileKindInput`, `DescribedFile`, `FilePreviewSource`, `TextPreview` — `FilePreview`'s kind detection, URL allow-list and text reader |
104
114
  | `/vite` | `pdfkitStandardFonts()`, `cspAssetsInlineLimit` — see below |
105
115
  | `/pdfkit-fonts` | **Node only.** `pdfkitStandardFontDirs`, `pdfkitStandardFontFiles`, `PDFKIT_STANDARD_FONTS`, the types `PdfkitStandardFont` and `PdfkitFontsWarn` — where the app's pdfkit keeps its standard fonts, for a server tracer |
@@ -457,6 +467,132 @@ what a combobox's polite live region says: the failure, else "loading",
457
467
  else the count once the query has settled — `null` while the user is still
458
468
  typing.
459
469
 
470
+ ### One submit in flight: `/submit-guard`
471
+
472
+ `createSubmitGuard()` returns `{ once, settle, pendingRun }`. `once(run)` wraps a
473
+ handler so it runs only when no submit is on its way: a second click, or
474
+ Enter, before the first request ends is dropped (its event, if it carries
475
+ one, has its default prevented, so the browser does not submit the form on
476
+ its own). `settle()` opens the guard again; a `run` that returns a promise
477
+ opens it when that promise ends, fulfilled or rejected, and a `run` that
478
+ throws opens it and rethrows. Any other result keeps it closed until
479
+ `settle()` — unless `once(run, { settleSync: true })`, which opens it at
480
+ once. So: **return the request's promise, or follow the request with a busy
481
+ flag.** The wrapper returns `run`'s result — its promise as one that settles
482
+ the same way, so a rejection still reaches the caller.
483
+
484
+ `pendingRun()` is the run on its way (an increasing number, `undefined` when
485
+ open), and `settle(run)` opens the guard only while that run is still the one
486
+ on its way. `busySettler(guard)` uses them: call the function it returns
487
+ with each value of a busy flag, and once the flag was true and is false
488
+ again it settles the run that was on its way when the flag rose — never a
489
+ newer one.
490
+
491
+ ```ts
492
+ import { createSubmitGuard } from '@nxgt/material-core/submit-guard';
493
+
494
+ const guard = createSubmitGuard();
495
+ const submit = guard.once((event: SubmitEvent) => {
496
+ event.preventDefault();
497
+ return fetch('/api/subscribe', { method: 'POST', body: new FormData(form) });
498
+ });
499
+ form.addEventListener('submit', submit); // a second submit before the reply is dropped
500
+ ```
501
+
502
+ Both libraries wrap it as `useSubmitOnce(busy?)`: `busy` is a boolean in
503
+ React (`useSubmitOnce(navigation.state !== 'idle')`) and a ref or a getter in
504
+ Vue (`useSubmitOnce(() => pending.value)`), followed through `busySettler`.
505
+ Without `busy`, the hooks pass `settleSync: true`, so a handler that returns
506
+ early does not lock the form.
507
+
508
+ ### Errors worded by code: `/code-errors`
509
+
510
+ `createCodeErrorMap({ translate, codeOf?, fallback })` is a zod 4 error map
511
+ (typed by shape: this package does not depend on zod). An issue's code —
512
+ by default `defaultCodeOf`: a custom issue's `params.code`, when it is a
513
+ string — is worded by `translate(code, params)`; when there is no code, or
514
+ `translate` returns `undefined` or the code itself (a translator's answer for
515
+ a key it lacks), `fallback(issue)` words it. `isEmptyRequired(issue)` tells
516
+ an empty required field (a string shorter than one character, or a missing
517
+ value), to give it a code of its own.
518
+
519
+ ```ts
520
+ import { z } from 'zod';
521
+ import {
522
+ createCodeErrorMap,
523
+ defaultCodeOf,
524
+ isEmptyRequired,
525
+ } from '@nxgt/material-core/code-errors';
526
+
527
+ const words: Record<string, string> = {
528
+ 'errors.taken': 'This name is already taken.',
529
+ 'errors.required': 'Please fill this in.',
530
+ };
531
+ const error = createCodeErrorMap({
532
+ translate: (code) => words[code],
533
+ codeOf: (issue) => (isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue)),
534
+ fallback: () => undefined, // zod's own message
535
+ });
536
+
537
+ const schema = z.object({
538
+ username: z.string().min(1).refine((name) => name !== 'admin', { params: { code: 'errors.taken' } }),
539
+ });
540
+ schema.safeParse({ username: 'admin' }, { error }); // "This name is already taken."
541
+ ```
542
+
543
+ Both libraries wrap it as `useCodeResolver(schema)`, with their
544
+ `zodLocaleError` as the fallback and the page's language.
545
+
546
+ ### Two-step verification: `/two-factor`
547
+
548
+ `groupSecret(secret, size = 4)` reads a TOTP secret in groups, as a person
549
+ types it into an authenticator app (`"JBSWY3DPEHPK3PXP"` → `"JBSW Y3DP EHPK
550
+ 3PXP"`); whitespace already in it is dropped first. `recoveryCodesText(codes,
551
+ { heading?, note? })` is the text file a person keeps: the heading, the note,
552
+ a blank line, one code per line and a trailing newline (a line not given is
553
+ left out). `downloadText(text, fileName, { type? })` saves it through an
554
+ object URL and a temporary `<a download>`, revoked a minute later,
555
+ as `/pdf-download` does (its `OBJECT_URL_TTL_MS`).
556
+
557
+ ```ts
558
+ import {
559
+ downloadText,
560
+ groupSecret,
561
+ recoveryCodesText,
562
+ } from '@nxgt/material-core/two-factor';
563
+
564
+ groupSecret('JBSWY3DPEHPK3PXP'); // "JBSW Y3DP EHPK 3PXP"
565
+ const text = recoveryCodesText(['7KQ2-M9XD', 'P4TW-8HZC'], {
566
+ heading: 'Recovery codes for ada@example.com',
567
+ note: 'Each code works once.',
568
+ });
569
+ downloadText(text, 'recovery-codes.txt');
570
+ ```
571
+
572
+ Both libraries' `EnrolmentSecret` and `RecoveryCodes` are built on them.
573
+
574
+ ### Printing one element: `/print-only`
575
+
576
+ `printOnly(element, name?)` prints that element and nothing else of the
577
+ page: it sets `data-nxgt-print="<name>"` on `<html>` and
578
+ `data-nxgt-print-target` on the element, calls `window.print()`, and removes
579
+ both on `afterprint`, or as soon as `print()` returns where it blocks until
580
+ the dialog closes. It returns a function that removes them at once; call it
581
+ when the element unmounts. No inline style and no new window: the hiding is
582
+ the libraries' stylesheet (`print-only.css`, in their `styles.css`),
583
+ which under print media and `html[data-nxgt-print]` shows only the target.
584
+
585
+ ```ts
586
+ import { printOnly } from '@nxgt/material-core/print-only';
587
+
588
+ const stop = printOnly(document.querySelector('#codes') as HTMLElement, 'recovery-codes');
589
+ // when the element unmounts:
590
+ stop();
591
+ ```
592
+
593
+ Both libraries re-export it from `/lib` (`@nxgt/material/lib`,
594
+ `@nxgt/material-vue/lib`); `RecoveryCodes`' Print button uses it.
595
+
460
596
  ### The emoji list's groups: `/emoji-groups`
461
597
 
462
598
  How both libraries' `EmojiList` groups, searches and names its emojis:
@@ -677,6 +813,44 @@ firstControl(roots)?.focus(); // the first date input, not a hidden one
677
813
  field.ref(focusHandle(roots)); // React Hook Form: shouldFocusError reaches it
678
814
  ```
679
815
 
816
+ ### Rows added, removed and moved: `/repeatable-group`
817
+
818
+ What both libraries' `RepeatableGroup` decides, as plain functions.
819
+ `focusAfterRemove(removed, remaining)` names where the focus goes once a row
820
+ is gone: the row that took its place, else the one before, else the Add
821
+ button. `focusAfterMove(to, direction, count)` keeps it on the move button
822
+ that was pressed, or on the row's other one when the row reached either end.
823
+ `focusRowTarget(root, target)` moves the focus there, reading the DOM when
824
+ called (rows are `[data-slot="repeatable-group-row"]`, the move buttons carry
825
+ `data-move`, the Add button `data-repeatable-group-add`). `canAddRow(count, max)`, `canRemoveRow(count, min)` and
826
+ `rowMoveTarget(index, direction, count)` are the limits (`RowMoveDirection`
827
+ is `'up' | 'down'`), and `RowAnnouncement` is what the live region says
828
+ (1-based positions). The DOM markers are exported as `ROW_SLOT`
829
+ (`'repeatable-group-row'`), `ROW_FIELDS_SLOT` (`'repeatable-group-fields'`)
830
+ and `ADD_ATTRIBUTE` (`'data-repeatable-group-add'`).
831
+
832
+ ```ts
833
+ import {
834
+ canAddRow,
835
+ canRemoveRow,
836
+ focusAfterRemove,
837
+ focusRowTarget,
838
+ type RowAnnouncement,
839
+ rowMoveTarget,
840
+ } from '@nxgt/material-core/repeatable-group';
841
+
842
+ const addable = canAddRow(rows.length, 6); // false at 6 rows
843
+ const removable = canRemoveRow(rows.length, 1); // false at 1 row
844
+ const up = rowMoveTarget(index, 'up', rows.length); // null for the first row
845
+
846
+ remove(index);
847
+ await nextTick(); // or after React commits
848
+ focusRowTarget(groupElement, focusAfterRemove(index, rows.length));
849
+
850
+ const said: RowAnnouncement = { kind: 'removed', index: index + 1 };
851
+ status.textContent = said.kind === 'removed' ? `Removed period ${said.index}` : '';
852
+ ```
853
+
680
854
  ### Escape while an overlay closes: `/escape-forward`
681
855
 
682
856
  Radix's and reka's dismissable layers give Escape to the highest layer only,
@@ -0,0 +1,26 @@
1
+ // src/lib/save-blob.ts
2
+ var OBJECT_URL_TTL_MS2 = 60000;
3
+ function revokeLater(url) {
4
+ window.setTimeout(() => URL.revokeObjectURL(url), OBJECT_URL_TTL_MS2);
5
+ }
6
+ function clickLink(href, attributes) {
7
+ const link = document.createElement("a");
8
+ link.href = href;
9
+ for (const [name, value] of Object.entries(attributes)) {
10
+ link.setAttribute(name, value);
11
+ }
12
+ link.style.display = "none";
13
+ document.body.append(link);
14
+ link.click();
15
+ link.remove();
16
+ }
17
+ function saveBlob(blob, fileName) {
18
+ const url = URL.createObjectURL(blob);
19
+ clickLink(url, { download: fileName });
20
+ revokeLater(url);
21
+ }
22
+
23
+ export { OBJECT_URL_TTL_MS2, revokeLater, clickLink, saveBlob };
24
+
25
+ //# debugId=3BAD8691A74B531364756E2164756E21
26
+ //# sourceMappingURL=index-15vj9xbx.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/save-blob.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Saving a Blob as a file through a temporary `<a download>`: what\n * `/pdf-download` and `/two-factor`'s `downloadText` share. Reads the DOM\n * only when called.\n */\n\n/**\n * How long an object URL outlives the click that started its download or\n * opened its tab. Revoking it synchronously cancels the download in Firefox\n * and Safari, which read the URL after `click()` returns.\n */\nexport const OBJECT_URL_TTL_MS = 60_000;\n\nexport function revokeLater(url: string) {\n\twindow.setTimeout(() => URL.revokeObjectURL(url), OBJECT_URL_TTL_MS);\n}\n\nexport function clickLink(href: string, attributes: Record<string, string>) {\n\tconst link = document.createElement('a');\n\tlink.href = href;\n\tfor (const [name, value] of Object.entries(attributes)) {\n\t\tlink.setAttribute(name, value);\n\t}\n\tlink.style.display = 'none';\n\t// Firefox ignores a click on a link that is not in the document.\n\tdocument.body.append(link);\n\tlink.click();\n\tlink.remove();\n}\n\n/** Saves a Blob under `fileName`, and revokes its URL once the save began. */\nexport function saveBlob(blob: Blob, fileName: string) {\n\tconst url = URL.createObjectURL(blob);\n\tclickLink(url, { download: fileName });\n\trevokeLater(url);\n}\n"
6
+ ],
7
+ "mappings": ";AAWO,IAAM,qBAAoB;AAE1B,SAAS,WAAW,CAAC,KAAa;AAAA,EACxC,OAAO,WAAW,MAAM,IAAI,gBAAgB,GAAG,GAAG,kBAAiB;AAAA;AAG7D,SAAS,SAAS,CAAC,MAAc,YAAoC;AAAA,EAC3E,MAAM,OAAO,SAAS,cAAc,GAAG;AAAA,EACvC,KAAK,OAAO;AAAA,EACZ,YAAY,MAAM,UAAU,OAAO,QAAQ,UAAU,GAAG;AAAA,IACvD,KAAK,aAAa,MAAM,KAAK;AAAA,EAC9B;AAAA,EACA,KAAK,MAAM,UAAU;AAAA,EAErB,SAAS,KAAK,OAAO,IAAI;AAAA,EACzB,KAAK,MAAM;AAAA,EACX,KAAK,OAAO;AAAA;AAIN,SAAS,QAAQ,CAAC,MAAY,UAAkB;AAAA,EACtD,MAAM,MAAM,IAAI,gBAAgB,IAAI;AAAA,EACpC,UAAU,KAAK,EAAE,UAAU,SAAS,CAAC;AAAA,EACrC,YAAY,GAAG;AAAA;",
8
+ "debugId": "3BAD8691A74B531364756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,41 @@
1
+ // src/lib/focus-control.ts
2
+ var TABBABLE = 'input:not([type="hidden"]):not(:disabled):not([tabindex="-1"]), textarea:not(:disabled):not([tabindex="-1"]), select:not(:disabled):not([tabindex="-1"]), button:not(:disabled):not([tabindex="-1"]), [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
3
+ function firstControl2(roots) {
4
+ for (const root of roots) {
5
+ const controls = root.matches(TABBABLE) ? [root] : root.querySelectorAll(TABBABLE);
6
+ for (const control of controls) {
7
+ if (control.getClientRects().length)
8
+ return control;
9
+ }
10
+ }
11
+ return null;
12
+ }
13
+ function focusHandle2(roots) {
14
+ return {
15
+ focus: () => {
16
+ firstControl2(roots)?.focus();
17
+ setTimeout(() => {
18
+ const list = [...roots];
19
+ const active = list[0]?.ownerDocument.activeElement ?? null;
20
+ if (!list.some((root) => root.contains(active))) {
21
+ firstControl2(list)?.focus();
22
+ }
23
+ });
24
+ },
25
+ select: () => {
26
+ const control = firstControl2(roots);
27
+ if (control && "select" in control) {
28
+ control.select();
29
+ }
30
+ },
31
+ setCustomValidity: () => {
32
+ return;
33
+ },
34
+ reportValidity: () => true
35
+ };
36
+ }
37
+
38
+ export { firstControl2, focusHandle2 };
39
+
40
+ //# debugId=084F2A6FA5DCD68964756E2164756E21
41
+ //# sourceMappingURL=index-407b6zs0.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/focus-control.ts"],
4
+ "sourcesContent": [
5
+ "/** What a user can Tab to inside a field. */\nconst TABBABLE =\n\t'input:not([type=\"hidden\"]):not(:disabled):not([tabindex=\"-1\"]), textarea:not(:disabled):not([tabindex=\"-1\"]), select:not(:disabled):not([tabindex=\"-1\"]), button:not(:disabled):not([tabindex=\"-1\"]), [contenteditable=\"true\"], [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The first control of `roots` a user can reach, in document order: one a\n * user can Tab to that is rendered — not a `display: none` input a library\n * keeps for forms. A root that is itself such a control is its own first\n * control. `null` when none is. Reads the DOM when called.\n */\nexport function firstControl(roots: Iterable<Element>): HTMLElement | null {\n\tfor (const root of roots) {\n\t\tconst controls = root.matches(TABBABLE)\n\t\t\t? [root as HTMLElement]\n\t\t\t: root.querySelectorAll<HTMLElement>(TABBABLE);\n\t\tfor (const control of controls) {\n\t\t\tif (control.getClientRects().length) return control;\n\t\t}\n\t}\n\treturn null;\n}\n\n/** What a form library calls on the element a field registered. */\nexport type FocusHandle = {\n\tfocus: () => void;\n\tselect: () => void;\n\tsetCustomValidity: (message: string) => void;\n\treportValidity: () => boolean;\n};\n\n/**\n * A stand-in element for a field made of several controls, for a form\n * library that focuses an invalid field by calling `focus()` on what the\n * field registered (React Hook Form's `field.ref`). `focus()` focuses the\n * first control of `roots` a user can reach, looked up when it is called —\n * `roots` is read live, so pass the field's own set — and again a turn\n * later when focus is not in the field by then: the error that triggered it\n * may re-render the field and replace its control. `select()` selects that\n * control's text when it has any. Native validation is not used:\n * `setCustomValidity` does nothing and `reportValidity` returns `true`.\n */\nexport function focusHandle(roots: Iterable<Element>): FocusHandle {\n\treturn {\n\t\tfocus: () => {\n\t\t\tfirstControl(roots)?.focus();\n\t\t\tsetTimeout(() => {\n\t\t\t\tconst list = [...roots];\n\t\t\t\tconst active = list[0]?.ownerDocument.activeElement ?? null;\n\t\t\t\tif (!list.some((root) => root.contains(active))) {\n\t\t\t\t\tfirstControl(list)?.focus();\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tselect: () => {\n\t\t\tconst control = firstControl(roots);\n\t\t\tif (control && 'select' in control) {\n\t\t\t\t(control as HTMLInputElement).select();\n\t\t\t}\n\t\t},\n\t\tsetCustomValidity: () => undefined,\n\t\treportValidity: () => true,\n\t};\n}\n"
6
+ ],
7
+ "mappings": ";AACA,IAAM,WACL;AAQM,SAAS,aAAY,CAAC,OAA8C;AAAA,EAC1E,WAAW,QAAQ,OAAO;AAAA,IACzB,MAAM,WAAW,KAAK,QAAQ,QAAQ,IACnC,CAAC,IAAmB,IACpB,KAAK,iBAA8B,QAAQ;AAAA,IAC9C,WAAW,WAAW,UAAU;AAAA,MAC/B,IAAI,QAAQ,eAAe,EAAE;AAAA,QAAQ,OAAO;AAAA,IAC7C;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAsBD,SAAS,YAAW,CAAC,OAAuC;AAAA,EAClE,OAAO;AAAA,IACN,OAAO,MAAM;AAAA,MACZ,cAAa,KAAK,GAAG,MAAM;AAAA,MAC3B,WAAW,MAAM;AAAA,QAChB,MAAM,OAAO,CAAC,GAAG,KAAK;AAAA,QACtB,MAAM,SAAS,KAAK,IAAI,cAAc,iBAAiB;AAAA,QACvD,IAAI,CAAC,KAAK,KAAK,CAAC,SAAS,KAAK,SAAS,MAAM,CAAC,GAAG;AAAA,UAChD,cAAa,IAAI,GAAG,MAAM;AAAA,QAC3B;AAAA,OACA;AAAA;AAAA,IAEF,QAAQ,MAAM;AAAA,MACb,MAAM,UAAU,cAAa,KAAK;AAAA,MAClC,IAAI,WAAW,YAAY,SAAS;AAAA,QAClC,QAA6B,OAAO;AAAA,MACtC;AAAA;AAAA,IAED,mBAAmB,MAAG;AAAA,MAAG;AAAA;AAAA,IACzB,gBAAgB,MAAM;AAAA,EACvB;AAAA;",
8
+ "debugId": "084F2A6FA5DCD68964756E2164756E21",
9
+ "names": []
10
+ }
@@ -7,9 +7,13 @@ import {
7
7
  import {
8
8
  resolvePdfFileName2
9
9
  } from "./index-paaaedrm.js";
10
+ import {
11
+ revokeLater,
12
+ clickLink,
13
+ saveBlob
14
+ } from "./index-15vj9xbx.js";
10
15
 
11
16
  // src/lib/pdf-download.ts
12
- var OBJECT_URL_TTL_MS2 = 60000;
13
17
  function fetchInit(source) {
14
18
  if (!isPdfUrlSource2(source))
15
19
  return;
@@ -39,25 +43,6 @@ async function resolvePdfBlob(source, getData) {
39
43
  return null;
40
44
  }
41
45
  }
42
- function revokeLater(url) {
43
- window.setTimeout(() => URL.revokeObjectURL(url), OBJECT_URL_TTL_MS2);
44
- }
45
- function clickLink(href, attributes) {
46
- const link = document.createElement("a");
47
- link.href = href;
48
- for (const [name, value] of Object.entries(attributes)) {
49
- link.setAttribute(name, value);
50
- }
51
- link.style.display = "none";
52
- document.body.append(link);
53
- link.click();
54
- link.remove();
55
- }
56
- function saveBlob(blob, fileName) {
57
- const url = URL.createObjectURL(blob);
58
- clickLink(url, { download: fileName });
59
- revokeLater(url);
60
- }
61
46
  function openInNewTab(href) {
62
47
  clickLink(href, { target: "_blank", rel: "noopener" });
63
48
  }
@@ -107,7 +92,7 @@ async function printPdfFile2(source, options = {}) {
107
92
  document.body.append(frame);
108
93
  }
109
94
 
110
- export { OBJECT_URL_TTL_MS2, downloadPdfFile2, printPdfFile2 };
95
+ export { downloadPdfFile2, printPdfFile2 };
111
96
 
112
- //# debugId=77B6E1C9EF4463B464756E2164756E21
113
- //# sourceMappingURL=index-h3vjydz7.js.map
97
+ //# debugId=3B08A2D665AEF3C464756E2164756E21
98
+ //# sourceMappingURL=index-r93kpbtg.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/pdf-download.ts"],
4
+ "sourcesContent": [
5
+ "import { resolvePdfFileName } from './pdf-file-name';\nimport {\n\tcopyPdfData,\n\tisBlob,\n\tisPdfUrlSource,\n\ttype PdfSource,\n\tpdfSourceUrl,\n} from './pdf-source';\nimport { clickLink, revokeLater, saveBlob } from './save-blob';\n\nexport { OBJECT_URL_TTL_MS } from './save-blob';\n\nexport type PdfFileOptions = {\n\t/** Name of the saved file; `.pdf` is added when missing. */\n\tfileName?: string;\n\t/**\n\t * Name used when neither `fileName`, a `File`'s name nor the URL gives\n\t * one: `document`, or its translation.\n\t */\n\tdefaultName?: string;\n\t/**\n\t * Bytes of the document the reader already loaded. Saving them needs no\n\t * second request, and works for a cross-origin URL, where the `download`\n\t * attribute is ignored.\n\t */\n\tgetData?: () =>\n\t\t| Uint8Array\n\t\t| null\n\t\t| undefined\n\t\t| Promise<Uint8Array | null | undefined>;\n};\n\nfunction fetchInit(source: PdfSource): RequestInit | undefined {\n\tif (!isPdfUrlSource(source)) return undefined;\n\treturn {\n\t\theaders: source.httpHeaders,\n\t\tcredentials: source.withCredentials ? 'include' : 'same-origin',\n\t};\n}\n\nasync function resolvePdfBlob(\n\tsource: PdfSource,\n\tgetData: PdfFileOptions['getData'],\n): Promise<Blob | null> {\n\tconst url = pdfSourceUrl(source);\n\tif (url === null) {\n\t\t// A Blob or bytes: a Blob copies what it is given.\n\t\treturn isBlob(source)\n\t\t\t? source\n\t\t\t: new Blob([copyPdfData(source as ArrayBuffer | Uint8Array)], {\n\t\t\t\t\ttype: 'application/pdf',\n\t\t\t\t});\n\t}\n\ttry {\n\t\tconst bytes = await getData?.();\n\t\tif (bytes?.byteLength) {\n\t\t\treturn new Blob([copyPdfData(bytes)], { type: 'application/pdf' });\n\t\t}\n\t} catch {\n\t\t/* the document may be gone; fetch it instead */\n\t}\n\ttry {\n\t\tconst init = fetchInit(source);\n\t\tconst response = await (init ? fetch(url, init) : fetch(url));\n\t\treturn response.ok ? await response.blob() : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction openInNewTab(href: string) {\n\tclickLink(href, { target: '_blank', rel: 'noopener' });\n}\n\n/**\n * The URL a new tab may open when the document could not be read. None for\n * a URL with headers: a tab sends no `Authorization`, so it would only show\n * the server's refusal.\n */\nfunction fallbackUrl(source: PdfSource) {\n\tif (isPdfUrlSource(source) && source.httpHeaders) return null;\n\treturn pdfSourceUrl(source);\n}\n\n/**\n * Downloads the PDF under its resolved name. A URL is saved from the loaded\n * bytes or fetched; when neither works (CORS), it opens in a new tab, which\n * a popup blocker may stop once the click's activation has expired. A URL\n * with headers never opens in a tab: it would load without them.\n */\nexport async function downloadPdfFile(\n\tsource: PdfSource,\n\toptions: PdfFileOptions = {},\n) {\n\tconst name = resolvePdfFileName(\n\t\tsource,\n\t\toptions.fileName,\n\t\toptions.defaultName,\n\t);\n\tconst blob = await resolvePdfBlob(source, options.getData);\n\tconst url = fallbackUrl(source);\n\tif (blob) saveBlob(blob, name);\n\telse if (url !== null) openInNewTab(url);\n}\n\n/**\n * Prints the PDF from a same-origin object URL in a hidden frame. When the\n * browser refuses (a PDF plugin that blocks `print()`, a cross-origin URL\n * that could not be fetched), the document opens in a new tab instead.\n */\nexport async function printPdfFile(\n\tsource: PdfSource,\n\toptions: Pick<PdfFileOptions, 'getData'> = {},\n) {\n\tconst blob = await resolvePdfBlob(source, options.getData);\n\tif (!blob) {\n\t\tconst url = fallbackUrl(source);\n\t\tif (url !== null) openInNewTab(url);\n\t\treturn;\n\t}\n\tconst url = URL.createObjectURL(blob);\n\tconst frame = document.createElement('iframe');\n\tframe.setAttribute('aria-hidden', 'true');\n\tframe.style.position = 'fixed';\n\tframe.style.right = '0';\n\tframe.style.bottom = '0';\n\tframe.style.width = '0';\n\tframe.style.height = '0';\n\tframe.style.border = '0';\n\tframe.src = url;\n\tframe.onload = () => {\n\t\ttry {\n\t\t\tframe.contentWindow?.focus();\n\t\t\tframe.contentWindow?.print();\n\t\t} catch {\n\t\t\topenInNewTab(url);\n\t\t} finally {\n\t\t\twindow.setTimeout(() => frame.remove(), 1000);\n\t\t\trevokeLater(url);\n\t\t}\n\t};\n\tdocument.body.append(frame);\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;AAgCA,SAAS,SAAS,CAAC,QAA4C;AAAA,EAC9D,IAAI,CAAC,gBAAe,MAAM;AAAA,IAAG;AAAA,EAC7B,OAAO;AAAA,IACN,SAAS,OAAO;AAAA,IAChB,aAAa,OAAO,kBAAkB,YAAY;AAAA,EACnD;AAAA;AAGD,eAAe,cAAc,CAC5B,QACA,SACuB;AAAA,EACvB,MAAM,MAAM,cAAa,MAAM;AAAA,EAC/B,IAAI,QAAQ,MAAM;AAAA,IAEjB,OAAO,QAAO,MAAM,IACjB,SACA,IAAI,KAAK,CAAC,aAAY,MAAkC,CAAC,GAAG;AAAA,MAC5D,MAAM;AAAA,IACP,CAAC;AAAA,EACJ;AAAA,EACA,IAAI;AAAA,IACH,MAAM,QAAQ,MAAM,UAAU;AAAA,IAC9B,IAAI,OAAO,YAAY;AAAA,MACtB,OAAO,IAAI,KAAK,CAAC,aAAY,KAAK,CAAC,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAAA,IAClE;AAAA,IACC,MAAM;AAAA,EAGR,IAAI;AAAA,IACH,MAAM,OAAO,UAAU,MAAM;AAAA,IAC7B,MAAM,WAAW,OAAO,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,GAAG;AAAA,IAC3D,OAAO,SAAS,KAAK,MAAM,SAAS,KAAK,IAAI;AAAA,IAC5C,MAAM;AAAA,IACP,OAAO;AAAA;AAAA;AAIT,SAAS,YAAY,CAAC,MAAc;AAAA,EACnC,UAAU,MAAM,EAAE,QAAQ,UAAU,KAAK,WAAW,CAAC;AAAA;AAQtD,SAAS,WAAW,CAAC,QAAmB;AAAA,EACvC,IAAI,gBAAe,MAAM,KAAK,OAAO;AAAA,IAAa,OAAO;AAAA,EACzD,OAAO,cAAa,MAAM;AAAA;AAS3B,eAAsB,gBAAe,CACpC,QACA,UAA0B,CAAC,GAC1B;AAAA,EACD,MAAM,OAAO,oBACZ,QACA,QAAQ,UACR,QAAQ,WACT;AAAA,EACA,MAAM,OAAO,MAAM,eAAe,QAAQ,QAAQ,OAAO;AAAA,EACzD,MAAM,MAAM,YAAY,MAAM;AAAA,EAC9B,IAAI;AAAA,IAAM,SAAS,MAAM,IAAI;AAAA,EACxB,SAAI,QAAQ;AAAA,IAAM,aAAa,GAAG;AAAA;AAQxC,eAAsB,aAAY,CACjC,QACA,UAA2C,CAAC,GAC3C;AAAA,EACD,MAAM,OAAO,MAAM,eAAe,QAAQ,QAAQ,OAAO;AAAA,EACzD,IAAI,CAAC,MAAM;AAAA,IACV,MAAM,MAAM,YAAY,MAAM;AAAA,IAC9B,IAAI,QAAQ;AAAA,MAAM,aAAa,GAAG;AAAA,IAClC;AAAA,EACD;AAAA,EACA,MAAM,MAAM,IAAI,gBAAgB,IAAI;AAAA,EACpC,MAAM,QAAQ,SAAS,cAAc,QAAQ;AAAA,EAC7C,MAAM,aAAa,eAAe,MAAM;AAAA,EACxC,MAAM,MAAM,WAAW;AAAA,EACvB,MAAM,MAAM,QAAQ;AAAA,EACpB,MAAM,MAAM,SAAS;AAAA,EACrB,MAAM,MAAM,QAAQ;AAAA,EACpB,MAAM,MAAM,SAAS;AAAA,EACrB,MAAM,MAAM,SAAS;AAAA,EACrB,MAAM,MAAM;AAAA,EACZ,MAAM,SAAS,MAAM;AAAA,IACpB,IAAI;AAAA,MACH,MAAM,eAAe,MAAM;AAAA,MAC3B,MAAM,eAAe,MAAM;AAAA,MAC1B,MAAM;AAAA,MACP,aAAa,GAAG;AAAA,cACf;AAAA,MACD,OAAO,WAAW,MAAM,MAAM,OAAO,GAAG,IAAI;AAAA,MAC5C,YAAY,GAAG;AAAA;AAAA;AAAA,EAGjB,SAAS,KAAK,OAAO,KAAK;AAAA;",
8
+ "debugId": "3B08A2D665AEF3C464756E2164756E21",
9
+ "names": []
10
+ }
package/dist/index.js CHANGED
@@ -12,10 +12,9 @@ import {
12
12
  resolvePdfFileName2
13
13
  } from "./chunks/index-paaaedrm.js";
14
14
  import {
15
- OBJECT_URL_TTL_MS2,
16
15
  downloadPdfFile2,
17
16
  printPdfFile2
18
- } from "./chunks/index-h3vjydz7.js";
17
+ } from "./chunks/index-r93kpbtg.js";
19
18
  import {
20
19
  MAX_FRACTION_DIGITS2,
21
20
  fractionDigitsOption2
@@ -44,6 +43,9 @@ import {
44
43
  pdfBytesResponse2,
45
44
  toWebStream2
46
45
  } from "./chunks/index-2gcgcmc0.js";
46
+ import {
47
+ OBJECT_URL_TTL_MS2
48
+ } from "./chunks/index-15vj9xbx.js";
47
49
  import {
48
50
  text2,
49
51
  heading2,
@@ -129,5 +131,5 @@ export {
129
131
  totals2 as totals
130
132
  };
131
133
 
132
- //# debugId=1DC0C9D608FB064D64756E2164756E21
134
+ //# debugId=E96D8B40D58DC02064756E2164756E21
133
135
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -4,6 +4,6 @@
4
4
  "sourcesContent": [
5
5
  ],
6
6
  "mappings": "",
7
- "debugId": "1DC0C9D608FB064D64756E2164756E21",
7
+ "debugId": "E96D8B40D58DC02064756E2164756E21",
8
8
  "names": []
9
9
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The part of a zod issue a code error map reads, by shape: core has no zod
3
+ * dependency, and every issue zod 4 hands an error map is assignable to it.
4
+ */
5
+ export type CodeIssue = {
6
+ readonly code: string;
7
+ /** A custom issue's params: `.refine(…, { params: { code } })`. */
8
+ readonly params?: Record<string, unknown> | undefined;
9
+ /** The value that failed. */
10
+ readonly input?: unknown;
11
+ /** What a size issue measured: `'string'`, `'number'`, `'array'`… */
12
+ readonly origin?: string | undefined;
13
+ readonly minimum?: unknown;
14
+ readonly maximum?: unknown;
15
+ readonly expected?: unknown;
16
+ readonly message?: string | undefined;
17
+ };
18
+ /** The words for an issue, or `undefined` to let zod word it. */
19
+ export type CodeErrorMap = (issue: CodeIssue) => string | undefined;
20
+ export type CodeErrorMapOptions = {
21
+ /**
22
+ * The app's words for `code`, interpolating `params` (the custom issue's
23
+ * params, so `{ code, min: 3 }` can read "At least 3"). `undefined`, or
24
+ * the code returned verbatim (what a translator does with a key it lacks),
25
+ * means there are no words for it: `fallback` words the issue instead.
26
+ */
27
+ translate: (code: string, params: Record<string, unknown> | undefined) => string | undefined;
28
+ /**
29
+ * Which code an issue carries. Defaults to {@link defaultCodeOf}: a custom
30
+ * issue's `params.code`, when it is a string.
31
+ */
32
+ codeOf?: (issue: CodeIssue) => string | undefined;
33
+ /**
34
+ * Words an issue with no code, or one whose code has no words: the
35
+ * libraries pass `zodLocaleError({ language, translate })`.
36
+ */
37
+ fallback: CodeErrorMap;
38
+ };
39
+ /** A custom issue's `params.code`, when it is a string; otherwise none. */
40
+ export declare function defaultCodeOf(issue: CodeIssue): string | undefined;
41
+ /**
42
+ * Whether an issue only says the field was left empty: a string shorter than
43
+ * one character (`z.string().min(1)`), or a value of the wrong type that is
44
+ * not there at all (`undefined` or `null`).
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * createCodeErrorMap({
49
+ * translate,
50
+ * fallback,
51
+ * codeOf: (issue) =>
52
+ * isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue),
53
+ * });
54
+ * ```
55
+ */
56
+ export declare function isEmptyRequired(issue: CodeIssue): boolean;
57
+ /**
58
+ * A zod 4 error map that words an issue by the code it carries, in the
59
+ * app's words: a refinement's `params.code` (an API error code, say) reads
60
+ * as `translate(code, params)`. An issue with no code, or whose code
61
+ * `translate` has no words for, is worded by `fallback`.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { createCodeErrorMap } from '@nxgt/material-core/code-errors';
66
+ *
67
+ * const error = createCodeErrorMap({
68
+ * translate: (code, params) => translate(code, params, language),
69
+ * fallback: zodLocaleError({ language, translate }),
70
+ * });
71
+ * schema.safeParse(values, { error });
72
+ * ```
73
+ */
74
+ export declare function createCodeErrorMap({ translate, codeOf, fallback, }: CodeErrorMapOptions): CodeErrorMap;
75
+ //# sourceMappingURL=code-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-errors.d.ts","sourceRoot":"","sources":["../../src/lib/code-errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACtD,6BAA6B;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC,CAAC;AAEF,iEAAiE;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,MAAM,GAAG,SAAS,CAAC;AAEpE,MAAM,MAAM,mBAAmB,GAAG;IACjC;;;;;OAKG;IACH,SAAS,EAAE,CACV,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KACvC,MAAM,GAAG,SAAS,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,MAAM,GAAG,SAAS,CAAC;IAClD;;;OAGG;IACH,QAAQ,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAIlE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAMzD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,EAClC,SAAS,EACT,MAAsB,EACtB,QAAQ,GACR,EAAE,mBAAmB,GAAG,YAAY,CASpC"}