@nxgt/material-core 0.9.0 → 0.10.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 +41 -0
- package/README.md +139 -5
- package/dist/chunks/index-15vj9xbx.js +26 -0
- package/dist/chunks/index-15vj9xbx.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/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/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 +42 -9
- package/package.json +26 -2
- package/dist/chunks/index-h3vjydz7.js.map +0 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @nxgt/material-core
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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).
|
|
8
|
+
|
|
9
|
+
- **`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="…"`.
|
|
10
|
+
- **`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`.
|
|
11
|
+
- **`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.
|
|
12
|
+
- `@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.
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
- [#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).
|
|
17
|
+
|
|
18
|
+
- **`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`.
|
|
19
|
+
- **`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.
|
|
20
|
+
- **`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`.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<AuthCard title="Two-step verification" brand={<Logo />} toolbar={<LanguageSwitcher />}>
|
|
24
|
+
<EnrolmentSecret uri={enrolment.uri} secret={enrolment.secret} />
|
|
25
|
+
</AuthCard>
|
|
26
|
+
<RecoveryCodes codes={codes} fileHeading={`Recovery codes for ${email}`} saved={saved} onSavedChange={setSaved} onDone={done} />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```vue
|
|
30
|
+
<RecoveryCodes v-model:saved="saved" :codes="codes" :file-heading="`Recovery codes for ${email}`" @done="done" />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`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.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
## 0.9.1
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- [#541](https://github.com/softistx/nxgt-material/pull/541) [`6d10133`](https://github.com/softistx/nxgt-material/commit/6d1013356e8c42631166b6281081811912e31323) Thanks [@SteveGT96](https://github.com/SteveGT96)! - The roadmaps plan one Vite plugin for the setup, and list the strict Content
|
|
42
|
+
Security Policy as shipped.
|
|
43
|
+
|
|
3
44
|
## 0.9.0
|
|
4
45
|
|
|
5
46
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -13,11 +13,15 @@ rule, a field's first reachable control (`/focus-control`) and its `aria-describ
|
|
|
13
13
|
(`/escape-forward`), the Toaster's guard against a tap's emulated hover
|
|
14
14
|
(`/touch-hover`), the focus a toast's action hands back (`/toast-focus`), a
|
|
15
15
|
table row's activation, the zod error map's issue messages
|
|
16
|
-
(`/zod-issue-messages`),
|
|
17
|
-
(`/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
(`/zod-issue-messages`), a zod error map that words an issue by its code
|
|
17
|
+
(`/code-errors`), the page's Content Security Policy nonce (`/csp-nonce`), an
|
|
18
|
+
object built on its first use (`/lazy-object`), one submit in flight
|
|
19
|
+
(`/submit-guard`), the two-step verification helpers (`/two-factor`: a TOTP
|
|
20
|
+
secret grouped to be typed, the recovery codes' text file, a text download),
|
|
21
|
+
printing one element of the page (`/print-only`), the Vite plugin that
|
|
22
|
+
bundles pdfkit's standard fonts and a `build.assetsInlineLimit` that keeps
|
|
23
|
+
fonts out of `data:` URIs, and the Node-only resolution of those fonts that
|
|
24
|
+
the Next.js helper and the Nuxt module run at build time.
|
|
21
25
|
|
|
22
26
|
**You usually do not install it yourself.** Both libraries depend on it and
|
|
23
27
|
re-export what they always exported, at the same subpaths:
|
|
@@ -100,6 +104,10 @@ built-in.
|
|
|
100
104
|
| `/debounce` | `debounce`, the type `Debounced` — a function called once calls pause, with `cancel` and `flush` (`SearchField`'s `debounceMs`) |
|
|
101
105
|
| `/text-match` | `foldText`, `matchesText` — case- and accent-insensitive matching (`'e'` finds "École"), lowercased for a locale |
|
|
102
106
|
| `/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 |
|
|
107
|
+
| `/submit-guard` | `createSubmitGuard`, `busySettler` and the types `SubmitGuard`, `SubmitOnceOptions` — one submit in flight until its request ends, behind both libraries' `useSubmitOnce` |
|
|
108
|
+
| `/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` |
|
|
109
|
+
| `/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` |
|
|
110
|
+
| `/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
111
|
| `/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
112
|
| `/vite` | `pdfkitStandardFonts()`, `cspAssetsInlineLimit` — see below |
|
|
105
113
|
| `/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 +465,132 @@ what a combobox's polite live region says: the failure, else "loading",
|
|
|
457
465
|
else the count once the query has settled — `null` while the user is still
|
|
458
466
|
typing.
|
|
459
467
|
|
|
468
|
+
### One submit in flight: `/submit-guard`
|
|
469
|
+
|
|
470
|
+
`createSubmitGuard()` returns `{ once, settle, pendingRun }`. `once(run)` wraps a
|
|
471
|
+
handler so it runs only when no submit is on its way: a second click, or
|
|
472
|
+
Enter, before the first request ends is dropped (its event, if it carries
|
|
473
|
+
one, has its default prevented, so the browser does not submit the form on
|
|
474
|
+
its own). `settle()` opens the guard again; a `run` that returns a promise
|
|
475
|
+
opens it when that promise ends, fulfilled or rejected, and a `run` that
|
|
476
|
+
throws opens it and rethrows. Any other result keeps it closed until
|
|
477
|
+
`settle()` — unless `once(run, { settleSync: true })`, which opens it at
|
|
478
|
+
once. So: **return the request's promise, or follow the request with a busy
|
|
479
|
+
flag.** The wrapper returns `run`'s result — its promise as one that settles
|
|
480
|
+
the same way, so a rejection still reaches the caller.
|
|
481
|
+
|
|
482
|
+
`pendingRun()` is the run on its way (an increasing number, `undefined` when
|
|
483
|
+
open), and `settle(run)` opens the guard only while that run is still the one
|
|
484
|
+
on its way. `busySettler(guard)` uses them: call the function it returns
|
|
485
|
+
with each value of a busy flag, and once the flag was true and is false
|
|
486
|
+
again it settles the run that was on its way when the flag rose — never a
|
|
487
|
+
newer one.
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
import { createSubmitGuard } from '@nxgt/material-core/submit-guard';
|
|
491
|
+
|
|
492
|
+
const guard = createSubmitGuard();
|
|
493
|
+
const submit = guard.once((event: SubmitEvent) => {
|
|
494
|
+
event.preventDefault();
|
|
495
|
+
return fetch('/api/subscribe', { method: 'POST', body: new FormData(form) });
|
|
496
|
+
});
|
|
497
|
+
form.addEventListener('submit', submit); // a second submit before the reply is dropped
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Both libraries wrap it as `useSubmitOnce(busy?)`: `busy` is a boolean in
|
|
501
|
+
React (`useSubmitOnce(navigation.state !== 'idle')`) and a ref or a getter in
|
|
502
|
+
Vue (`useSubmitOnce(() => pending.value)`), followed through `busySettler`.
|
|
503
|
+
Without `busy`, the hooks pass `settleSync: true`, so a handler that returns
|
|
504
|
+
early does not lock the form.
|
|
505
|
+
|
|
506
|
+
### Errors worded by code: `/code-errors`
|
|
507
|
+
|
|
508
|
+
`createCodeErrorMap({ translate, codeOf?, fallback })` is a zod 4 error map
|
|
509
|
+
(typed by shape: this package does not depend on zod). An issue's code —
|
|
510
|
+
by default `defaultCodeOf`: a custom issue's `params.code`, when it is a
|
|
511
|
+
string — is worded by `translate(code, params)`; when there is no code, or
|
|
512
|
+
`translate` returns `undefined` or the code itself (a translator's answer for
|
|
513
|
+
a key it lacks), `fallback(issue)` words it. `isEmptyRequired(issue)` tells
|
|
514
|
+
an empty required field (a string shorter than one character, or a missing
|
|
515
|
+
value), to give it a code of its own.
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import { z } from 'zod';
|
|
519
|
+
import {
|
|
520
|
+
createCodeErrorMap,
|
|
521
|
+
defaultCodeOf,
|
|
522
|
+
isEmptyRequired,
|
|
523
|
+
} from '@nxgt/material-core/code-errors';
|
|
524
|
+
|
|
525
|
+
const words: Record<string, string> = {
|
|
526
|
+
'errors.taken': 'This name is already taken.',
|
|
527
|
+
'errors.required': 'Please fill this in.',
|
|
528
|
+
};
|
|
529
|
+
const error = createCodeErrorMap({
|
|
530
|
+
translate: (code) => words[code],
|
|
531
|
+
codeOf: (issue) => (isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue)),
|
|
532
|
+
fallback: () => undefined, // zod's own message
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const schema = z.object({
|
|
536
|
+
username: z.string().min(1).refine((name) => name !== 'admin', { params: { code: 'errors.taken' } }),
|
|
537
|
+
});
|
|
538
|
+
schema.safeParse({ username: 'admin' }, { error }); // "This name is already taken."
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Both libraries wrap it as `useCodeResolver(schema)`, with their
|
|
542
|
+
`zodLocaleError` as the fallback and the page's language.
|
|
543
|
+
|
|
544
|
+
### Two-step verification: `/two-factor`
|
|
545
|
+
|
|
546
|
+
`groupSecret(secret, size = 4)` reads a TOTP secret in groups, as a person
|
|
547
|
+
types it into an authenticator app (`"JBSWY3DPEHPK3PXP"` → `"JBSW Y3DP EHPK
|
|
548
|
+
3PXP"`); whitespace already in it is dropped first. `recoveryCodesText(codes,
|
|
549
|
+
{ heading?, note? })` is the text file a person keeps: the heading, the note,
|
|
550
|
+
a blank line, one code per line and a trailing newline (a line not given is
|
|
551
|
+
left out). `downloadText(text, fileName, { type? })` saves it through an
|
|
552
|
+
object URL and a temporary `<a download>`, revoked a minute later,
|
|
553
|
+
as `/pdf-download` does (its `OBJECT_URL_TTL_MS`).
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
import {
|
|
557
|
+
downloadText,
|
|
558
|
+
groupSecret,
|
|
559
|
+
recoveryCodesText,
|
|
560
|
+
} from '@nxgt/material-core/two-factor';
|
|
561
|
+
|
|
562
|
+
groupSecret('JBSWY3DPEHPK3PXP'); // "JBSW Y3DP EHPK 3PXP"
|
|
563
|
+
const text = recoveryCodesText(['7KQ2-M9XD', 'P4TW-8HZC'], {
|
|
564
|
+
heading: 'Recovery codes for ada@example.com',
|
|
565
|
+
note: 'Each code works once.',
|
|
566
|
+
});
|
|
567
|
+
downloadText(text, 'recovery-codes.txt');
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Both libraries' `EnrolmentSecret` and `RecoveryCodes` are built on them.
|
|
571
|
+
|
|
572
|
+
### Printing one element: `/print-only`
|
|
573
|
+
|
|
574
|
+
`printOnly(element, name?)` prints that element and nothing else of the
|
|
575
|
+
page: it sets `data-nxgt-print="<name>"` on `<html>` and
|
|
576
|
+
`data-nxgt-print-target` on the element, calls `window.print()`, and removes
|
|
577
|
+
both on `afterprint`, or as soon as `print()` returns where it blocks until
|
|
578
|
+
the dialog closes. It returns a function that removes them at once; call it
|
|
579
|
+
when the element unmounts. No inline style and no new window: the hiding is
|
|
580
|
+
the libraries' stylesheet (`print-only.css`, in their `styles.css`),
|
|
581
|
+
which under print media and `html[data-nxgt-print]` shows only the target.
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
import { printOnly } from '@nxgt/material-core/print-only';
|
|
585
|
+
|
|
586
|
+
const stop = printOnly(document.querySelector('#codes') as HTMLElement, 'recovery-codes');
|
|
587
|
+
// when the element unmounts:
|
|
588
|
+
stop();
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Both libraries re-export it from `/lib` (`@nxgt/material/lib`,
|
|
592
|
+
`@nxgt/material-vue/lib`); `RecoveryCodes`' Print button uses it.
|
|
593
|
+
|
|
460
594
|
### The emoji list's groups: `/emoji-groups`
|
|
461
595
|
|
|
462
596
|
How both libraries' `EmojiList` groups, searches and names its emojis:
|
|
@@ -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
|
+
}
|
|
@@ -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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/lib/code-errors.ts
|
|
2
|
+
function defaultCodeOf(issue) {
|
|
3
|
+
if (issue.code !== "custom")
|
|
4
|
+
return;
|
|
5
|
+
const code = issue.params?.code;
|
|
6
|
+
return typeof code === "string" ? code : undefined;
|
|
7
|
+
}
|
|
8
|
+
function isEmptyRequired(issue) {
|
|
9
|
+
if (issue.code === "too_small")
|
|
10
|
+
return issue.origin === "string" && Number(issue.minimum) === 1;
|
|
11
|
+
if (issue.code === "invalid_type")
|
|
12
|
+
return issue.input === undefined || issue.input === null;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function createCodeErrorMap({
|
|
16
|
+
translate,
|
|
17
|
+
codeOf = defaultCodeOf,
|
|
18
|
+
fallback
|
|
19
|
+
}) {
|
|
20
|
+
return (issue) => {
|
|
21
|
+
const code = codeOf(issue);
|
|
22
|
+
if (code) {
|
|
23
|
+
const words = translate(code, issue.params);
|
|
24
|
+
if (words !== undefined && words !== code)
|
|
25
|
+
return words;
|
|
26
|
+
}
|
|
27
|
+
return fallback(issue);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
createCodeErrorMap,
|
|
32
|
+
defaultCodeOf,
|
|
33
|
+
isEmptyRequired
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//# debugId=7DC5306E81E48FAB64756E2164756E21
|
|
37
|
+
//# sourceMappingURL=code-errors.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/code-errors.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * The part of a zod issue a code error map reads, by shape: core has no zod\n * dependency, and every issue zod 4 hands an error map is assignable to it.\n */\nexport type CodeIssue = {\n\treadonly code: string;\n\t/** A custom issue's params: `.refine(…, { params: { code } })`. */\n\treadonly params?: Record<string, unknown> | undefined;\n\t/** The value that failed. */\n\treadonly input?: unknown;\n\t/** What a size issue measured: `'string'`, `'number'`, `'array'`… */\n\treadonly origin?: string | undefined;\n\treadonly minimum?: unknown;\n\treadonly maximum?: unknown;\n\treadonly expected?: unknown;\n\treadonly message?: string | undefined;\n};\n\n/** The words for an issue, or `undefined` to let zod word it. */\nexport type CodeErrorMap = (issue: CodeIssue) => string | undefined;\n\nexport type CodeErrorMapOptions = {\n\t/**\n\t * The app's words for `code`, interpolating `params` (the custom issue's\n\t * params, so `{ code, min: 3 }` can read \"At least 3\"). `undefined`, or\n\t * the code returned verbatim (what a translator does with a key it lacks),\n\t * means there are no words for it: `fallback` words the issue instead.\n\t */\n\ttranslate: (\n\t\tcode: string,\n\t\tparams: Record<string, unknown> | undefined,\n\t) => string | undefined;\n\t/**\n\t * Which code an issue carries. Defaults to {@link defaultCodeOf}: a custom\n\t * issue's `params.code`, when it is a string.\n\t */\n\tcodeOf?: (issue: CodeIssue) => string | undefined;\n\t/**\n\t * Words an issue with no code, or one whose code has no words: the\n\t * libraries pass `zodLocaleError({ language, translate })`.\n\t */\n\tfallback: CodeErrorMap;\n};\n\n/** A custom issue's `params.code`, when it is a string; otherwise none. */\nexport function defaultCodeOf(issue: CodeIssue): string | undefined {\n\tif (issue.code !== 'custom') return undefined;\n\tconst code = issue.params?.code;\n\treturn typeof code === 'string' ? code : undefined;\n}\n\n/**\n * Whether an issue only says the field was left empty: a string shorter than\n * one character (`z.string().min(1)`), or a value of the wrong type that is\n * not there at all (`undefined` or `null`).\n *\n * @example\n * ```ts\n * createCodeErrorMap({\n * translate,\n * fallback,\n * codeOf: (issue) =>\n * isEmptyRequired(issue) ? 'errors.required' : defaultCodeOf(issue),\n * });\n * ```\n */\nexport function isEmptyRequired(issue: CodeIssue): boolean {\n\tif (issue.code === 'too_small')\n\t\treturn issue.origin === 'string' && Number(issue.minimum) === 1;\n\tif (issue.code === 'invalid_type')\n\t\treturn issue.input === undefined || issue.input === null;\n\treturn false;\n}\n\n/**\n * A zod 4 error map that words an issue by the code it carries, in the\n * app's words: a refinement's `params.code` (an API error code, say) reads\n * as `translate(code, params)`. An issue with no code, or whose code\n * `translate` has no words for, is worded by `fallback`.\n *\n * @example\n * ```ts\n * import { createCodeErrorMap } from '@nxgt/material-core/code-errors';\n *\n * const error = createCodeErrorMap({\n * translate: (code, params) => translate(code, params, language),\n * fallback: zodLocaleError({ language, translate }),\n * });\n * schema.safeParse(values, { error });\n * ```\n */\nexport function createCodeErrorMap({\n\ttranslate,\n\tcodeOf = defaultCodeOf,\n\tfallback,\n}: CodeErrorMapOptions): CodeErrorMap {\n\treturn (issue) => {\n\t\tconst code = codeOf(issue);\n\t\tif (code) {\n\t\t\tconst words = translate(code, issue.params);\n\t\t\tif (words !== undefined && words !== code) return words;\n\t\t}\n\t\treturn fallback(issue);\n\t};\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AA6CO,SAAS,aAAa,CAAC,OAAsC;AAAA,EACnE,IAAI,MAAM,SAAS;AAAA,IAAU;AAAA,EAC7B,MAAM,OAAO,MAAM,QAAQ;AAAA,EAC3B,OAAO,OAAO,SAAS,WAAW,OAAO;AAAA;AAkBnC,SAAS,eAAe,CAAC,OAA2B;AAAA,EAC1D,IAAI,MAAM,SAAS;AAAA,IAClB,OAAO,MAAM,WAAW,YAAY,OAAO,MAAM,OAAO,MAAM;AAAA,EAC/D,IAAI,MAAM,SAAS;AAAA,IAClB,OAAO,MAAM,UAAU,aAAa,MAAM,UAAU;AAAA,EACrD,OAAO;AAAA;AAoBD,SAAS,kBAAkB;AAAA,EACjC;AAAA,EACA,SAAS;AAAA,EACT;AAAA,GACqC;AAAA,EACrC,OAAO,CAAC,UAAU;AAAA,IACjB,MAAM,OAAO,OAAO,KAAK;AAAA,IACzB,IAAI,MAAM;AAAA,MACT,MAAM,QAAQ,UAAU,MAAM,MAAM,MAAM;AAAA,MAC1C,IAAI,UAAU,aAAa,UAAU;AAAA,QAAM,OAAO;AAAA,IACnD;AAAA,IACA,OAAO,SAAS,KAAK;AAAA;AAAA;",
|
|
8
|
+
"debugId": "7DC5306E81E48FAB64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { type PdfSource } from './pdf-source';
|
|
2
|
-
|
|
3
|
-
* How long an object URL outlives the click that started its download or
|
|
4
|
-
* opened its tab. Revoking it synchronously cancels the download in Firefox
|
|
5
|
-
* and Safari, which read the URL after `click()` returns.
|
|
6
|
-
*/
|
|
7
|
-
export declare const OBJECT_URL_TTL_MS = 60000;
|
|
2
|
+
export { OBJECT_URL_TTL_MS } from './save-blob';
|
|
8
3
|
export type PdfFileOptions = {
|
|
9
4
|
/** Name of the saved file; `.pdf` is added when missing. */
|
|
10
5
|
fileName?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pdf-download.d.ts","sourceRoot":"","sources":["../../src/lib/pdf-download.ts"],"names":[],"mappings":"AACA,OAAO,EAIN,KAAK,SAAS,EAEd,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"pdf-download.d.ts","sourceRoot":"","sources":["../../src/lib/pdf-download.ts"],"names":[],"mappings":"AACA,OAAO,EAIN,KAAK,SAAS,EAEd,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MACP,UAAU,GACV,IAAI,GACJ,SAAS,GACT,OAAO,CAAC,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAC1C,CAAC;AAsDF;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE,cAAmB,iBAW5B;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAM,iBA8B7C"}
|
package/dist/lib/pdf-download.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
OBJECT_URL_TTL_MS2,
|
|
3
2
|
downloadPdfFile2,
|
|
4
3
|
printPdfFile2
|
|
5
|
-
} from "../chunks/index-
|
|
4
|
+
} from "../chunks/index-r93kpbtg.js";
|
|
5
|
+
import {
|
|
6
|
+
OBJECT_URL_TTL_MS2
|
|
7
|
+
} from "../chunks/index-15vj9xbx.js";
|
|
6
8
|
import"../chunks/index-paaaedrm.js";
|
|
7
9
|
import"../chunks/index-5r6rzp43.js";
|
|
8
10
|
export {
|
|
@@ -11,5 +13,5 @@ export {
|
|
|
11
13
|
printPdfFile2 as printPdfFile
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
//# debugId=
|
|
16
|
+
//# debugId=ACA699EE49816B1764756E2164756E21
|
|
15
17
|
//# sourceMappingURL=pdf-download.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nxgt/material-core/print-only` — prints one element of the page and
|
|
3
|
+
* nothing else, without an inline style or a new window: the page's
|
|
4
|
+
* stylesheet does the hiding. Both libraries ship the rules (`print-only.css`,
|
|
5
|
+
* through their `styles.css`): under
|
|
6
|
+
* `html[data-nxgt-print]`, print media shows only `[data-nxgt-print-target]`.
|
|
7
|
+
* Reads the DOM only when called.
|
|
8
|
+
*/
|
|
9
|
+
/** Set on `<html>` while printing, to the `name` given to {@link printOnly}. */
|
|
10
|
+
export declare const PRINT_ATTRIBUTE = "data-nxgt-print";
|
|
11
|
+
/** Set on the element being printed. */
|
|
12
|
+
export declare const PRINT_TARGET_ATTRIBUTE = "data-nxgt-print-target";
|
|
13
|
+
/**
|
|
14
|
+
* Marks `target` as the only thing to print, opens the browser's print
|
|
15
|
+
* dialog and unmarks it once printing is over: on `afterprint`, or as soon as
|
|
16
|
+
* `print()` returns where it blocks until the dialog closes (it fired
|
|
17
|
+
* `beforeprint` before returning). The returned function unmarks it at once —
|
|
18
|
+
* call it when the element goes away, for a browser whose `print()` returns
|
|
19
|
+
* early and never says it is done.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const stop = printOnly(card, 'recovery-codes');
|
|
24
|
+
* // later, when the card unmounts:
|
|
25
|
+
* stop();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function printOnly(target: HTMLElement, name?: string): () => void;
|
|
29
|
+
//# sourceMappingURL=print-only.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"print-only.d.ts","sourceRoot":"","sources":["../../src/lib/print-only.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gFAAgF;AAChF,eAAO,MAAM,eAAe,oBAAoB,CAAC;AACjD,wCAAwC;AACxC,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,SAAY,GAAG,MAAM,IAAI,CA0B3E"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/lib/print-only.ts
|
|
2
|
+
var PRINT_ATTRIBUTE = "data-nxgt-print";
|
|
3
|
+
var PRINT_TARGET_ATTRIBUTE = "data-nxgt-print-target";
|
|
4
|
+
function printOnly(target, name = "element") {
|
|
5
|
+
const doc = target.ownerDocument;
|
|
6
|
+
const view = doc.defaultView ?? window;
|
|
7
|
+
const root = doc.documentElement;
|
|
8
|
+
let began = false;
|
|
9
|
+
const onBegin = () => {
|
|
10
|
+
began = true;
|
|
11
|
+
};
|
|
12
|
+
const clear = () => {
|
|
13
|
+
view.removeEventListener("beforeprint", onBegin);
|
|
14
|
+
view.removeEventListener("afterprint", clear);
|
|
15
|
+
root.removeAttribute(PRINT_ATTRIBUTE);
|
|
16
|
+
target.removeAttribute(PRINT_TARGET_ATTRIBUTE);
|
|
17
|
+
};
|
|
18
|
+
root.setAttribute(PRINT_ATTRIBUTE, name);
|
|
19
|
+
target.setAttribute(PRINT_TARGET_ATTRIBUTE, "");
|
|
20
|
+
view.addEventListener("beforeprint", onBegin);
|
|
21
|
+
view.addEventListener("afterprint", clear);
|
|
22
|
+
try {
|
|
23
|
+
view.print();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
clear();
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
if (began)
|
|
29
|
+
clear();
|
|
30
|
+
return clear;
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
PRINT_ATTRIBUTE,
|
|
34
|
+
PRINT_TARGET_ATTRIBUTE,
|
|
35
|
+
printOnly
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//# debugId=BF8B5FF251A0F79964756E2164756E21
|
|
39
|
+
//# sourceMappingURL=print-only.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/print-only.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `@nxgt/material-core/print-only` — prints one element of the page and\n * nothing else, without an inline style or a new window: the page's\n * stylesheet does the hiding. Both libraries ship the rules (`print-only.css`,\n * through their `styles.css`): under\n * `html[data-nxgt-print]`, print media shows only `[data-nxgt-print-target]`.\n * Reads the DOM only when called.\n */\n\n/** Set on `<html>` while printing, to the `name` given to {@link printOnly}. */\nexport const PRINT_ATTRIBUTE = 'data-nxgt-print';\n/** Set on the element being printed. */\nexport const PRINT_TARGET_ATTRIBUTE = 'data-nxgt-print-target';\n\n/**\n * Marks `target` as the only thing to print, opens the browser's print\n * dialog and unmarks it once printing is over: on `afterprint`, or as soon as\n * `print()` returns where it blocks until the dialog closes (it fired\n * `beforeprint` before returning). The returned function unmarks it at once —\n * call it when the element goes away, for a browser whose `print()` returns\n * early and never says it is done.\n *\n * @example\n * ```ts\n * const stop = printOnly(card, 'recovery-codes');\n * // later, when the card unmounts:\n * stop();\n * ```\n */\nexport function printOnly(target: HTMLElement, name = 'element'): () => void {\n\tconst doc = target.ownerDocument;\n\tconst view = doc.defaultView ?? window;\n\tconst root = doc.documentElement;\n\tlet began = false;\n\tconst onBegin = () => {\n\t\tbegan = true;\n\t};\n\tconst clear = () => {\n\t\tview.removeEventListener('beforeprint', onBegin);\n\t\tview.removeEventListener('afterprint', clear);\n\t\troot.removeAttribute(PRINT_ATTRIBUTE);\n\t\ttarget.removeAttribute(PRINT_TARGET_ATTRIBUTE);\n\t};\n\troot.setAttribute(PRINT_ATTRIBUTE, name);\n\ttarget.setAttribute(PRINT_TARGET_ATTRIBUTE, '');\n\tview.addEventListener('beforeprint', onBegin);\n\tview.addEventListener('afterprint', clear);\n\ttry {\n\t\tview.print();\n\t} catch (error) {\n\t\tclear();\n\t\tthrow error;\n\t}\n\tif (began) clear();\n\treturn clear;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAUO,IAAM,kBAAkB;AAExB,IAAM,yBAAyB;AAiB/B,SAAS,SAAS,CAAC,QAAqB,OAAO,WAAuB;AAAA,EAC5E,MAAM,MAAM,OAAO;AAAA,EACnB,MAAM,OAAO,IAAI,eAAe;AAAA,EAChC,MAAM,OAAO,IAAI;AAAA,EACjB,IAAI,QAAQ;AAAA,EACZ,MAAM,UAAU,MAAM;AAAA,IACrB,QAAQ;AAAA;AAAA,EAET,MAAM,QAAQ,MAAM;AAAA,IACnB,KAAK,oBAAoB,eAAe,OAAO;AAAA,IAC/C,KAAK,oBAAoB,cAAc,KAAK;AAAA,IAC5C,KAAK,gBAAgB,eAAe;AAAA,IACpC,OAAO,gBAAgB,sBAAsB;AAAA;AAAA,EAE9C,KAAK,aAAa,iBAAiB,IAAI;AAAA,EACvC,OAAO,aAAa,wBAAwB,EAAE;AAAA,EAC9C,KAAK,iBAAiB,eAAe,OAAO;AAAA,EAC5C,KAAK,iBAAiB,cAAc,KAAK;AAAA,EACzC,IAAI;AAAA,IACH,KAAK,MAAM;AAAA,IACV,OAAO,OAAO;AAAA,IACf,MAAM;AAAA,IACN,MAAM;AAAA;AAAA,EAEP,IAAI;AAAA,IAAO,MAAM;AAAA,EACjB,OAAO;AAAA;",
|
|
8
|
+
"debugId": "BF8B5FF251A0F79964756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saving a Blob as a file through a temporary `<a download>`: what
|
|
3
|
+
* `/pdf-download` and `/two-factor`'s `downloadText` share. Reads the DOM
|
|
4
|
+
* only when called.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* How long an object URL outlives the click that started its download or
|
|
8
|
+
* opened its tab. Revoking it synchronously cancels the download in Firefox
|
|
9
|
+
* and Safari, which read the URL after `click()` returns.
|
|
10
|
+
*/
|
|
11
|
+
export declare const OBJECT_URL_TTL_MS = 60000;
|
|
12
|
+
export declare function revokeLater(url: string): void;
|
|
13
|
+
export declare function clickLink(href: string, attributes: Record<string, string>): void;
|
|
14
|
+
/** Saves a Blob under `fileName`, and revokes its URL once the save began. */
|
|
15
|
+
export declare function saveBlob(blob: Blob, fileName: string): void;
|
|
16
|
+
//# sourceMappingURL=save-blob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save-blob.d.ts","sourceRoot":"","sources":["../../src/lib/save-blob.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAExC,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,QAEtC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAWzE;AAED,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,QAIpD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** How {@link SubmitGuard.once} treats what `run` returns. */
|
|
2
|
+
export type SubmitOnceOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* A result that is not a promise (a handler that returned early, say)
|
|
5
|
+
* settles the guard at once. Default `false`: the guard then waits for
|
|
6
|
+
* `settle()`, which suits a request followed by a busy flag
|
|
7
|
+
* ({@link busySettler}). Without one, return the request's promise or set
|
|
8
|
+
* this.
|
|
9
|
+
*/
|
|
10
|
+
settleSync?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** One submit in flight: see {@link createSubmitGuard}. */
|
|
13
|
+
export type SubmitGuard = {
|
|
14
|
+
/**
|
|
15
|
+
* `run`, wrapped so it runs only when no submit is on its way. The wrapper
|
|
16
|
+
* returns what `run` returned — a promise as one that settles as `run`'s
|
|
17
|
+
* does, rejection included — or `undefined` when the call was dropped.
|
|
18
|
+
* A dropped call whose first argument is an event (a form's `submit`)
|
|
19
|
+
* has its default prevented, so it never falls back to the browser's own
|
|
20
|
+
* submission, which would reload the page.
|
|
21
|
+
*/
|
|
22
|
+
once<A extends unknown[], R>(run: (...args: A) => R, options?: SubmitOnceOptions): (...args: A) => R | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Opens the guard again: the next call goes through. Given a run (from
|
|
25
|
+
* {@link SubmitGuard.pendingRun}), only while that run is still the one
|
|
26
|
+
* on its way: a late `settle(run)` never reopens it over a newer one.
|
|
27
|
+
*/
|
|
28
|
+
settle(run?: number): void;
|
|
29
|
+
/** The run on its way (an increasing number), or `undefined` when open. */
|
|
30
|
+
pendingRun(): number | undefined;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Lets one submit through until the request it started settles. A second
|
|
34
|
+
* click, or Enter, before the busy state renders (a disabled or loading
|
|
35
|
+
* button only stops clicks once rendered, and never Enter) would post again,
|
|
36
|
+
* and may abort the first request mid-call.
|
|
37
|
+
*
|
|
38
|
+
* `settle()` opens it again. A `run` that returns a promise settles it when
|
|
39
|
+
* that promise ends, fulfilled or rejected; a `run` that throws settles it
|
|
40
|
+
* and rethrows; any other result keeps it closed until `settle()`, unless
|
|
41
|
+
* `once(run, { settleSync: true })`. So: return the request's promise, or
|
|
42
|
+
* follow the request with a busy flag ({@link busySettler}). The guard never
|
|
43
|
+
* swallows a rejection: the promise the wrapper returns rejects as `run`'s
|
|
44
|
+
* did, for the caller to handle (left unhandled, it is reported as `run`'s
|
|
45
|
+
* would have been), and the guard adds no promise of its own that could
|
|
46
|
+
* reject unhandled.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const guard = createSubmitGuard();
|
|
51
|
+
* const submit = guard.once((values: Values) => save(values));
|
|
52
|
+
* form.addEventListener('submit', (event) => {
|
|
53
|
+
* event.preventDefault();
|
|
54
|
+
* submit(read(form)); // a second submit before `save` ends is dropped
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function createSubmitGuard(): SubmitGuard;
|
|
59
|
+
/**
|
|
60
|
+
* Follows a request's busy flag for `guard`: call the function it returns
|
|
61
|
+
* with each value of the flag. Once the flag was true and is false again, it
|
|
62
|
+
* settles the run that was on its way when the flag rose — never a newer one
|
|
63
|
+
* — or, when none was, the run on its way when it falls.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const onBusy = busySettler(guard);
|
|
68
|
+
* router.subscribe((state) => onBusy(state.navigation.state !== 'idle'));
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare function busySettler(guard: SubmitGuard): (busy: boolean) => void;
|
|
72
|
+
//# sourceMappingURL=submit-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit-guard.d.ts","sourceRoot":"","sources":["../../src/lib/submit-guard.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,MAAM,iBAAiB,GAAG;IAC/B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,WAAW,GAAG;IACzB;;;;;;;OAOG;IACH,IAAI,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EAC1B,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,EACtB,OAAO,CAAC,EAAE,iBAAiB,GACzB,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,UAAU,IAAI,MAAM,GAAG,SAAS,CAAC;CACjC,CAAC;AAcF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CAuC/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAcvE"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/lib/submit-guard.ts
|
|
2
|
+
var preventDefault = (event) => {
|
|
3
|
+
const target = event;
|
|
4
|
+
if (typeof target?.preventDefault === "function")
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
};
|
|
7
|
+
var isThenable = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
|
|
8
|
+
function createSubmitGuard() {
|
|
9
|
+
let pending = false;
|
|
10
|
+
let run = 0;
|
|
11
|
+
const settle = (id) => {
|
|
12
|
+
if (id === undefined || id === run)
|
|
13
|
+
pending = false;
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
once(submit, options = {}) {
|
|
17
|
+
return (...args) => {
|
|
18
|
+
if (pending) {
|
|
19
|
+
preventDefault(args[0]);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
pending = true;
|
|
23
|
+
run += 1;
|
|
24
|
+
const id = run;
|
|
25
|
+
let ran;
|
|
26
|
+
try {
|
|
27
|
+
ran = submit(...args);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
settle(id);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (!isThenable(ran)) {
|
|
33
|
+
if (options.settleSync)
|
|
34
|
+
settle(id);
|
|
35
|
+
return ran;
|
|
36
|
+
}
|
|
37
|
+
const settled = Promise.resolve(ran).finally(() => settle(id));
|
|
38
|
+
return settled;
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
settle,
|
|
42
|
+
pendingRun: () => pending ? run : undefined
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function busySettler(guard) {
|
|
46
|
+
let rose = false;
|
|
47
|
+
let runAtRise;
|
|
48
|
+
return (busy) => {
|
|
49
|
+
if (busy) {
|
|
50
|
+
if (!rose)
|
|
51
|
+
runAtRise = guard.pendingRun();
|
|
52
|
+
rose = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!rose)
|
|
56
|
+
return;
|
|
57
|
+
rose = false;
|
|
58
|
+
const run = runAtRise ?? guard.pendingRun();
|
|
59
|
+
if (run !== undefined)
|
|
60
|
+
guard.settle(run);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
busySettler,
|
|
65
|
+
createSubmitGuard
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//# debugId=0F5FC92C3A2BF8E064756E2164756E21
|
|
69
|
+
//# sourceMappingURL=submit-guard.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/submit-guard.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/** How {@link SubmitGuard.once} treats what `run` returns. */\nexport type SubmitOnceOptions = {\n\t/**\n\t * A result that is not a promise (a handler that returned early, say)\n\t * settles the guard at once. Default `false`: the guard then waits for\n\t * `settle()`, which suits a request followed by a busy flag\n\t * ({@link busySettler}). Without one, return the request's promise or set\n\t * this.\n\t */\n\tsettleSync?: boolean;\n};\n\n/** One submit in flight: see {@link createSubmitGuard}. */\nexport type SubmitGuard = {\n\t/**\n\t * `run`, wrapped so it runs only when no submit is on its way. The wrapper\n\t * returns what `run` returned — a promise as one that settles as `run`'s\n\t * does, rejection included — or `undefined` when the call was dropped.\n\t * A dropped call whose first argument is an event (a form's `submit`)\n\t * has its default prevented, so it never falls back to the browser's own\n\t * submission, which would reload the page.\n\t */\n\tonce<A extends unknown[], R>(\n\t\trun: (...args: A) => R,\n\t\toptions?: SubmitOnceOptions,\n\t): (...args: A) => R | undefined;\n\t/**\n\t * Opens the guard again: the next call goes through. Given a run (from\n\t * {@link SubmitGuard.pendingRun}), only while that run is still the one\n\t * on its way: a late `settle(run)` never reopens it over a newer one.\n\t */\n\tsettle(run?: number): void;\n\t/** The run on its way (an increasing number), or `undefined` when open. */\n\tpendingRun(): number | undefined;\n};\n\n/** Prevents the default of a dropped call's event, if it carries one. */\nconst preventDefault = (event: unknown) => {\n\tconst target = event as { preventDefault?: unknown } | null;\n\tif (typeof target?.preventDefault === 'function')\n\t\t(event as Event).preventDefault();\n};\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n\ttypeof value === 'object' &&\n\tvalue !== null &&\n\ttypeof (value as { then?: unknown }).then === 'function';\n\n/**\n * Lets one submit through until the request it started settles. A second\n * click, or Enter, before the busy state renders (a disabled or loading\n * button only stops clicks once rendered, and never Enter) would post again,\n * and may abort the first request mid-call.\n *\n * `settle()` opens it again. A `run` that returns a promise settles it when\n * that promise ends, fulfilled or rejected; a `run` that throws settles it\n * and rethrows; any other result keeps it closed until `settle()`, unless\n * `once(run, { settleSync: true })`. So: return the request's promise, or\n * follow the request with a busy flag ({@link busySettler}). The guard never\n * swallows a rejection: the promise the wrapper returns rejects as `run`'s\n * did, for the caller to handle (left unhandled, it is reported as `run`'s\n * would have been), and the guard adds no promise of its own that could\n * reject unhandled.\n *\n * @example\n * ```ts\n * const guard = createSubmitGuard();\n * const submit = guard.once((values: Values) => save(values));\n * form.addEventListener('submit', (event) => {\n * event.preventDefault();\n * submit(read(form)); // a second submit before `save` ends is dropped\n * });\n * ```\n */\nexport function createSubmitGuard(): SubmitGuard {\n\tlet pending = false;\n\t// Which run is on its way: an older run's promise (or busy flag) ending\n\t// late never reopens the guard over a newer one.\n\tlet run = 0;\n\tconst settle = (id?: number) => {\n\t\tif (id === undefined || id === run) pending = false;\n\t};\n\treturn {\n\t\tonce(submit, options = {}) {\n\t\t\treturn (...args) => {\n\t\t\t\tif (pending) {\n\t\t\t\t\tpreventDefault(args[0]);\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tpending = true;\n\t\t\t\trun += 1;\n\t\t\t\tconst id = run;\n\t\t\t\tlet ran: ReturnType<typeof submit>;\n\t\t\t\ttry {\n\t\t\t\t\tran = submit(...args);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsettle(id);\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tif (!isThenable(ran)) {\n\t\t\t\t\tif (options.settleSync) settle(id);\n\t\t\t\t\treturn ran;\n\t\t\t\t}\n\t\t\t\t// The one chain on `ran`, handed back: a rejection reaches the\n\t\t\t\t// caller, never a second, unhandled promise. It is the same\n\t\t\t\t// promise type for a native promise (`Promise.resolve` returns it).\n\t\t\t\tconst settled = Promise.resolve(ran).finally(() => settle(id));\n\t\t\t\treturn settled as unknown as typeof ran;\n\t\t\t};\n\t\t},\n\t\tsettle,\n\t\tpendingRun: () => (pending ? run : undefined),\n\t};\n}\n\n/**\n * Follows a request's busy flag for `guard`: call the function it returns\n * with each value of the flag. Once the flag was true and is false again, it\n * settles the run that was on its way when the flag rose — never a newer one\n * — or, when none was, the run on its way when it falls.\n *\n * @example\n * ```ts\n * const onBusy = busySettler(guard);\n * router.subscribe((state) => onBusy(state.navigation.state !== 'idle'));\n * ```\n */\nexport function busySettler(guard: SubmitGuard): (busy: boolean) => void {\n\tlet rose = false;\n\tlet runAtRise: number | undefined;\n\treturn (busy) => {\n\t\tif (busy) {\n\t\t\tif (!rose) runAtRise = guard.pendingRun();\n\t\t\trose = true;\n\t\t\treturn;\n\t\t}\n\t\tif (!rose) return;\n\t\trose = false;\n\t\tconst run = runAtRise ?? guard.pendingRun();\n\t\tif (run !== undefined) guard.settle(run);\n\t};\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAqCA,IAAM,iBAAiB,CAAC,UAAmB;AAAA,EAC1C,MAAM,SAAS;AAAA,EACf,IAAI,OAAO,QAAQ,mBAAmB;AAAA,IACpC,MAAgB,eAAe;AAAA;AAGlC,IAAM,aAAa,CAAC,UACnB,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA6B,SAAS;AA4BxC,SAAS,iBAAiB,GAAgB;AAAA,EAChD,IAAI,UAAU;AAAA,EAGd,IAAI,MAAM;AAAA,EACV,MAAM,SAAS,CAAC,OAAgB;AAAA,IAC/B,IAAI,OAAO,aAAa,OAAO;AAAA,MAAK,UAAU;AAAA;AAAA,EAE/C,OAAO;AAAA,IACN,IAAI,CAAC,QAAQ,UAAU,CAAC,GAAG;AAAA,MAC1B,OAAO,IAAI,SAAS;AAAA,QACnB,IAAI,SAAS;AAAA,UACZ,eAAe,KAAK,EAAE;AAAA,UACtB;AAAA,QACD;AAAA,QACA,UAAU;AAAA,QACV,OAAO;AAAA,QACP,MAAM,KAAK;AAAA,QACX,IAAI;AAAA,QACJ,IAAI;AAAA,UACH,MAAM,OAAO,GAAG,IAAI;AAAA,UACnB,OAAO,OAAO;AAAA,UACf,OAAO,EAAE;AAAA,UACT,MAAM;AAAA;AAAA,QAEP,IAAI,CAAC,WAAW,GAAG,GAAG;AAAA,UACrB,IAAI,QAAQ;AAAA,YAAY,OAAO,EAAE;AAAA,UACjC,OAAO;AAAA,QACR;AAAA,QAIA,MAAM,UAAU,QAAQ,QAAQ,GAAG,EAAE,QAAQ,MAAM,OAAO,EAAE,CAAC;AAAA,QAC7D,OAAO;AAAA;AAAA;AAAA,IAGT;AAAA,IACA,YAAY,MAAO,UAAU,MAAM;AAAA,EACpC;AAAA;AAeM,SAAS,WAAW,CAAC,OAA6C;AAAA,EACxE,IAAI,OAAO;AAAA,EACX,IAAI;AAAA,EACJ,OAAO,CAAC,SAAS;AAAA,IAChB,IAAI,MAAM;AAAA,MACT,IAAI,CAAC;AAAA,QAAM,YAAY,MAAM,WAAW;AAAA,MACxC,OAAO;AAAA,MACP;AAAA,IACD;AAAA,IACA,IAAI,CAAC;AAAA,MAAM;AAAA,IACX,OAAO;AAAA,IACP,MAAM,MAAM,aAAa,MAAM,WAAW;AAAA,IAC1C,IAAI,QAAQ;AAAA,MAAW,MAAM,OAAO,GAAG;AAAA;AAAA;",
|
|
8
|
+
"debugId": "0F5FC92C3A2BF8E064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A secret in groups of `size` characters (4 by default), as one reads it
|
|
3
|
+
* out to type it: `"JBSWY3DPEHPK3PXP"` → `"JBSW Y3DP EHPK 3PXP"`. Whitespace
|
|
4
|
+
* already in it is dropped first, so a grouped secret groups the same.
|
|
5
|
+
*/
|
|
6
|
+
export declare function groupSecret(secret: string, size?: number): string;
|
|
7
|
+
export type RecoveryCodesTextOptions = {
|
|
8
|
+
/** The file's first line: what the codes are for (an account, a product). */
|
|
9
|
+
heading?: string;
|
|
10
|
+
/** Its second line: how to use them ("Each code works once."). */
|
|
11
|
+
note?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The recovery codes as the text file a person keeps: the heading, the note,
|
|
15
|
+
* a blank line, then one code per line and a trailing newline. A heading or
|
|
16
|
+
* note left out (or empty) is a line left out; with neither, the file is the
|
|
17
|
+
* codes alone.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* recoveryCodesText(['A-1', 'B-2'], { heading: 'Codes for ada@example.com', note: 'Each works once.' });
|
|
22
|
+
* // "Codes for ada@example.com\nEach works once.\n\nA-1\nB-2\n"
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function recoveryCodesText(codes: readonly string[], { heading, note }?: RecoveryCodesTextOptions): string;
|
|
26
|
+
export type DownloadTextOptions = {
|
|
27
|
+
/** The file's media type; `text/plain;charset=utf-8` by default. */
|
|
28
|
+
type?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Saves `text` as a file named `fileName`, through an object URL and a
|
|
32
|
+
* temporary `<a download>`; the URL is revoked a minute later
|
|
33
|
+
* (`OBJECT_URL_TTL_MS`), as revoking it at once cancels the download in
|
|
34
|
+
* Firefox and Safari.
|
|
35
|
+
*/
|
|
36
|
+
export declare function downloadText(text: string, fileName: string, options?: DownloadTextOptions): void;
|
|
37
|
+
//# sourceMappingURL=two-factor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two-factor.d.ts","sourceRoot":"","sources":["../../src/lib/two-factor.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,SAAI,GAAG,MAAM,CAS5D;AAED,MAAM,MAAM,wBAAwB,GAAG;IACtC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,EAAE,OAAO,EAAE,IAAI,EAAE,GAAE,wBAA6B,GAC9C,MAAM,CAIR;AAED,MAAM,MAAM,mBAAmB,GAAG;IACjC,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,mBAAwB,GAC/B,IAAI,CAGN"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
saveBlob
|
|
3
|
+
} from "../chunks/index-15vj9xbx.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/two-factor.ts
|
|
6
|
+
function groupSecret(secret, size = 4) {
|
|
7
|
+
const raw = secret.replace(/\s+/g, "");
|
|
8
|
+
const step = Math.floor(size);
|
|
9
|
+
if (!(step >= 1))
|
|
10
|
+
return raw;
|
|
11
|
+
const groups = [];
|
|
12
|
+
for (let start = 0;start < raw.length; start += step) {
|
|
13
|
+
groups.push(raw.slice(start, start + step));
|
|
14
|
+
}
|
|
15
|
+
return groups.join(" ");
|
|
16
|
+
}
|
|
17
|
+
function recoveryCodesText(codes, { heading, note } = {}) {
|
|
18
|
+
const head = [heading, note].filter((line) => Boolean(line));
|
|
19
|
+
const body = `${codes.join(`
|
|
20
|
+
`)}
|
|
21
|
+
`;
|
|
22
|
+
return head.length ? `${head.join(`
|
|
23
|
+
`)}
|
|
24
|
+
|
|
25
|
+
${body}` : body;
|
|
26
|
+
}
|
|
27
|
+
function downloadText(text, fileName, options = {}) {
|
|
28
|
+
const type = options.type ?? "text/plain;charset=utf-8";
|
|
29
|
+
saveBlob(new Blob([text], { type }), fileName);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
downloadText,
|
|
33
|
+
groupSecret,
|
|
34
|
+
recoveryCodesText
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
//# debugId=86107FED2A9CC6B264756E2164756E21
|
|
38
|
+
//# sourceMappingURL=two-factor.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/two-factor.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `@nxgt/material-core/two-factor` — the framework-free part of both\n * libraries' `EnrolmentSecret` and `RecoveryCodes`: a TOTP secret grouped\n * to be typed by hand, the recovery codes as a text file, and saving that\n * file. Reads the DOM only when called. Printing one element of the page is\n * `/print-only`.\n */\nimport { saveBlob } from './save-blob';\n\n/**\n * A secret in groups of `size` characters (4 by default), as one reads it\n * out to type it: `\"JBSWY3DPEHPK3PXP\"` → `\"JBSW Y3DP EHPK 3PXP\"`. Whitespace\n * already in it is dropped first, so a grouped secret groups the same.\n */\nexport function groupSecret(secret: string, size = 4): string {\n\tconst raw = secret.replace(/\\s+/g, '');\n\tconst step = Math.floor(size);\n\tif (!(step >= 1)) return raw;\n\tconst groups: string[] = [];\n\tfor (let start = 0; start < raw.length; start += step) {\n\t\tgroups.push(raw.slice(start, start + step));\n\t}\n\treturn groups.join(' ');\n}\n\nexport type RecoveryCodesTextOptions = {\n\t/** The file's first line: what the codes are for (an account, a product). */\n\theading?: string;\n\t/** Its second line: how to use them (\"Each code works once.\"). */\n\tnote?: string;\n};\n\n/**\n * The recovery codes as the text file a person keeps: the heading, the note,\n * a blank line, then one code per line and a trailing newline. A heading or\n * note left out (or empty) is a line left out; with neither, the file is the\n * codes alone.\n *\n * @example\n * ```ts\n * recoveryCodesText(['A-1', 'B-2'], { heading: 'Codes for ada@example.com', note: 'Each works once.' });\n * // \"Codes for ada@example.com\\nEach works once.\\n\\nA-1\\nB-2\\n\"\n * ```\n */\nexport function recoveryCodesText(\n\tcodes: readonly string[],\n\t{ heading, note }: RecoveryCodesTextOptions = {},\n): string {\n\tconst head = [heading, note].filter((line): line is string => Boolean(line));\n\tconst body = `${codes.join('\\n')}\\n`;\n\treturn head.length ? `${head.join('\\n')}\\n\\n${body}` : body;\n}\n\nexport type DownloadTextOptions = {\n\t/** The file's media type; `text/plain;charset=utf-8` by default. */\n\ttype?: string;\n};\n\n/**\n * Saves `text` as a file named `fileName`, through an object URL and a\n * temporary `<a download>`; the URL is revoked a minute later\n * (`OBJECT_URL_TTL_MS`), as revoking it at once cancels the download in\n * Firefox and Safari.\n */\nexport function downloadText(\n\ttext: string,\n\tfileName: string,\n\toptions: DownloadTextOptions = {},\n): void {\n\tconst type = options.type ?? 'text/plain;charset=utf-8';\n\tsaveBlob(new Blob([text], { type }), fileName);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;AAcO,SAAS,WAAW,CAAC,QAAgB,OAAO,GAAW;AAAA,EAC7D,MAAM,MAAM,OAAO,QAAQ,QAAQ,EAAE;AAAA,EACrC,MAAM,OAAO,KAAK,MAAM,IAAI;AAAA,EAC5B,IAAI,EAAE,QAAQ;AAAA,IAAI,OAAO;AAAA,EACzB,MAAM,SAAmB,CAAC;AAAA,EAC1B,SAAS,QAAQ,EAAG,QAAQ,IAAI,QAAQ,SAAS,MAAM;AAAA,IACtD,OAAO,KAAK,IAAI,MAAM,OAAO,QAAQ,IAAI,CAAC;AAAA,EAC3C;AAAA,EACA,OAAO,OAAO,KAAK,GAAG;AAAA;AAsBhB,SAAS,iBAAiB,CAChC,SACE,SAAS,SAAmC,CAAC,GACtC;AAAA,EACT,MAAM,OAAO,CAAC,SAAS,IAAI,EAAE,OAAO,CAAC,SAAyB,QAAQ,IAAI,CAAC;AAAA,EAC3E,MAAM,OAAO,GAAG,MAAM,KAAK;AAAA,CAAI;AAAA;AAAA,EAC/B,OAAO,KAAK,SAAS,GAAG,KAAK,KAAK;AAAA,CAAI;AAAA;AAAA,EAAQ,SAAS;AAAA;AAcjD,SAAS,YAAY,CAC3B,MACA,UACA,UAA+B,CAAC,GACzB;AAAA,EACP,MAAM,OAAO,QAAQ,QAAQ;AAAA,EAC7B,SAAS,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,KAAK,CAAC,GAAG,QAAQ;AAAA;",
|
|
8
|
+
"debugId": "86107FED2A9CC6B264756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/docs/roadmap.md
CHANGED
|
@@ -6,18 +6,33 @@ major they target.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
both libraries read the nonce from, so every `<style>` they create carries
|
|
11
|
-
it under a strict `style-src-elem`, and a `build.assetsInlineLimit`
|
|
12
|
-
(`cspAssetsInlineLimit` in `/vite`) that keeps fonts out of `data:` URIs.
|
|
13
|
-
Shipped here first; both libraries use it now.
|
|
14
|
-
|
|
15
|
-
- **Built on first use** (`/lazy-object`) — `buildOnce` and `lazyObject`,
|
|
16
|
-
with which both libraries build their zod schemas on first use rather
|
|
17
|
-
than at import, so an app under a strict CSP can configure zod first.
|
|
9
|
+
Nothing is in progress.
|
|
18
10
|
|
|
19
11
|
## Next
|
|
20
12
|
|
|
13
|
+
- **One Vite plugin for the whole setup** — `nxgtMaterial()` is implemented
|
|
14
|
+
once in `/vite` and re-exported by `@nxgt/material/vite` and
|
|
15
|
+
`@nxgt/material-vue/vite`, so both libraries get the same one-call setup.
|
|
16
|
+
Every option is on by default and each can be turned off or refined; the
|
|
17
|
+
helpers that exist today (`cspAssetsInlineLimit` and the others) stay
|
|
18
|
+
exported.
|
|
19
|
+
- `csp`: a nonce placeholder through `html.cspNonce` and
|
|
20
|
+
`applyCspNonce(html, nonce)`; zod's jitless mode as an inline nonced
|
|
21
|
+
script in `index.html` (`ZOD_JITLESS_SCRIPT` for server rendering); the
|
|
22
|
+
recommended policy sent by the dev server as Report-Only.
|
|
23
|
+
- `fonts`: the KaTeX fonts emitted as files, and `sans: 'inter'` for a
|
|
24
|
+
self-hosted Inter.
|
|
25
|
+
- `styles`: a build that fails with the exact fix when the stylesheet import
|
|
26
|
+
or the `@source` line is missing.
|
|
27
|
+
- `pdf`: pdfkit's standard fonts handled automatically when
|
|
28
|
+
`@react-pdf/renderer` is installed, and `bundle: true` (the Nuxt module's
|
|
29
|
+
`pdf.bundleStandardFonts` today) for a server that has no
|
|
30
|
+
`node_modules`.
|
|
31
|
+
- Nuxt: the Nuxt module takes the same options.
|
|
32
|
+
- Server helpers: `createCspNonce()` (Web Crypto) and `cspPolicy({ ... })`,
|
|
33
|
+
which returns `{ directives, header }` as `@nxgt/material`'s
|
|
34
|
+
Content Security Policy guide describes; `NonceProvider`'s `nonce`
|
|
35
|
+
becomes optional on the client. Ships as minors.
|
|
21
36
|
- **The shared media player** — the player state machine (playback, queue and
|
|
22
37
|
shuffle, chapters, transcript sync, resume position, quality levels, Wake
|
|
23
38
|
Lock), its keyboard shortcuts and the shortcut-help list, so
|
|
@@ -46,6 +61,8 @@ major they target.
|
|
|
46
61
|
|
|
47
62
|
## Not planned
|
|
48
63
|
|
|
64
|
+
- **A Next.js plugin** — the plugin targets Vite only. No supported consumer
|
|
65
|
+
builds with Next.js, and `withNxgtPrint` stays the config helper for it.
|
|
49
66
|
- **Components.** This package stays framework-free: React components are in
|
|
50
67
|
`@nxgt/material`, Vue ones in `@nxgt/material-vue`.
|
|
51
68
|
- **A fake DOM for tests.** The one the libraries' own tests use is the
|
|
@@ -59,6 +76,22 @@ major they target.
|
|
|
59
76
|
|
|
60
77
|
## Shipped
|
|
61
78
|
|
|
79
|
+
- **Two-step verification helpers and printing one element** (`/two-factor`,
|
|
80
|
+
`/print-only`) — a TOTP secret grouped to be typed by hand, the recovery
|
|
81
|
+
codes as a text file, saving a text file, and printing one element of the
|
|
82
|
+
page with no inline style, behind both libraries' `EnrolmentSecret` and
|
|
83
|
+
`RecoveryCodes`. 0.10.0.
|
|
84
|
+
- **One submit in flight and errors worded by code** (`/submit-guard`,
|
|
85
|
+
`/code-errors`) — the guard behind both libraries' `useSubmitOnce`, and the
|
|
86
|
+
zod error map behind their `useCodeResolver`, which words a refinement's
|
|
87
|
+
`params.code` in the app's words and leaves the rest to a fallback. 0.10.0.
|
|
88
|
+
- **The page's Content Security Policy nonce** (`/csp-nonce`) — one place both
|
|
89
|
+
libraries read the nonce from, so every `<style>` they create carries it
|
|
90
|
+
under a strict `style-src-elem`, and `cspAssetsInlineLimit` (`/vite`), which
|
|
91
|
+
keeps fonts out of `data:` URIs. 0.9.0.
|
|
92
|
+
- **Built on first use** (`/lazy-object`) — `buildOnce` and `lazyObject`, with
|
|
93
|
+
which both libraries build their zod schemas on first use rather than at
|
|
94
|
+
import, so an app under a strict CSP can configure zod first. 0.9.0.
|
|
62
95
|
- **Debounce, text matching and the async search** (`/debounce`,
|
|
63
96
|
`/text-match`, `/async-search`) — the debounce both libraries'
|
|
64
97
|
`SearchField` kept a copy of each, accent- and case-insensitive matching,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/material-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the page's Content Security Policy nonce, an object built on first use, the zod issue messages, the date range presets, the debounce, accent-insensitive text matching and the race-free async search behind Autocomplete and SearchField, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin, a CSP-safe Vite assetsInlineLimit and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the page's Content Security Policy nonce, an object built on first use, the zod issue messages, the date range presets, the debounce, accent-insensitive text matching and the race-free async search behind Autocomplete and SearchField, the submit guard, the code error map, the two-factor helpers (a grouped TOTP secret, the recovery codes' file, a text download), printing one element of the page, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin, a CSP-safe Vite assetsInlineLimit and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"email": "stevegires@gmail.com"
|
|
@@ -183,6 +183,26 @@
|
|
|
183
183
|
"import": "./dist/lib/async-search.js",
|
|
184
184
|
"default": "./dist/lib/async-search.js"
|
|
185
185
|
},
|
|
186
|
+
"./submit-guard": {
|
|
187
|
+
"types": "./dist/lib/submit-guard.d.ts",
|
|
188
|
+
"import": "./dist/lib/submit-guard.js",
|
|
189
|
+
"default": "./dist/lib/submit-guard.js"
|
|
190
|
+
},
|
|
191
|
+
"./code-errors": {
|
|
192
|
+
"types": "./dist/lib/code-errors.d.ts",
|
|
193
|
+
"import": "./dist/lib/code-errors.js",
|
|
194
|
+
"default": "./dist/lib/code-errors.js"
|
|
195
|
+
},
|
|
196
|
+
"./print-only": {
|
|
197
|
+
"types": "./dist/lib/print-only.d.ts",
|
|
198
|
+
"import": "./dist/lib/print-only.js",
|
|
199
|
+
"default": "./dist/lib/print-only.js"
|
|
200
|
+
},
|
|
201
|
+
"./two-factor": {
|
|
202
|
+
"types": "./dist/lib/two-factor.d.ts",
|
|
203
|
+
"import": "./dist/lib/two-factor.js",
|
|
204
|
+
"default": "./dist/lib/two-factor.js"
|
|
205
|
+
},
|
|
186
206
|
"./file-preview": {
|
|
187
207
|
"types": "./dist/file-preview/index.d.ts",
|
|
188
208
|
"import": "./dist/file-preview/index.js",
|
|
@@ -264,6 +284,10 @@
|
|
|
264
284
|
"src/lib/debounce.ts",
|
|
265
285
|
"src/lib/text-match.ts",
|
|
266
286
|
"src/lib/async-search.ts",
|
|
287
|
+
"src/lib/submit-guard.ts",
|
|
288
|
+
"src/lib/code-errors.ts",
|
|
289
|
+
"src/lib/two-factor.ts",
|
|
290
|
+
"src/lib/print-only.ts",
|
|
267
291
|
"src/file-preview/index.ts",
|
|
268
292
|
"src/pdf-viewer/index.ts",
|
|
269
293
|
"src/vite/index.ts",
|
|
@@ -1,10 +0,0 @@
|
|
|
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';\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 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 revokeLater(url: string) {\n\twindow.setTimeout(() => URL.revokeObjectURL(url), OBJECT_URL_TTL_MS);\n}\n\nfunction 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. */\nfunction saveBlob(blob: Blob, fileName: string) {\n\tconst url = URL.createObjectURL(blob);\n\tclickLink(url, { download: fileName });\n\trevokeLater(url);\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": ";;;;;;;;;;;AAcO,IAAM,qBAAoB;AAsBjC,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,WAAW,CAAC,KAAa;AAAA,EACjC,OAAO,WAAW,MAAM,IAAI,gBAAgB,GAAG,GAAG,kBAAiB;AAAA;AAGpE,SAAS,SAAS,CAAC,MAAc,YAAoC;AAAA,EACpE,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;AAIb,SAAS,QAAQ,CAAC,MAAY,UAAkB;AAAA,EAC/C,MAAM,MAAM,IAAI,gBAAgB,IAAI;AAAA,EACpC,UAAU,KAAK,EAAE,UAAU,SAAS,CAAC;AAAA,EACrC,YAAY,GAAG;AAAA;AAGhB,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": "77B6E1C9EF4463B464756E2164756E21",
|
|
9
|
-
"names": []
|
|
10
|
-
}
|