@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.
- package/CHANGELOG.md +98 -0
- package/README.md +180 -6
- package/dist/chunks/index-15vj9xbx.js +26 -0
- package/dist/chunks/index-15vj9xbx.js.map +10 -0
- package/dist/chunks/index-407b6zs0.js +41 -0
- package/dist/chunks/index-407b6zs0.js.map +10 -0
- package/dist/chunks/{index-h3vjydz7.js → index-r93kpbtg.js} +8 -23
- package/dist/chunks/index-r93kpbtg.js.map +10 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/code-errors.d.ts +75 -0
- package/dist/lib/code-errors.d.ts.map +1 -0
- package/dist/lib/code-errors.js +37 -0
- package/dist/lib/code-errors.js.map +10 -0
- package/dist/lib/focus-control.js +7 -39
- package/dist/lib/focus-control.js.map +3 -4
- package/dist/lib/pdf-download.d.ts +1 -6
- package/dist/lib/pdf-download.d.ts.map +1 -1
- package/dist/lib/pdf-download.js +5 -3
- package/dist/lib/pdf-download.js.map +1 -1
- package/dist/lib/print-only.d.ts +29 -0
- package/dist/lib/print-only.d.ts.map +1 -0
- package/dist/lib/print-only.js +39 -0
- package/dist/lib/print-only.js.map +10 -0
- package/dist/lib/repeatable-group.d.ts +70 -0
- package/dist/lib/repeatable-group.d.ts.map +1 -0
- package/dist/lib/repeatable-group.js +59 -0
- package/dist/lib/repeatable-group.js.map +10 -0
- package/dist/lib/save-blob.d.ts +16 -0
- package/dist/lib/save-blob.d.ts.map +1 -0
- package/dist/lib/submit-guard.d.ts +72 -0
- package/dist/lib/submit-guard.d.ts.map +1 -0
- package/dist/lib/submit-guard.js +69 -0
- package/dist/lib/submit-guard.js.map +10 -0
- package/dist/lib/two-factor.d.ts +37 -0
- package/dist/lib/two-factor.d.ts.map +1 -0
- package/dist/lib/two-factor.js +38 -0
- package/dist/lib/two-factor.js.map +10 -0
- package/docs/roadmap.md +13 -0
- package/package.json +32 -2
- 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
|
|
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`),
|
|
17
|
-
(`/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
95
|
+
export { downloadPdfFile2, printPdfFile2 };
|
|
111
96
|
|
|
112
|
-
//# debugId=
|
|
113
|
-
//# sourceMappingURL=index-
|
|
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-
|
|
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=
|
|
134
|
+
//# debugId=E96D8B40D58DC02064756E2164756E21
|
|
133
135
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -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"}
|