@nxgt/material 3.96.2 → 3.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +2 -2
  3. package/dist/calculator-field.js +2 -2
  4. package/dist/{calculator-BVuQG9PG.js → calculator-kdSjeRhf.js} +1 -1
  5. package/dist/calculator.js +1 -1
  6. package/dist/{components-B125euHt.js → components-3rMGln0P.js} +11597 -11442
  7. package/dist/components.js +34 -34
  8. package/dist/{currency-field-Dj1mBZ9n.js → currency-field-5Zg_sjY1.js} +91 -57
  9. package/dist/editor.js +1 -1
  10. package/dist/{financial-variant-DnHWDZ8t.js → financial-variant-DR7pCGqi.js} +2 -2
  11. package/dist/lib/components/ui/collections/filter/use-filter-field-config.d.ts +1 -0
  12. package/dist/lib/components/ui/collections/row-menu/use-row-menu-focus.d.ts +4 -2
  13. package/dist/lib/components/ui/display/event-chip/event-chip-badges.d.ts +2 -1
  14. package/dist/lib/components/ui/display/event-chip/event-chip-content.d.ts +2 -0
  15. package/dist/lib/components/ui/display/event-chip/event-chip.d.ts +3 -1
  16. package/dist/lib/components/ui/fields/currency-field/currency-field.d.ts +9 -1
  17. package/dist/lib/components/ui/fields/currency-field/use-currency-amount.d.ts +3 -1
  18. package/dist/lib/components/ui/fields/currency-field/use-minor-units.d.ts +9 -0
  19. package/dist/lib/components/ui/fields/repeatable-group/partials/repeatable-group-heading.d.ts +12 -0
  20. package/dist/lib/components/ui/fields/repeatable-group/repeatable-group.d.ts +1 -1
  21. package/dist/lib/components/ui/fields/repeatable-group/types.d.ts +5 -0
  22. package/dist/lib/components/ui/fields/repeatable-group/use-repeatable-group-text.d.ts +19 -0
  23. package/dist/lib/components/ui/fields/textarea-field/textarea-field.d.ts +3 -1
  24. package/dist/lib/components/ui/media/image-cropper/crop-image.d.ts +15 -2
  25. package/dist/lib/components/ui/media/image-cropper/image-cropper.d.ts +9 -4
  26. package/dist/lib/components/ui/media/image-cropper/index.d.ts +3 -2
  27. package/dist/lib/components/ui/media/image-cropper/types.d.ts +9 -0
  28. package/dist/lib/components/ui/media/image-cropper/use-apply-crop.d.ts +16 -0
  29. package/dist/lib/components/ui/media/image-cropper/use-image-cropper.d.ts +5 -5
  30. package/dist/lib/components/ui/widgets/education/questions/partials/content-lang.d.ts +14 -0
  31. package/dist/lib/components/ui/widgets/education/questions/partials/question-content.d.ts +1 -1
  32. package/dist/lib/components/ui/widgets/education/questions/question-props.types.d.ts +87 -0
  33. package/dist/lib/components/ui/widgets/education/questions/question.d.ts +1 -1
  34. package/dist/lib/components/ui/widgets/education/questions/types.d.ts +1 -74
  35. package/dist/lib/components/ui/widgets/event-calendar/partials/calendar-event-chip.d.ts +3 -1
  36. package/dist/main.js +50 -50
  37. package/dist/material.css +1 -1
  38. package/dist/{pdf-MkmivNXn.js → pdf-9BAZhzg9.js} +9777 -7726
  39. package/dist/pdf-reader.js +1 -1
  40. package/dist/pdf.worker.min.mjs +5 -5
  41. package/dist/{percent-field-DT4qNaCH.js → percent-field-D1Y4HUPt.js} +1 -1
  42. package/dist/{public-BxLp7Qn8.js → public-BuRnxJOK.js} +2 -2
  43. package/dist/{use-chat-composer-U7jZfdQN.js → use-chat-composer-4cs6opDX.js} +2092 -2085
  44. package/docs/components/data-display.md +3 -2
  45. package/docs/components/data-table-filter-and-lists.md +3 -1
  46. package/docs/components/form-fields.md +6 -2
  47. package/docs/components/media-and-files.md +40 -2
  48. package/docs/components/rich-widgets.md +13 -3
  49. package/docs/guides/csp.md +0 -1
  50. package/docs/guides/event-calendar.md +7 -2
  51. package/docs/guides/forms.md +1 -1
  52. package/docs/guides/i18n.md +1 -1
  53. package/docs/guides/pdf-reader.md +13 -2
  54. package/docs/troubleshooting.md +7 -0
  55. package/package.json +3 -3
@@ -1632,8 +1632,9 @@ import { EventChip } from '@nxgt/material/components';
1632
1632
  | `allDay` | `boolean` | `false` | no time |
1633
1633
  | `compact` | `boolean` | `false` | one line, the time before the title, for month cells |
1634
1634
  | `continuesBefore`, `continuesAfter` | `boolean` | `false` | square edges for a multi-day event |
1635
- | `badge` | `{ label: string; tone?: Tone }` | — | a small tinted label after the title (neutral without a `tone`); cut with an ellipsis; in a box under 20px tall or 7rem wide it becomes a tone dot (its tooltip is the label) while the label stays in the chip's accessible name. `EventCalendar` sets it from `CalendarEvent.badge` |
1636
- | `badges` | `{ label: string; tone?: Tone }[]` | — | further badges after `badge`, each with the same pill and tone-dot behaviour and each in the accessible name. `EventCalendar` sets it from `CalendarEvent.badges` |
1635
+ | `short` | `boolean` | `false` | the box is too short (under 60px) for title, time and a badge row: the badges join the time's line, as a tone dot under 10rem wide. `EventCalendar` sets it in the week and day views |
1636
+ | `badge` | `{ label: string; tone?: Tone }` | — | a small tinted label after the title (neutral without a `tone`); cut with an ellipsis; in a box under 60px tall (`short`) it joins the time's line, and under 10rem wide there it becomes a tone dot; in a box under 20px tall or 7rem wide it is a tone dot too (its tooltip is the label) while the label stays in the chip's accessible name. `EventCalendar` sets it from `CalendarEvent.badge` |
1637
+ | `badges` | `{ label: string; tone?: Tone }[]` | — | further badges after `badge`, each with the same time-line placement, pill and tone-dot behaviour and each in the accessible name. `EventCalendar` sets it from `CalendarEvent.badges` |
1637
1638
  | `tone` | `Tone` | — | draws the chip in a tone of the library (or one your app declared) when it sets no `color`; a `color` wins |
1638
1639
  | `cancelled` | `boolean` | `false` | a muted chip with a dashed outline, the title struck through (`<del>`) and "Cancelled" in its accessible name (`material.event-chip.cancelled`, English and French) |
1639
1640
  | `selected`, `disabled` | `boolean` | `false` | state |
@@ -1334,7 +1334,9 @@ a toast takes it, so it is safe to call right after the action settles or the
1334
1334
  dialog closes. For those two frames the focus is claimed back to the trigger
1335
1335
  whatever took it, so a deliberate focus move (Tab, or a dialog or toast opened
1336
1336
  right after the call) within them is overridden: call it once the follow-up UI
1337
- is done. It returns `true` when the button was found and holds the focus,
1337
+ is done. The reclaim stops when the component unmounts (a navigation right
1338
+ after the call is safe) or when the `focusRef` element is disconnected. It
1339
+ returns `true` when the button was found and holds the focus,
1338
1340
  and `false` when the row is gone (archived) or the button cannot take it
1339
1341
  (disabled, inert); the focus then stays where it is.
1340
1342
 
@@ -218,6 +218,7 @@ export function PlanForm({ onSave }: { onSave: (values: Values) => void }) {
218
218
  control={control}
219
219
  name="periods"
220
220
  label="Periods"
221
+ description="One period per term."
221
222
  rowLabel="period"
222
223
  addLabel="Add period"
223
224
  max={6}
@@ -247,6 +248,7 @@ export function PlanForm({ onSave }: { onSave: (values: Values) => void }) {
247
248
  | `newRow` | `() => FieldArray<T, N>` | — | the value Add appends |
248
249
  | `renderRow` | `(row) => ReactNode` | — | one row's fields; `row` is `{ field, index, position, path }` (`index` 0-based, `position` 1-based, `path` `${name}.${index}`) |
249
250
  | `label` | `ReactNode` | — | names the group (the list inside it is not named again) |
251
+ | `description` | `ReactNode` | — | help text under the label; the group's `aria-describedby` points at it (with the group error), so it is announced with the group |
250
252
  | `rowLabel` | `string` | `'row'` (translated) | names each row's buttons ("Remove period 2") and the announcements |
251
253
  | `addLabel` | `string` | `'Add {rowLabel}'` | the Add button's text |
252
254
  | `reorderable` | `boolean` | `false` | move up / move down buttons on each row |
@@ -521,6 +523,7 @@ import { TextareaField } from '@nxgt/material/components';
521
523
  | `maxRows` | `number` | — | maximum height in lines, then it scrolls |
522
524
  | `maxLength` | `number` | — | shows an `n/max` counter, red when over |
523
525
  | `showCount` | `boolean` | `true` when `maxLength` is set | force the counter on or off |
526
+ | `monospace` | `boolean` | `false` | React only; `TextareaFormField` takes it too. Sets the theme's monospace font (`--font-mono`) on the textarea, for code-like plain text such as test input and expected output |
524
527
  | `error` | `boolean` | — | error outline and helper colour |
525
528
 
526
529
  **Emits:** the native `ChangeEvent<HTMLTextAreaElement>`; `TextareaFormField`
@@ -857,11 +860,12 @@ export function Amount() {
857
860
  | `currency` | `string` | `'EUR'` | the suffix text; formatting follows the `I18nProvider` language |
858
861
  | `trailingAction` | `ReactNode` | — | a button after the currency code, inside the field (`CalculatorField`'s "Open calculator" uses it); it keeps its own name and focus, and the input leaves room for it |
859
862
  | `fractionDigits` | `number` | the currency's own | decimals shown once the amount is committed (on blur): ISO 4217 by default — `JPY` 0, `EUR` 2, `KWD` 3 (2 for a code `Intl` does not know). Pass `2` to keep two decimals whatever the currency; the value is clamped to 0–20 (the range every `Intl` accepts), and `NaN` or `Infinity` counts as not given. What is typed is not rounded, on screen until blur nor in the emitted number: a `JPY` field shows `1,235` for `1234.5` and emits `1234.5` |
863
+ | `minorUnits` | `boolean` | `false` | `value`, `defaultValue` and `onChange` are whole minor units (cents, centimes, yen) instead of major units: `<CurrencyField currency="EUR" minorUnits value={1999} />` shows `19.99` and emits `1999`. The decimals are the currency's own (`EUR` 2, `XAF` 0, `KWD` 3), or `fractionDigits` when given, the same rule as `formatAmount`'s `minorUnits` option; an amount typed with more decimals is rounded half away from zero to a whole minor unit (`1.005` emits `101`). Also a prop of `CurrencyFormField`, whose form value is then the integer |
860
864
 
861
- **Emits:** `number | null` — never the formatted string.
865
+ **Emits:** `number | null` (major units, whole minor units with `minorUnits`) — never the formatted string.
862
866
  **Form wrapper:** `CurrencyFormField` — `control` and `name`, see
863
867
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
864
- **Storybook:** `Form fields/CurrencyField` · **Vue:** `CurrencyField`, `CurrencyFormField`
868
+ **Storybook:** `Form fields/CurrencyField` · **Vue:** `CurrencyField`, `CurrencyFormField` (`minorUnits` is React only for now, #2772)
865
869
 
866
870
  ---
867
871
 
@@ -484,19 +484,57 @@ import { Button, ImageCropper, ImageCropperDialog } from '@nxgt/material/compone
484
484
  ```
485
485
 
486
486
  Parts: `ImageCropper` (inline), `ImageCropperDialog` (the same in a dialog, closed
487
- after `onCrop`).
487
+ after `onCrop`, or after its promise resolves).
488
488
 
489
489
  | Prop | Type | Default | Effect |
490
490
  | --- | --- | --- | --- |
491
491
  | `src` | `string` | — | image to crop (URL or object URL) |
492
- | `onCrop` | `({ file, area }) => void` | — | receives the cropped `File` and the pixel area |
492
+ | `onCrop` | `({ file, area }) => void \| Promise<void>` | — | receives the cropped `File` and the pixel area. Return a promise (an upload) and Apply stays busy (`aria-busy`, disabled) and Cancel is disabled until it settles; a rejection keeps the crop (and the dialog open) so the user can apply again, and the error is yours to show |
493
493
  | `aspect` | `number` | — | fixes the ratio and hides the presets; omitted, the user picks free / 1:1 / 4:3 / 16:9 / 3:4 |
494
494
  | `cropShape` | `'rect' \| 'round'` | `'rect'` | round mask for avatars |
495
495
  | `fileName` | `string` | — | name of the produced `File` |
496
+ | `outputSize` | `number` | the crop's size | longest side of the file in px (512, say); downscales, never upscales |
497
+ | `mimeType` | `string` | `'image/jpeg'` | encoding of the file (`'image/webp'`, `'image/png'`, `'image/avif'`); the default file extension follows it (png, webp, avif, otherwise jpg) |
498
+ | `quality` | `number` | `0.92` | 0 to 1, for the lossy types; ignored for `image/png` |
496
499
  | `onCancel` | `() => void` | — | `ImageCropper`: shows a Cancel button |
497
500
  | `cancelVariant` | `ButtonVariant` | `'tonal'` | `ImageCropper`: the Cancel button's variant; `'ghost'` restores the earlier look |
498
501
  | `open` / `onOpenChange` / `trigger` | | — | `ImageCropperDialog`: controlled or trigger-opened |
499
502
 
503
+ Without `outputSize` a 3000 px crop is written at 3000 px. For an avatar, cap it
504
+ and pick a lighter type; `cropImageToFile(src, area, options)` takes the same three options:
505
+
506
+ ```tsx
507
+ <ImageCropperDialog src={src} aspect={1} outputSize={512} mimeType="image/webp" quality={0.85} onCrop={upload} />
508
+ ```
509
+
510
+ The helper on its own:
511
+
512
+ ```ts
513
+ import { cropImageToFile, fitOutputSize, type CropImageOptions } from '@nxgt/material/components';
514
+
515
+ const file = await cropImageToFile(src, pixelCrop, { outputSize: 512, mimeType: 'image/webp', quality: 0.85 });
516
+ ```
517
+
518
+ `fitOutputSize(width, height, outputSize)` returns the capped `{ width, height }`, and `ImageCropOutput` is the type of the three options.
519
+
520
+ A rejection from `onCrop` is swallowed: the dialog stays open and Apply is usable
521
+ again, but nothing is shown. Handle the error inside `onCrop` (try/catch, then a
522
+ toast), and rethrow it so the dialog stays open:
523
+
524
+ ```tsx
525
+ <ImageCropperDialog
526
+ src={src}
527
+ onCrop={async ({ file }) => {
528
+ try {
529
+ await uploadAvatar(file);
530
+ } catch (error) {
531
+ toast.error('Upload failed, try again');
532
+ throw error; // keeps the dialog open
533
+ }
534
+ }}
535
+ />
536
+ ```
537
+
500
538
  **Storybook:** `Media & files/ImageCropper` · **Vue:** `ImageCropper`, `ImageCropperDialog`
501
539
 
502
540
  ---
@@ -400,7 +400,10 @@ const events: CalendarEvent[] = [
400
400
  - **Small boxes.** A long label is cut with an ellipsis inside its chip. In a
401
401
  box under 20px tall or 7rem wide (a phone's week column) it falls back to a
402
402
  small dot in the badge's tone, with the label as its tooltip; the label stays
403
- in the event's accessible name. The agenda view wraps a long description
403
+ in the event's accessible name. In a week or day box under 60px tall (a
404
+ one-hour event at the default slot height) the badge sits on the time's line
405
+ instead of a row of its own, and becomes the dot under 10rem of chip width.
406
+ The agenda view wraps a long description
404
407
  instead of cutting it.
405
408
 
406
409
  For a status and a priority together, add `badges` (`CalendarEventBadge[]`):
@@ -2828,7 +2831,7 @@ French bundle — see [i18n](../guides/i18n.md).
2828
2831
  | `QuestionCard` `value` / `onValueChange` | `QuestionAnswer` | — | controlled answer |
2829
2832
  | `Question` `kind: 'open'` | `maxLength?` | — | a multi-line answer in a textarea named by the prompt; a counter shows while editable when `maxLength` is set. No `correct`: pass the grader's `verdict` |
2830
2833
  | `Question` `kind: 'code'` | `language?`, `maxLength?` | — | source code: drawn by `renderCode`, else a monospace textarea |
2831
- | `QuestionCard` `renderCode` | `(field: QuestionCodeField) => ReactNode` | — | the code field of a `code` question; `field` is `{ value, onChange, label, labelledBy, describedBy, language, maxLength, readOnly, disabled }`. Keeps Monaco out of the card's bundle |
2834
+ | `QuestionCard` `renderCode` | `(field: QuestionCodeField) => ReactNode` | — | the code field of a `code` question; `field` is `{ value, onChange, label, labelledBy, describedBy, language, maxLength, readOnly, disabled, lang }`. Keeps Monaco out of the card's bundle |
2832
2835
  | `Question` `description` | `string` | — | `open`, `code`: a persistent helper line under the answer field ("A person will mark this answer"), linked to it by `aria-describedby` and passed to `renderCode` as `field.describedBy`; always shown, unlike `hint` (a reveal button). Its id is `${promptId}-description` |
2833
2836
  | `QuestionCard` `promptId` | `string` | generated | the id of the prompt text: point a wrapping section's `aria-labelledby` at it (and `aria-describedby` at `${promptId}-description`) |
2834
2837
  | `QuestionCard` `preview` | `boolean` | `false` | an author's preview, `<QuestionCard question={q} preview />`: forces `revealed` and `readOnly`, drops `onValueChange`, and shows the marks (the `question.correct` options marked, the expected text or number filled in) and the `explanation`, with no verdict banner. `verdict` and `showFeedback` are always ignored in a preview; `value` is ignored when the question has a `correct` — an `open` or `code` question has none, so pass its reference answer as `value`. Not for a learner's attempt |
@@ -2840,8 +2843,15 @@ French bundle — see [i18n](../guides/i18n.md).
2840
2843
  | `Question` `references` | `{ title: string; url?: string }[]` | — | a labelled "References" list once revealed or in a `preview`; a safe `url` is a link, an external one with `rel="noopener noreferrer"` |
2841
2844
  | `Question` `kind: 'code'` | `starterCode?`, `examples?: { input, expected }[]` | — | `starterCode` fills the field while there is no draft (`value` null or undefined; an empty string is a draft), it reaches `onValueChange` on the first edit only; `examples` is a table (Input, Expected) above the field |
2842
2845
  | new strings | `material.quiz.rich.*` | — | English fallbacks and a French bundle (React only for now) |
2846
+ | `QuestionCard` `contentLang` | `string` | — | the language of the question's content (a BCP 47 tag, `"en"`): sets `lang` on the prompt, the options (and their descriptions), the hints, the explanation, the option explanations, the references, the code examples and the starter code (and `lang` on the `renderCode` field). The card's own strings (hint button, kind and number labels, headings, notes) keep the page's language, which a `lang` on a wrapper around the card would also change. A grader's `feedback` and the typed answer keep the page's language (the code textarea takes the `lang` for the starter code). Without it no `lang` is set. Applies wherever the card renders: the player, a results review (`revealed`, `verdict`) and an author `preview` |
2843
2847
  | `QuestionCard` `variant` / `density` / `showShortcuts` / `number` / `showFeedback` | — | `'card'`, `'comfortable'`, `false`, —, `true` | framed or plain, spacing, key badges, "Question 2", the verdict banner (for a card with marks and no banner, use `preview`) |
2844
2848
 
2849
+ An English quiz in a French interface: `contentLang` labels the content only, the card's own strings stay in the page's language (set it only when the content differs from the page; `Quiz` does not forward it):
2850
+
2851
+ ```tsx
2852
+ <QuestionCard question={question} contentLang="en" />
2853
+ ```
2854
+
2845
2855
  A quiz question with Markdown, math, progressive hints and references:
2846
2856
 
2847
2857
  ```tsx
@@ -2908,7 +2918,7 @@ A server-graded answer, shown as given (no comparison in the browser):
2908
2918
  />
2909
2919
  ```
2910
2920
 
2911
- **Storybook:** `Rich widgets/Quiz`, `Rich widgets/QuestionCard` · **Vue:** `Quiz`, `QuestionCard` (the modes, results, number kind and shortcuts are React only for now, #890)
2921
+ **Storybook:** `Rich widgets/Quiz`, `Rich widgets/QuestionCard` · **Vue:** `Quiz`, `QuestionCard` (the modes, results, number kind, shortcuts and `contentLang` are React only for now, #890)
2912
2922
 
2913
2923
  ### QuestionMediaView
2914
2924
 
@@ -240,7 +240,6 @@ library avoids:
240
240
  | --- | --- | --- |
241
241
  | `Resizable` (react-resizable-panels) | `insertRule` | on a constructed stylesheet (`adoptedStyleSheets`), which CSP does not govern |
242
242
  | `ImageCropper` (react-easy-crop) | `createElement('style')` | the branch is skipped: `disableAutomaticStylesInjection` is passed |
243
- | `PdfReader` (pdf.js), both readers | the `isEvalSupported` probe, `new Function("")` | unreachable: `PdfReader` passes `isEvalSupported: false`. If you call `pdfjs.getDocument` yourself, pass it too |
244
243
  | `MermaidDiagram` (mermaid's lodash-es, cytoscape and parser) | `Function("return this")()` | only when neither `global` nor `self` exists: never in a browser |
245
244
  | `InputOTP` (input-otp) | a `<style>` inside `<noscript>` | rendered only with scripts off, where no CSP report is sent |
246
245
 
@@ -100,6 +100,11 @@ A cancelled event strikes its title (`<del>`) and adds "Cancelled" to its
100
100
  accessible name, so the state is never carried by style alone. The preview
101
101
  shows such an event's title struck and named the same way (and read-only, since
102
102
  its title is not an input then). `tone` and
103
+ A short event (under 60px tall, such as one hour at the default slot height) in
104
+ the week and day views draws its badges on the time's line instead of a row of
105
+ its own, and a tone dot under 10rem of chip width; the label stays in the
106
+ accessible name and tooltip.
107
+
103
108
  `badges` work in every view and in `mode="template"`. `EventChip` takes the same
104
109
  `tone`, `badges` and `cancelled`.
105
110
 
@@ -124,8 +129,8 @@ read-only one leaves out. The preview shows no Edit or Delete button, so
124
129
  | View | Spans | Day badge | Week badge | Event tone and cancelled |
125
130
  | --- | --- | --- | --- | --- |
126
131
  | Month | shades the cell, or its half for a half day; the label shows from 7rem of cell width, with a dot under it | next to the day number | on the first cell of the row, with room below its events | chip tone; struck title, dashed outline |
127
- | Week / workweek | hatched band under the hours with its label, and a bar in the all-day strip (a half day only the band) | in the day's header | in the gutter corner | block tone; struck title, dashed outline |
128
- | Day | the same as the week; with `resources`, a span shades its own column | in the day's header | in the gutter corner | the same |
132
+ | Week / workweek | hatched band under the hours with its label, and a bar in the all-day strip (a half day only the band) | in the day's header | in the gutter corner | block tone; struck title, dashed outline; a short block keeps its badge on the time line |
133
+ | Day | the same as the week; with `resources`, a span shades its own column | in the day's header | in the gutter corner | the same, badge on the time line in a short block |
129
134
  | Agenda, phone day list | a plain, non-clickable row | in the day's heading | in the week's heading | tone dot; struck title, "Cancelled" in words |
130
135
 
131
136
  ## weekStartsOn
@@ -496,7 +496,7 @@ or fails validation.
496
496
  | `NumberField` · `NumberFormField` | `number \| null`. `null` when cleared; clamped to `min` / `max` on blur | `null` |
497
497
  | `PercentField` · `PercentFormField` | `number \| null`, `min` 0, `max` 100, `step` 1 by default | `null` |
498
498
  | `QuantityField` · `QuantityFormField` | `{ value: number \| null, unit: string }`. An error on the field, `.value` or `.unit` shows as one message under the pair | `{ value: null, unit: '' }` |
499
- | `CurrencyField` · `CurrencyFormField` | `number \| null`, the amount only, not rounded. `currency` (default `'EUR'`) and `fractionDigits` (the currency's own decimals by default) are display | `null` |
499
+ | `CurrencyField` · `CurrencyFormField` | `number \| null`, the amount only, not rounded; with `minorUnits`, a whole number of minor units (`1999` for `19.99` EUR, the digits of the currency or `fractionDigits`). `currency` (default `'EUR'`) and `fractionDigits` (the currency's own decimals by default) are display | `null` |
500
500
  | `CalculatorField` · `CalculatorFormField` (`@nxgt/material/calculator-field`) | `number \| null`. `null` when empty; what Apply writes (a currency amount rounded half-up to its decimals, held to `min` / `max`). `onExactValueChange` gets the exact decimal string | `null` |
501
501
  | `PhoneField` · `PhoneFormField` | E.164 `string` (`'+33612345678'`), or `null` when empty. `PhoneFormField` reports a non-empty number that is not yet possible as the field error `material.phone-field.incomplete`; `PhoneField` signals it with `onValidityChange`. `defaultCountry` is `'FR'` | `null` |
502
502
  | `UrlField` · `UrlFormField` | `string` (as typed) | `''` |
@@ -247,7 +247,7 @@ until the bar is drawn), the `ThreadLayout` stepper's `nav` name `material.navig
247
247
  single value's input, when nothing names the slider, `material.slider.value`
248
248
  ("Value", "Valeur"). `CopyButton` reads `material.copy.copy`, `copied` and `failed` ("Copy", "Copied",
249
249
  "Copy failed"). An overflowing `TabsList`'s scroll buttons read
250
- `material.tabs.scroll-left` / `scroll-right` ("Scroll tabs left" / "Scroll tabs right"). `QuestionCard`'s rich content reads `material.quiz.rich.*` (the hint button and names "Show hint 1 of 3" / "Hint 1 of 3", "About each answer", "References", "Examples" with its Input / Expected headers). The other icon-only buttons are named from it too: the
250
+ `material.tabs.scroll-left` / `scroll-right` ("Scroll tabs left" / "Scroll tabs right"). `QuestionCard`'s own strings follow the UI language while a quiz in another language sets `contentLang` for its content (see the `QuestionCard` table in `components/rich-widgets.md`). Its rich content reads `material.quiz.rich.*` (the hint button and names "Show hint 1 of 3" / "Hint 1 of 3", "About each answer", "References", "Examples" with its Input / Expected headers). The other icon-only buttons are named from it too: the
251
251
  `Sidebar` toggle (`material.sidebar.toggle`), the data table's row expander
252
252
  (`material.table.labels.toggle-row`) and its header, a hidden "Expand" / "Déplier"
253
253
  (`material.table.labels.expand`), its loading status, "Loading data…" /
@@ -16,11 +16,22 @@ import { PdfReader } from '@nxgt/material/pdf-reader';
16
16
  </div>
17
17
  ```
18
18
 
19
+ - [Browser support](#browser-support)
19
20
  - [Setup](#setup)
20
21
  - [Server rendering](#server-rendering)
21
22
  - [Content-Security-Policy](#content-security-policy)
22
23
  - [Moving from the classic reader](#moving-from-the-classic-reader)
23
24
 
25
+ ## Browser support
26
+
27
+ The reader runs on pdf.js 6, which needs Chrome 125, Safari 18 and a current
28
+ Firefox ESR (the target pdf.js 6 keeps; the features it needs are in Firefox
29
+ 124 and later): `AbortSignal.any()`, `:dir()`, `light-dark()` and CSS nesting.
30
+ An older browser fails to open the reader, see
31
+ [troubleshooting](../troubleshooting.md#the-pdf-reader-does-not-open-in-an-older-browser).
32
+ The floor is the PDF reader's only; the rest of the library keeps its own. The
33
+ build and the tests of an app that bundles pdf.js need Node 22.13 or later.
34
+
24
35
  ## Setup
25
36
 
26
37
  The pdf.js **worker** ships with the package as a file, with nothing to copy.
@@ -158,8 +169,8 @@ policy of the response that serves its script). Without it:
158
169
  little, text and layout are unaffected.
159
170
 
160
171
  `'wasm-unsafe-eval'` allows WebAssembly only; it does not allow `eval()` or
161
- `new Function()`. The reader opens documents with `isEvalSupported: false`, so
162
- it needs neither. The full policy and the rest of the library:
172
+ `new Function()`. pdf.js 6 no longer probes for `eval`, so the reader needs
173
+ neither. The full policy and the rest of the library:
163
174
  [CSP](csp.md).
164
175
 
165
176
  ## Moving from the classic reader
@@ -199,6 +199,7 @@ words in your error.
199
199
  - [A chart does not announce a point when I hover it](#a-chart-does-not-announce-a-point-when-i-hover-it)
200
200
  - [The server HTML holds a client-render marker (`data-dgst`)](#the-server-html-holds-a-client-render-marker-data-dgst)
201
201
  - [The PDF reader is empty in the server HTML, then appears](#the-pdf-reader-is-empty-in-the-server-html-then-appears)
202
+ - [The PDF reader does not open in an older browser](#the-pdf-reader-does-not-open-in-an-older-browser)
202
203
 
203
204
  **Printing**
204
205
 
@@ -3590,6 +3591,12 @@ The marker is `<!--$!--><template data-dgst=""></template><!--/$-->`.
3590
3591
  **Why:** the reader is client-only by design. pdf.js needs the browser, so on the server it leaves only a client-render marker (`<!--$!--><template data-dgst=""></template><!--/$-->`) and React renders it in the browser. This is not a hydration mismatch; see [the client-render marker](#the-server-html-holds-a-client-render-marker-data-dgst).
3591
3592
  **Fix:** nothing to fix. Reserve the reader's height in your layout so the page does not jump when it appears, and do not import `pdfjs-dist` in your own server code. See the [SSR section of the guide](./guides/pdf-reader.md#server-rendering).
3592
3593
 
3594
+ ### The PDF reader does not open in an older browser
3595
+
3596
+ **When:** `PdfReader` (or the PDF viewer) shows its error state, or stays blank, on Chrome older than 125, Safari older than 18, or an old Firefox, with an error such as `AbortSignal.any is not a function` in the console.
3597
+ **Why:** pdf.js 6 dropped the older browsers and relies on `AbortSignal.any()`, `:dir()`, `light-dark()` and CSS nesting. Only the PDF reader has this floor; the rest of the library is unchanged.
3598
+ **Fix:** ask for Chrome 125, Safari 18 or a current Firefox ESR, or offer a download link next to the reader for older browsers. A build or test run that bundles pdf.js also needs Node 22.13 or later. See [browser support](./guides/pdf-reader.md#browser-support).
3599
+
3593
3600
  ## Printing
3594
3601
 
3595
3602
  ### `RecoveryCodes`' Print button prints the whole page
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.96.2",
3
+ "version": "3.98.0",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"
@@ -390,7 +390,7 @@
390
390
  "docs:previews": "rm -rf node_modules/.cache/nxgt-preview-free/missing && NXGT_PREVIEW_FREE=1 vitest run --project previews && bun .storybook/preview-free-report.ts"
391
391
  },
392
392
  "dependencies": {
393
- "@nxgt/material-core": "^0.55.0",
393
+ "@nxgt/material-core": "^0.56.0",
394
394
  "@base-ui/react": "^1.6.0",
395
395
  "@dnd-kit/core": "^6.3.1",
396
396
  "@dnd-kit/sortable": "^10.0.0",
@@ -451,7 +451,7 @@
451
451
  "lucide-react": "^1.26.0",
452
452
  "mermaid": "^12.0.0",
453
453
  "next-themes": "^0.4.6",
454
- "pdfjs-dist": "5.4.296",
454
+ "pdfjs-dist": "6.5.438",
455
455
  "qrcode-generator": "^2.0.4",
456
456
  "radix-ui": "^1.6.5",
457
457
  "react-barcode": "^1.6.1",