@nxgt/material 3.12.0 → 3.13.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 (100) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +24 -15
  3. package/dist/{async-slice-4qbER86I.js → async-slice-ChtlTP8W.js} +2 -2
  4. package/dist/{background-cEqcZQ3v.js → background-Ce7M5V42.js} +2 -1
  5. package/dist/backgrounds/css-react/icons-quiet.css +43 -0
  6. package/dist/backgrounds/css-react/icons.css +2 -0
  7. package/dist/backgrounds.js +1 -1
  8. package/dist/{button-BNjw-pnE.js → button-BrgyWw7T.js} +1 -1
  9. package/dist/charts.js +3 -3
  10. package/dist/{chip-DLgQTJBZ.js → chip-BZu-bvAJ.js} +1 -1
  11. package/dist/{code-editor-BZbaYLvo.js → code-editor-DjY7_8TH.js} +4 -4
  12. package/dist/code-editor.js +4 -4
  13. package/dist/{code-language-items-BT5BO5w6.js → code-language-items-D1lpNQ6J.js} +2 -2
  14. package/dist/components/components.css +2 -0
  15. package/dist/components/ui/display/avatar/avatar.css +17 -0
  16. package/dist/components/ui/feedback/alert/alert.css +16 -0
  17. package/dist/{components-yx1t_5_P.js → components-BQWoJgYR.js} +15131 -14980
  18. package/dist/components.js +26 -26
  19. package/dist/{dialog-BQp5IdFl.js → dialog-rOMjlKJ7.js} +1 -1
  20. package/dist/dnd.js +1 -1
  21. package/dist/draw-animate.js +1 -1
  22. package/dist/draw.js +1 -1
  23. package/dist/{drawing-B8VuG7nx.js → drawing-iP1SDjdL.js} +60 -54
  24. package/dist/{dropdown-menu-BsH9W_3C.js → dropdown-menu-BEyP-A0J.js} +1 -1
  25. package/dist/editor.js +527 -527
  26. package/dist/{empty-state-WaU4-T2c.js → empty-state-ZtqqpX0S.js} +1 -1
  27. package/dist/hooks.js +2 -2
  28. package/dist/{i18n-provider-C6HhBw34.js → i18n-provider-GV4Ukoe2.js} +1 -1
  29. package/dist/i18n.js +5 -5
  30. package/dist/{icon-badge-DUAq9f7H.js → icon-badge-_tY1MwU9.js} +1 -1
  31. package/dist/lib/backgrounds/background/types.d.ts +2 -1
  32. package/dist/lib/backgrounds/index.d.ts +1 -1
  33. package/dist/lib/components/ui/collections/data-table/numeric-class.d.ts +2 -0
  34. package/dist/lib/components/ui/collections/data-table/table-meta.d.ts +8 -3
  35. package/dist/lib/components/ui/display/entity-header/entity-header.d.ts +8 -2
  36. package/dist/lib/components/ui/display/entity-header/entity-stats.d.ts +1 -1
  37. package/dist/lib/components/ui/display/entity-header/partials/entity-header-body.d.ts +2 -1
  38. package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +43 -8
  39. package/dist/lib/components/ui/display/list-tile/partials/list-tile-body.d.ts +4 -1
  40. package/dist/lib/components/ui/display/list-tile/partials/list-tile-line.d.ts +3 -0
  41. package/dist/lib/components/ui/display/list-tile/partials/list-tile-toggle.d.ts +20 -0
  42. package/dist/lib/components/ui/display/stat-card/index.d.ts +2 -0
  43. package/dist/lib/components/ui/display/stat-card/stat-card-group.d.ts +12 -0
  44. package/dist/lib/components/ui/feedback/alert/alert-tone-icon.d.ts +9 -0
  45. package/dist/lib/components/ui/feedback/alert/alert.d.ts +4 -0
  46. package/dist/lib/components/ui/fields/form-section/form-section-heading-classes.d.ts +4 -0
  47. package/dist/lib/components/ui/fields/form-section/form-section.d.ts +9 -1
  48. package/dist/lib/components/ui/layout/list-details-layout/list-details-layout.d.ts +12 -2
  49. package/dist/lib/components/ui/layout/list-details-layout/partials/list-details-back.d.ts +16 -0
  50. package/dist/lib/components/ui/layout/list-details-layout/partials/list-details-details.d.ts +11 -3
  51. package/dist/lib/components/ui/layout/list-details-layout/partials/pane-opener.d.ts +15 -0
  52. package/dist/lib/components/ui/layout/list-details-layout/partials/use-container-narrow.d.ts +7 -0
  53. package/dist/lib/components/ui/layout/list-details-layout/partials/use-pane-focus.d.ts +13 -0
  54. package/dist/lib/components/ui/widgets/drawing/drawing-types.d.ts +6 -0
  55. package/dist/lib/components/ui/widgets/drawing/partials/drawing-root.d.ts +5 -1
  56. package/dist/lib/draw-animate/morph-drawing.d.ts +2 -8
  57. package/dist/lib/i18n/resources/en/en.d.ts +3 -0
  58. package/dist/lib/i18n/resources/en/material.json.d.ts +3 -0
  59. package/dist/lib/i18n/resources/fr/fr.d.ts +3 -0
  60. package/dist/lib/i18n/resources/fr/material.json.d.ts +3 -0
  61. package/dist/lib/i18n/resources/resources.d.ts +6 -0
  62. package/dist/lib/i18n/translator.d.ts +1 -1
  63. package/dist/lib/print/components/print-document/print-document.d.ts +12 -2
  64. package/dist/lib/print/index.d.ts +2 -1
  65. package/dist/lib.js +1 -1
  66. package/dist/main.js +43 -43
  67. package/dist/map.js +4 -4
  68. package/dist/material.css +1 -1
  69. package/dist/pdf-reader.js +1 -1
  70. package/dist/print.js +101 -98
  71. package/dist/{public-B0Nq_yYV.js → public-D_Gz7wgr.js} +6 -6
  72. package/dist/{rich-code-block-v7WmeZp9.js → rich-code-block-RN-EpeTA.js} +4 -4
  73. package/dist/{sortable-provider-CgDTolQR.js → sortable-provider-Iej7IS4n.js} +1 -1
  74. package/dist/{spinner-BtSIYVrC.js → spinner-BwWDXIbg.js} +1 -1
  75. package/dist/{stored-language-BeIKw3-K.js → stored-language-BAAP7qS3.js} +4 -2
  76. package/dist/theme-picker.js +2 -2
  77. package/dist/{theme-toggle-B2Pehdhn.js → theme-toggle-8zHeXSD1.js} +1 -1
  78. package/dist/{toolbar-items-rUHdVqAq.js → toolbar-items-CVQBYp4K.js} +2 -2
  79. package/dist/{translate-material-Bd_KgfB8.js → translate-material-C0aOnHpZ.js} +2 -2
  80. package/dist/{translator-Dje8HrNz.js → translator-BU07gNva.js} +1 -1
  81. package/dist/{use-chat-composer-B0_STpQN.js → use-chat-composer-P4GxmCHa.js} +14 -14
  82. package/dist/{use-heading-spy-DXo6IOX9.js → use-heading-spy-DETdqnB6.js} +1 -1
  83. package/dist/{use-material-translation-B3V_bz-5.js → use-material-translation-DGeVp-qP.js} +2 -2
  84. package/docs/components/data-display.md +60 -8
  85. package/docs/components/data-table-filter-and-lists.md +1 -1
  86. package/docs/components/feedback-and-status.md +22 -4
  87. package/docs/components/form-fields.md +14 -3
  88. package/docs/components/navigation-and-layout.md +8 -3
  89. package/docs/guides/backgrounds.md +35 -2
  90. package/docs/guides/drawing.md +15 -0
  91. package/docs/guides/printing.md +113 -2
  92. package/docs/previews/feedback-status-alert.webp +0 -0
  93. package/docs/roadmap.md +5 -3
  94. package/docs/troubleshooting.md +45 -0
  95. package/lib/backgrounds/css-react/icons-quiet.css +43 -0
  96. package/lib/backgrounds/css-react/icons.css +2 -0
  97. package/lib/components/components.css +2 -0
  98. package/lib/components/ui/display/avatar/avatar.css +17 -0
  99. package/lib/components/ui/feedback/alert/alert.css +16 -0
  100. package/package.json +6 -2
@@ -1,4 +1,4 @@
1
- import { r as e } from "./i18n-provider-C6HhBw34.js";
1
+ import { r as e } from "./i18n-provider-GV4Ukoe2.js";
2
2
  import { _ as t, g as n, h as r, n as i, v as a, y as o } from "./number.utils-BG_eEt5g.js";
3
3
  import { useCallback as s, useEffect as c, useMemo as l, useRef as u } from "react";
4
4
  //#region ../../node_modules/date-fns/locale/fr/_lib/formatDistance.js
@@ -1,5 +1,5 @@
1
- import { r as e } from "./i18n-provider-C6HhBw34.js";
2
- import { t } from "./translate-material-Bd_KgfB8.js";
1
+ import { r as e } from "./i18n-provider-GV4Ukoe2.js";
2
+ import { t } from "./translate-material-C0aOnHpZ.js";
3
3
  import { useCallback as n } from "react";
4
4
  //#region lib/i18n/use-material-translation.ts
5
5
  function r() {
@@ -66,6 +66,12 @@ The hairline's strength is `--avatar-ring-share`, the share of the tone's text c
66
66
  :root { --avatar-ring-share: 80%; }
67
67
  ```
68
68
 
69
+ **Initials scale with the size.** The fallback's text is 40 % of the avatar's diameter (never under 10 px), whatever the size class (`size-6` to `size-16` and beyond): two letters keep clear of the edge at 32 px and grow on a larger avatar. The colours are the tone's, so the text stays 4.5:1. The avatar is a size container (`container-type: inline-size`) and the share is `--avatar-text-share` (a unitless number, `40` by default):
70
+
71
+ ```css
72
+ :root { --avatar-text-share: 36; }
73
+ ```
74
+
69
75
  A fallback you restyle with `className` keeps the hairline: pass `shadow-none` (or a `tone`) for a filled look, for instance `<AvatarFallback className="bg-primary text-primary-foreground shadow-none">`. A toneless `AvatarFallback` inside `<AvatarBase tone="success">` inherits the root's tone. `AvatarFallback` needs an `Avatar` or `AvatarBase` ancestor (or its own `tone`) to be coloured: on its own it renders transparent, with no hairline.
70
76
 
71
77
  A shaped avatar:
@@ -83,7 +89,7 @@ The fallback with no `tone` is neutral: the neutral tone's container (it was the
83
89
  <Avatar shape="cookie-9" fallback="MK" tone="maths" /> {/* a tone the app declared in AppTones */}
84
90
  ```
85
91
 
86
- **Storybook:** `Data display/Avatar` (`Shapes`, `ShapedFallbacks`), `Data display/Avatar/Tones` · **Vue:** `Avatar` (`status`, `status-pulse`), `AvatarImage`, `AvatarFallback`
92
+ **Storybook:** `Data display/Avatar` (`Shapes`, `ShapedFallbacks`), `Data display/Avatar/Initials` (`InitialsScale`), `Data display/Avatar/Tones` · **Vue:** `Avatar` (`status`, `status-pulse`), `AvatarImage`, `AvatarFallback`
87
93
 
88
94
  ---
89
95
 
@@ -439,7 +445,7 @@ import { StatCard } from '@nxgt/material/components';
439
445
  <StatCard tone="success" variant="tonal" valueSize="display" label="Revenue" value="€48,200" delta={12.4} icon={<CreditCard />} />
440
446
  ```
441
447
 
442
- **Compact tile (`size="sm"`).** For figures in a header band: a tile 40 to 56 px tall that hugs its content, the optional icon chip, then the value (tabular figures) over its label. It takes the same `tone` and `variant` (`tonal`, `outlined`, `filled`); `hint`, `delta` and `valueSize` are ignored (nothing is drawn for them, and `loading` shows a skeleton in place of the value only). Tiles in a `flex flex-wrap gap-2` row sit side by side and wrap on a phone.
448
+ **Compact tile (`size="sm"`).** For figures in a header band: a tile 40 to 56 px tall that hugs its content, the optional icon chip, then the value (tabular figures) over its label. It takes the same `tone` and `variant` (`tonal`, `outlined`, `filled`); `hint`, `delta` and `valueSize` are ignored (nothing is drawn for them, and `loading` shows a skeleton in place of the value only). Tiles in a `flex flex-wrap gap-2` row sit side by side and wrap on a phone; for an even layout use `StatCardGroup`.
443
449
 
444
450
  ```tsx
445
451
  <div className="flex flex-wrap gap-2">
@@ -448,11 +454,24 @@ import { StatCard } from '@nxgt/material/components';
448
454
  </div>
449
455
  ```
450
456
 
457
+ **`StatCardGroup`: an even grid of compact tiles.** Four hugging tiles leave an orphan at 360 px. Wrap the `size="sm"` tiles in a `StatCardGroup`: while the group is narrower than `md` (28 rem, its own width, not the window's) the tiles are two equal columns, 2×2 for four (an odd last tile takes the whole row); from there they are one row of tiles that hug their content, wrapping if they must. It takes the props of a `div`; the root is `data-slot="stat-card-group"` and `StatCardGroupProps` is exported. `EntityHeader`'s `stats` use it.
458
+
459
+ ```tsx
460
+ import { StatCard, StatCardGroup } from '@nxgt/material/components';
461
+
462
+ <StatCardGroup>
463
+ <StatCard size="sm" variant="tonal" tone="primary" label="Students" value={22} />
464
+ <StatCard size="sm" variant="tonal" tone="tertiary" label="Classes" value={6} />
465
+ <StatCard size="sm" label="Mean" value="12.4" />
466
+ <StatCard size="sm" label="Absent" value={0} />
467
+ </StatCardGroup>
468
+ ```
469
+
451
470
  Parts: the root carries `data-slot="stat-card"`, `data-variant` and, when a tone applies, `data-tone` (the tone, or `neutral` on a `tonal` or `filled` card without one); the icon wrapper carries `data-slot="stat-card-icon"`. The compact tile adds `data-size="sm"`. The types `StatCardProps`, `StatCardVariant`, `StatCardValueSize` and `StatCardSize` are exported.
452
471
 
453
472
  In `filled`, the delta and its trend arrow take the on-colour too (the arrow and the sign still say up or down). The outline's share is `--stat-card-ring-share` (`68%`, or `--nxgt-tone-ring-share` for every ring); it draws the tonal card's border, the filled card's edge in a calm zone and the hairline round the icon chip. Known limit: the dark theme's primary `filled` card has an edge of about 2.3:1 on a `Card` and 2.5:1 on the page (its text is 7:1); a `tonal` card avoids it. See [App tones](../guides/theming.md#app-tones-on-shape-fab-avatar-statcard-and-the-metric-cards).
454
473
 
455
- **Storybook:** `Data display/StatCard`, `Data display/StatCard/Tones` · **Vue:** `StatCard`
474
+ **Storybook:** `Data display/StatCard/Group` (`GroupOnPhone`, `GroupOnDesktop`), `Data display/StatCard/Tones` · **Vue:** `StatCard` (`StatCardGroup` is React only for now)
456
475
 
457
476
  ---
458
477
 
@@ -630,15 +649,16 @@ import { Badge, EntityHeader } from '@nxgt/material/components';
630
649
 
631
650
  | Prop | Type | Default | Effect |
632
651
  | --- | --- | --- | --- |
633
- | `title` | `ReactNode` | — | the `h1`; optional: without it no heading is rendered (see below) |
652
+ | `title` | `ReactNode` | — | the heading (an `h1` unless `headingLevel` says otherwise); optional: without it no heading is rendered (see below) |
653
+ | `headingLevel` | `1 \| 2 \| 3` | `1` | the title's heading level: pass `2` or `3` when the page already has its `h1` (a list title beside a detail header); the look does not change |
634
654
  | `icon`, `status` | `ReactNode` | — | before and after the title; `icon` is decoration, shown in an `aria-hidden` badge, so put nothing meaningful or focusable there |
635
655
  | `metadata` | `{ label: string; value: ReactNode }[]` | `[]` | facts under the title, a description list: a muted label beside its value, no colon; a link in a value is drawn in the primary text colour (`text-primary-text`), not the prose link's info blue, and `nxgt-plain-link` still opts out |
636
- | `stats` | `EntityStat[]` | `[]` | figures (counts, a mean) as compact `size="sm"` [StatCard](#statcard) tiles in a wrapping row under the metadata; a `tone` paints a tile, `0` is a figure, an empty value drops the tile (see Stats below) |
656
+ | `stats` | `EntityStat[]` | `[]` | figures (counts, a mean) as compact `size="sm"` [StatCard](#statcard) tiles in a `StatCardGroup` (2×2 on a phone, a wrapping row beside) under the metadata; a `tone` paints a tile, `0` is a figure, an empty value drops the tile (see Stats below) |
637
657
  | `actions` | `ResponsiveButtonProps[] \| ReactNode` | `[]` | an array becomes responsive buttons (icon-only on phones) |
638
658
 
639
659
  The root carries `data-slot="entity-header"`; the metadata is a `dl` with `data-slot="entity-metadata"` (a muted `dt` label beside its `dd` value, no colon), and the actions sit in `data-slot="entity-header-actions"` (absent when there are none). Inside an EntityHeader that replaces the old `summary-data` slot and its `Label:` text, so update a selector or a test that read "Label: value".
640
660
 
641
- **Stats.** Figures (counts, a mean) go in `stats`, shown as compact [StatCard](#statcard) tiles (`size="sm"`) in a wrapping row under the metadata; facts (dates, names) stay in `metadata`. A `tone` paints a tile (tonal), none leaves it outlined; `0` is a figure, an empty value drops the tile. The row is `data-slot="entity-stats"`, and the type `EntityStat` is exported.
661
+ **Stats.** Figures (counts, a mean) go in `stats`, shown as compact [StatCard](#statcard) tiles (`size="sm"`) in a `StatCardGroup` (2×2 on a phone, a row that wraps beside) under the metadata; facts (dates, names) stay in `metadata`. A `tone` paints a tile (tonal), none leaves it outlined; `0` is a figure, an empty value drops the tile. The row is `data-slot="entity-stats"`, and the type `EntityStat` is exported.
642
662
 
643
663
  ```tsx
644
664
  <EntityHeader
@@ -667,6 +687,12 @@ import { Badge, EntityHeader } from '@nxgt/material/components';
667
687
 
668
688
  The root is a plain `div` in every case, never a landmark, so there is no accessible name to supply.
669
689
 
690
+ **A page that has its `h1` already.** A detail header beside a list titled "Team" is not the page's title: `headingLevel={2}` keeps one `h1` and an outline in order.
691
+
692
+ ```tsx
693
+ <EntityHeader headingLevel={2} title="Ada Lovelace" metadata={[{ label: 'Role', value: 'Analyst' }]} />
694
+ ```
695
+
670
696
  Without a `title` the band is one row at every width: the icon inline before the status and metadata, the content taking the space, the actions at the end of the row or, when they do not fit, wrapped below and still at the end. An `Avatar` as `icon` is drawn without the badge around it.
671
697
 
672
698
  With a title, in a narrow container (under `@sm`) the order is title, metadata, `children`, then the actions; from `@sm` the actions sit beside the title, yet they come after the `children` in the DOM and so in focus order.
@@ -695,7 +721,8 @@ A tile with an `href` ends with a discreet chevron after `trailing`: decorative
695
721
  | `chevron` | `boolean` | `true` with an `href`, else `false` | the decorative chevron after `trailing` |
696
722
  | `linkComponent` | `AppShellLinkComponent` | plain `<a>` | the link element, as in the app shell: takes `href`, spreads every other prop, forwards `ref` |
697
723
  | `onClick` | `MouseEventHandler` | — | renders a `<button>` |
698
- | `selected` | `boolean` | `false` | highlighted row |
724
+ | `selected` | `boolean` | `false` | the current row: drawn selected; a link gets `aria-current="page"` (your own `aria-current` replaces it), a button `aria-pressed` |
725
+ | `toggle` | `Switch` props | — | makes the tile a switch row (see below): the whole row toggles a Switch at its end; `href`, `onClick`, `actions`, `chevron` and `linkComponent` are refused beside it by type |
699
726
  | `size` | `'sm' \| 'md'` | `'md'` | `sm` is the dense navigation row |
700
727
  | `leading`, `trailing`, `subtitle` | `ReactNode` | — | slots; `trailing` sits inside the link, so it holds decoration only |
701
728
  | `actions` | `ReactNode` | — | buttons at the end of the tile, outside the link, vertically centred (`data-slot="item-actions"`) |
@@ -735,11 +762,36 @@ export function SettingsRow() {
735
762
  }
736
763
  ```
737
764
 
765
+ **A selected link, for list-detail navigation.** Give `selected` from the router's location: the open row is a link (a shareable URL), `aria-current="page"` and drawn selected. Pass `aria-current="true"` when the selection is not a page.
766
+
767
+ ```tsx
768
+ import { useLocation } from 'react-router';
769
+
770
+ function GuideList({ guides }: { guides: { to: string; title: string }[] }) {
771
+ const { pathname } = useLocation();
772
+ return (
773
+ <nav aria-label="Guides">
774
+ {guides.map((guide) => (
775
+ <ListTile key={guide.to} title={guide.title} href={guide.to} linkComponent={RouterLink} selected={pathname === guide.to} />
776
+ ))}
777
+ </nav>
778
+ );
779
+ }
780
+ ```
781
+
782
+ **A switch row.** `toggle` takes a Switch's props (`checked`, `defaultChecked`, `onCheckedChange`, `name`, `thumbIcons`…). The tile is then a `<label>` for the switch, so a press anywhere on the row toggles it; the switch is the one tab stop (Space or Enter), named by `title` and described by `subtitle`, and `disabled` on the tile disables it. A switch row is neither a link nor a button: `href`, `onClick`, `actions` and `chevron` do not apply, and `trailing` sits before the switch.
783
+
784
+ ```tsx
785
+ const [on, setOn] = useState(true);
786
+
787
+ <ListTile title="Notifications" subtitle="Email and push" toggle={{ checked: on, onCheckedChange: setOn }} />
788
+ ```
789
+
738
790
  See also [A `ListTile` link reloads the whole page](../troubleshooting.md#a-listtile-link-reloads-the-whole-page). A disabled tile never renders the link component: it is a plain `<a>` without `href`, with `aria-disabled="true"`.
739
791
 
740
792
  The root carries `data-slot="item-detail"` (a tile that is a button or a plain row too).
741
793
 
742
- **Storybook:** `Data display/ListTile` · **Vue:** `ListTile`
794
+ **Storybook:** `Data display/ListTile`, `Data display/ListTile links`, `Data display/ListTile current` · **Vue:** `ListTile`
743
795
 
744
796
  ---
745
797
 
@@ -87,7 +87,7 @@ table enables, for typing your own column defs.
87
87
 
88
88
  | Prop | Type | Default | Effect |
89
89
  | --- | --- | --- | --- |
90
- | `columns` | `ColumnDef<DataTableFeatures, T>[]` | — | `meta.className` styles a column's cells; `meta.label` names the column in the cards sort menu |
90
+ | `columns` | `ColumnDef<DataTableFeatures, T>[]` | — | `meta.className` styles a column's cells; `meta.numeric` sets a column of figures flush to the end in tabular digits (`text-end tabular-nums`, header and cells), so decimals line up; `meta.label` names the column in the cards sort menu |
91
91
  | `data` | `T[]` | — | rows; sorting, filtering and pagination run client-side |
92
92
  | `layout` | `'table' \| 'cards' \| 'tiles' \| { phone, wide }` | `'table'` | `'cards'` lays the rows out as a list of cards, `'tiles'` as one column in a single surface ([Cards and tiles](../guides/cards-and-tiles.md)); both ignore `virtual`, `renderExpandedRow` and `showViewOptions` |
93
93
  | `cardDensity` | `'comfortable' \| 'compact'` | `'comfortable'` | `compact` fits cards of at least `--nxgt-cards-min` (15rem), up to four a row, with a tighter padding; `layout="cards"` only ([Cards and tiles](../guides/cards-and-tiles.md#columns)) |
@@ -38,22 +38,40 @@ import { CircleAlert } from 'lucide-react';
38
38
 
39
39
  <Alert
40
40
  variant="warning"
41
- icon={<CircleAlert />}
42
41
  title="Check the address"
43
42
  description="The postcode does not match the city."
44
43
  />
45
44
  ```
46
45
 
46
+ An Alert shows its tone's icon by default, the same one a [Banner](#banner)
47
+ shows (an `info` circle for `primary`, `secondary`, `foreground` and `info`, a
48
+ check, a triangle, an alert circle). Pass `icon={<CircleAlert />}` to use
49
+ your own, or `icon={null}` for none. Before 3.13 an Alert had no icon unless
50
+ you passed one, so an Alert without `icon` now gains one: add `icon={null}` to
51
+ keep the old look.
52
+
47
53
  | Prop | Type | Default | Effect |
48
54
  | --- | --- | --- | --- |
49
55
  | `variant` | `'primary' \| 'secondary' \| 'success' \| 'info' \| 'warning' \| 'error' \| 'foreground'` | `'primary'` | left border, tint and icon colour |
50
- | `icon` | `ReactNode` | — | shown before the text; no default icon |
56
+ | `icon` | `ReactNode` | the tone's icon | shown before the text; `null` (or `false`) shows none, any node replaces the tone's icon |
51
57
  | `title` | `ReactNode` | — | bold first line |
52
58
  | `description` | `ReactNode` | — | muted body text |
53
59
 
54
60
  The root carries `data-slot="alert"`, the title `data-slot="alert-title"` and the description `data-slot="alert-description"`, for tests and selectors.
55
61
 
56
- **Storybook:** `Feedback & status/Alert` · **Vue:** `Alert`
62
+ **In a form grid.** Inside `.form-grid-layout`'s `[data-slot="form-fields"]` or a [FormSection](form-fields.md#formsection)'s fields, an `Alert` or a `Banner` placed directly in the grid takes the whole row (`col-span-full w-full`) without any class from you; a utility class of your own on it still wins.
63
+
64
+ ```tsx
65
+ <form className="form-grid-layout">
66
+ <div data-slot="form-fields">
67
+ <Alert variant="warning" title="Check the address" />
68
+ <TextField label="Street" />
69
+ <TextField label="City" />
70
+ </div>
71
+ </form>
72
+ ```
73
+
74
+ **Storybook:** `Feedback & status/Alert/Tone icon` (`ToneIcon`), `Feedback & status/Alert/In a form grid` (`InFormGrid`) · **Vue:** `Alert` (the default icon and the form-grid row are React only for now)
57
75
 
58
76
  ---
59
77
 
@@ -85,7 +103,7 @@ import { Banner, Button } from '@nxgt/material/components';
85
103
  | Prop | Type | Default | Effect |
86
104
  | --- | --- | --- | --- |
87
105
  | `tone` | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | colours and the default icon |
88
- | `icon` | `ReactNode` | the tone's icon | replaces the default icon; `false` shows none |
106
+ | `icon` | `ReactNode` | the tone's icon | replaces the default icon; `false` shows none. The default icons are Alert's (one shared map) |
89
107
  | `action` | `ReactNode` | — | rendered at the end of the banner; under `sm` it drops below the text, in line with it; there a `ghost` or `link` button (a `LinkButton` too, any size) that is the action itself, not wrapped in another element, is pulled back by its own padding, so its label (or leading icon) starts at the text's edge |
90
108
  | `onDismiss` | `() => void` | — | shows a close button; the banner does not hide itself |
91
109
 
@@ -144,6 +144,7 @@ export function SettingsForm({ onSave }: { onSave: (values: Values) => void }) {
144
144
  | `titleAs` | `'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6'` | `'h2'` | the heading level: `'h3'` when the form already sits under an `h2` |
145
145
  | `description` | `ReactNode` | — | a muted line under the title; the group's accessible description |
146
146
  | `actions` | `ReactNode` | — | controls beside the title, wrapping under it on a phone |
147
+ | `variant` | `'label' \| 'heading'` | `'label'` | `label` is the small uppercase muted title; `heading` puts the title on the type scale (`title-md` emphasized) and the description in `body-md`, muted |
147
148
  | `separated` | `boolean` | `true` | a rule above the section when it follows another `FormSection` |
148
149
 
149
150
  The section is a `role="group"` named by its heading (`aria-labelledby`) and
@@ -157,11 +158,21 @@ grid uses, by the section's own width (it is a container query container, so
157
158
  it works outside a form grid too). The form grid's wide-field rules still
158
159
  apply inside it: a switch, a textarea, chips, files and the rich text editor
159
160
  take a row of their own. The rule between sections is a top border drawn on
160
- each section that follows another; the first has none. The heading is small,
161
+ each section that follows another; the first has none. The default heading is small,
161
162
  semibold, uppercase and muted, set apart from the fields' labels, and reads
162
- 4.5:1 in both themes.
163
+ 4.5:1 in both themes. `variant="heading"` is the section title the design
164
+ rules ask for: `title-md` emphasized in the text colour over a `body-md`
165
+ muted description (both 4.5:1 or more); the root carries `data-variant`. An
166
+ `Alert` or a `Banner` placed directly in the section's fields takes the whole
167
+ row.
163
168
 
164
- **Storybook:** `Form fields/FormSection` · **Vue:** `FormSection`
169
+ ```tsx
170
+ <FormSection variant="heading" title="Profile" description="How you appear to other members.">
171
+ <TextFormField control={control} name="firstName" label="First name" />
172
+ </FormSection>
173
+ ```
174
+
175
+ **Storybook:** `Form fields/FormSection/Heading variant` (`HeadingVariant`) · **Vue:** `FormSection` (no `variant` yet)
165
176
 
166
177
  ---
167
178
 
@@ -361,12 +361,11 @@ List / details that collapses to one view on mobile, a side pane that becomes a
361
361
  <img src="../previews/navigation-layout-threadlayout.webp" height="96" alt="ThreadLayout">
362
362
 
363
363
  ```tsx
364
- import { ListDetailsLayout, ListDetailsTrigger, PaneLayout, ThreadLayout } from '@nxgt/material/components';
364
+ import { ListDetailsLayout, PaneLayout, ThreadLayout } from '@nxgt/material/components';
365
365
 
366
366
  <ListDetailsLayout activeView={view} onActiveViewChange={setView}>
367
367
  <ListDetailsLayout.List>{conversations}</ListDetailsLayout.List>
368
- <ListDetailsLayout.Details>
369
- <ListDetailsTrigger view="list">Back</ListDetailsTrigger>
368
+ <ListDetailsLayout.Details back>
370
369
  {thread}
371
370
  </ListDetailsLayout.Details>
372
371
  </ListDetailsLayout>
@@ -397,6 +396,8 @@ app bar.
397
396
  | --- | --- | --- | --- |
398
397
  | `activeView` / `defaultActiveView` / `onActiveViewChange` | `'list' \| 'details'` | `'list'` | `ListDetailsLayout`: which view a phone shows |
399
398
  | `listPosition` | `'left' \| 'right'` | `'left'` | `ListDetailsLayout` |
399
+ | `breakpoint` | `'viewport' \| 'container'` | `'viewport'` | `ListDetailsLayout`: what the 648 px phone layout follows, the window or the layout's own width |
400
+ | `back` | `boolean \| ReactNode` | — | `ListDetailsLayout.Details`: a bar with the way back to the list, on a phone only; `true` is the translated "Back to the list" (`material['list-details'].back`), a node is its label |
400
401
  | `open` / `defaultOpen` / `onOpenChange` | `boolean` | `false` | `PaneLayout`: pane shown |
401
402
  | `paneSide` | `'left' \| 'right'` | `'left'` | `PaneLayout` |
402
403
  | `sheetTitle` | `string` | "Side Panel" (translated, `material['pane-layout']['sheet-title']`) | `PaneLayout.Pane`: screen-reader title of the sheet on phones |
@@ -404,6 +405,10 @@ app bar.
404
405
 
405
406
  `useListDetails()` and `usePaneLayout()` read the state from inside.
406
407
 
408
+ **The way back on a phone.** With `back` on `ListDetailsLayout.Details` the details open under a bar with a labelled back button (`ListDetailsLayout.Back`, also usable in your own header; it renders nothing on a wide screen or when the list shows). On a phone the layout also moves the focus: to the details when a row opens them, then back to that row when the way back is taken, also when `activeView` is controlled by the screen (a URL-driven list and detail). A wide screen shows both panes, with no bar and no focus moves. Leave `back` out and `ListDetailsTrigger view="list"` still works as before.
409
+
410
+ **Sizing.** The layout fills the box it is in: `h-full` and, in a flex column, `flex-1 min-h-0`, so a header above it and the layout under it share the height with no `h-dvh`. `breakpoint="container"` makes the single-view layout follow the layout's own width (a dialog, a column of a split view) rather than the window; the first render is the wide one, as on a server.
411
+
407
412
  **Storybook:** `Navigation & layout/ListDetailsLayout`, `Navigation & layout/PaneLayout`, `Navigation & layout/ThreadLayout` · **Vue:** `ListDetailsLayout`, `PaneLayout`, `ThreadLayout`
408
413
 
409
414
  ---
@@ -55,7 +55,7 @@ import '@nxgt/material/backgrounds.css';
55
55
  | `sky` | `'moon'`, `'eclipse'`, `'meteor-shower'`, `'ringed-planet'` or `'northern-lights'`: a second scene drawn over any preset, before the grain; see [Celestial presets and the sky overlay](#celestial-presets-and-the-sky-overlay) | none |
56
56
  | `icons` | for `preset="icons"`: a pack name (`school`, `health`, `finance`, `travel`, `nature`, `tech`) or up to 48 icon names and `{ name, family?, type? }` references; see [Icons](#icons) | the `school` pack |
57
57
  | `iconStyle` | for `preset="icons"`: one style (`{ family?, type? }` or a sprite name such as `'sharp-light'`) or a list of at most 8, one drawn per icon | duotone thin |
58
- | `iconMotif` | for `preset="icons"`: `layout` (`scatter`, `grid`, `constellation`, `drift`), `sizeRange`, `rotation`, `duotone` (`opacity` or `tones`), `secondaryOpacity` (0.4 or, with `tones`, 0.85 unless set), `calm` | `{ layout: 'scatter', duotone: 'opacity', calm: true }` (`secondaryOpacity` unset: 0.4, or 0.85 with `tones`) |
58
+ | `iconMotif` | for `preset="icons"`: `layout` (`scatter`, `grid`, `constellation`, `drift`), `sizeRange`, `rotation`, `duotone` (`opacity` or `tones`), `secondaryOpacity` (0.4 or, with `tones`, 0.85 unless set), `calm`, `quiet` (`start`, `end`, `center`: a band the icons fade out of) | `{ layout: 'scatter', duotone: 'opacity', calm: true }` (`secondaryOpacity` unset: 0.4, or 0.85 with `tones`) |
59
59
  | `fill` | `viewport` (the viewport's size) or `container` (its parent's size: the parent needs a definite height) | `viewport` |
60
60
 
61
61
 
@@ -357,7 +357,40 @@ import { Background } from '@nxgt/material/backgrounds';
357
357
  - `animated` drifts each icon a few pixels on `translate` (never under `prefers-reduced-motion`, paused while hidden).
358
358
  - In an `ActivityLayout` the keep-inside pass hides an icon that would cross the layer's edge or the header band.
359
359
  - A theme can name the preset with `background: { preset: 'icons', icons, iconStyle, iconMotif, sky }`; the props win. The theme picker draws the first four icons in the theme's tile ([themes](themes.md#icon-motifs-in-a-theme)).
360
- - CSS: `backgrounds/icons.css` (or `backgrounds.css`). React only: `@nxgt/material-vue` does not draw it.
360
+ - CSS: `backgrounds/icons.css` (or `backgrounds.css`). Both pull in `backgrounds/icons-quiet.css`, the mask behind `iconMotif.quiet`, which can also be imported alone. React only: `@nxgt/material-vue` does not draw it.
361
+
362
+ ### Keeping text clear of the icons: `quiet`
363
+
364
+ A greeting band, a hero or a card header often puts text on the motif. `calm`
365
+ only thins the icons in the middle of the frame; `iconMotif.quiet` clears a
366
+ **band** of it: the icons fade out over `start`, `end` or the `center`, so the
367
+ text sits on the wash alone.
368
+
369
+ ```tsx
370
+ <Background preset="icons" icons="school" iconMotif={{ quiet: 'start' }} className="h-48 rounded-2xl">
371
+ <div className="absolute inset-y-0 start-0 flex w-[42%] flex-col justify-center ps-6">
372
+ <h2>Good morning, Ada</h2>
373
+ <p>Three classes today, the first at 9:00.</p>
374
+ </div>
375
+ </Background>
376
+ ```
377
+
378
+ - `'start'` and `'end'` follow the reading direction (the left, then the right,
379
+ in a left-to-right page; swapped in a right-to-left one). `'center'` keeps the
380
+ middle clear, for a centred title.
381
+ - It is a CSS mask on the scene, so it holds at every width and with every
382
+ `layout`; the icons are the same ones (the seed and the layout are unchanged),
383
+ only hidden. No inline style: it is a class on the scene (`ic-q-start`,
384
+ `ic-q-end`, `ic-q-center`) and a rule of the preset's stylesheet.
385
+ - The band is `--background-quiet` wide, **42%** by default, then the icons fade
386
+ back in over 30% more. Set it with a class: `className="[--background-quiet:60%]"`.
387
+ Keep your text inside the band's width, and widen the band on a phone, where
388
+ the text takes more of the line.
389
+ - Contrast: under the mask the text sits on the surface and the wash (at most
390
+ 15% of a tone), never on an icon. The `Backgrounds/Icons/Quiet band` story
391
+ measures the title and the muted line at 4.5:1 against the surface and each
392
+ tone of the wash, light and dark.
393
+ - A theme sets it too: `background: { preset: 'icons', iconMotif: { quiet: 'end' } }`.
361
394
 
362
395
  ## Dark mode
363
396
 
@@ -220,10 +220,25 @@ A `Drawing` takes `title` **or** `decorative`; the type refuses neither.
220
220
  ```tsx
221
221
  <Drawing scene={chart} title="Sales by month, rising" /> {/* role="img" aria-label */}
222
222
  <Drawing scene={flourish} decorative /> {/* aria-hidden */}
223
+ <Drawing
224
+ scene={chart}
225
+ title="Sales by month, rising"
226
+ description="January 12, February 18, March 25, April 31."
227
+ /> {/* role="img", aria-label, <desc>, aria-describedby */}
223
228
  ```
224
229
 
230
+ The `<desc>`'s id is the drawing's own id prefix plus `-desc`, so two drawings on
231
+ a page never share one. `toSVG` takes the same `description` (with a `title`; it
232
+ throws on a `decorative` drawing): `toSVG(scene, { title, description })` writes the
233
+ `<desc id="…-desc">` and the root's `aria-describedby`, escaped, in the file or
234
+ e-mail too.
235
+
225
236
  - The name is an `aria-label`, never a `<title>` element: React 19 would hoist a
226
237
  `<title>` into the document's head.
238
+ - A long description, such as a chart's data, is the `description` prop, next to
239
+ the `title` (it needs one: a decorative drawing is hidden). It is written as a
240
+ `<desc>` inside the `<svg>`, which the root names in `aria-describedby`, so a
241
+ screen reader reads the name, the role, then the description.
227
242
  - A drawing with an interactive node is `role="group"`, because an image cannot
228
243
  hold focusable children; a decorative one cannot hold an interactive node and
229
244
  throws `A decorative drawing has an interactive node: give the Drawing a title instead`.
@@ -121,6 +121,8 @@ import { PrintDocument, PrintHeader } from '@nxgt/material/print';
121
121
  | `producer` | `creator` | The software that wrote the PDF. |
122
122
  | `language` | the `I18nProvider` around the document, then `'en'` | The built-in strings ("Page 2 of 5") and the PDF's language. |
123
123
  | `locale`, `timeZone` | `language`, the runtime's | How the data blocks format amounts and dates. |
124
+ | `theme` | — | `{ fonts: { sans, serif, mono }, colors }`: the page's font family, which every block inherits. Pass a family you embedded with [`registerPdfFont`](#embedding-a-font-and-archiving) to ship the font in the file. |
125
+ | `conformance` | — | A PDF/A level (`'PDF/A-2b'`, …) the file claims. Needs every font embedded; see [Embedding a font](#embedding-a-font-and-archiving). |
124
126
 
125
127
  Nothing but `children` is required: `<PrintDocument>{…}</PrintDocument>` is an
126
128
  A4 page with the margins the blocks were designed for.
@@ -366,7 +368,8 @@ TypeScript checks every `key` against `T`.
366
368
  | `label` | `string` | — | Required. The header text, and the label in a [`PrintKeyValue`](#printkeyvalue-one-record-as-label-and-value-pairs) |
367
369
  | `width` | `number \| '<n>%'` | — | A fixed width in points (`40`) or a share of the table (`'25%'`) |
368
370
  | `flex` | `number` | `1` | Share of the width the fixed columns leave. Ignored when `width` is set |
369
- | `align` | `'left' \| 'center' \| 'right'` | `'right'` for `amount`, `number` and `percent`, else `'left'` | Header and cells |
371
+ | `align` | `'left' \| 'center' \| 'right'` | `'right'` for `amount`, `number`, `percent` and `numeric: true` columns, else `'left'` | Header and cells |
372
+ | `numeric` | `boolean` | `true` for `amount`, `number` and `percent`, else `false` | A column of figures: its cells print with [tabular digits](#tabular-figures) and flush right. Set it on a column whose `format` is a function; `numeric: false` drops the tabular digits of an `amount` column but keeps it flush right (give `align: 'left'` to move it) |
370
373
  | `format` | `'amount' \| 'date' \| 'number' \| 'percent' \| (value, row) => string` | none | How the value prints (below) |
371
374
  | `currency` | `string \| (row: T) => string \| null \| undefined` | the table's `currency` | ISO 4217 code for `format: 'amount'`, for the column or per row |
372
375
  | `total` | `'sum' \| (rows: T[]) => number \| string \| null` | none | A figure in the totals row: the sum of the column, or your own |
@@ -553,7 +556,7 @@ function resolvePrintTable<T>(
553
556
  rows: T[],
554
557
  options?: { currency?: string; totalLabel?: string },
555
558
  ): {
556
- columns: { id: string; label: string; align: 'left' | 'center' | 'right'; width?: number | `${number}%`; flex?: number }[];
559
+ columns: { id: string; label: string; align: 'left' | 'center' | 'right'; width?: number | `${number}%`; flex?: number; numeric?: boolean }[];
557
560
  rows: PrintValue[][];
558
561
  footer: PrintValue[][]; // one totals row, or none
559
562
  empty: boolean;
@@ -976,6 +979,8 @@ type PdfDocument = {
976
979
  version: 1;
977
980
  meta?: { title?: string; author?: string; subject?: string; keywords?: string; creator?: string; producer?: string };
978
981
  language?: string; // the built-in strings ('en', 'fr') and the PDF's language; 'en' by default
982
+ outline?: boolean; // the bookmarks panel: one entry per heading; off by default
983
+ conformance?: 'PDF/A-1' | 'PDF/A-1b' | 'PDF/A-2' | 'PDF/A-2b' | 'PDF/A-3' | 'PDF/A-3b'; // needs embedded fonts
979
984
  locale?: string; // how amounts, dates and numbers print; `language` by default
980
985
  timeZone?: string;
981
986
  theme?: { colors?: Partial<Record<PdfThemeColor, string>>; fonts?: Partial<Record<'sans' | 'serif' | 'mono', string>> };
@@ -1332,6 +1337,112 @@ Rendering a model throws, before any page is drawn, on:
1332
1337
  | `[pdf] Unknown node type "chart".` | A `type` this version does not know: a typo in hand-written JSON, or a document from a newer version |
1333
1338
  | `[pdf] Unsupported document version 2: this version renders version 1.` | `version` is not `1` |
1334
1339
 
1340
+ ## Accessible, archival and consistent PDFs
1341
+
1342
+ What a PDF built here is, and is not, for assistive technology and archives.
1343
+
1344
+ ### What is written, and what is not tagged
1345
+
1346
+ `@react-pdf/renderer` (4.9) draws text and boxes; it never writes the
1347
+ structure tree a *tagged* PDF needs (the `Marked` flag, `StructTreeRoot`,
1348
+ marked content for headings, tables and figures). pdfkit, underneath, can tag,
1349
+ but react-pdf does not call it, so **no document made here is a tagged PDF,
1350
+ and none claims to be PDF/UA**. A screen reader reads the text in the
1351
+ document's order (the order you wrote the nodes), with no heading or table
1352
+ semantics. What *is* written:
1353
+
1354
+ | What | How | Where a reader sees it |
1355
+ | --- | --- | --- |
1356
+ | Language | `language` on the document (or `<PrintDocument language>`) | The reader's speech language, and the file's `/Lang` |
1357
+ | Title, author, subject, keywords | `meta`, or the `PrintDocument` props | The window title (shown instead of the file name), the document properties |
1358
+ | Outline (bookmarks) | `outline: true` on a [model document](#documents-as-data): each `heading` is an entry, a level 2 under the level 1 before it, a level 3 under the level 2 | The bookmarks panel, with a jump to the heading's page |
1359
+ | Reading order | The order of the nodes, header then body | The text selection, copy and screen-reader order |
1360
+ | PDF/A marker | `conformance: 'PDF/A-2b'` with an embedded font | The archive validators' first check (this is not a validation: run a PDF/A validator on what you keep) |
1361
+
1362
+ Text in an image or a drawing is not text: put what it says in the page
1363
+ next to it. The model's `image` node has no `alt`: there is nowhere in an
1364
+ untagged PDF to write it. The outline lists the headings of the page bodies,
1365
+ not of a repeated `header` or `footer`.
1366
+
1367
+ ```ts
1368
+ import { heading, page, pdfDocument, text } from '@nxgt/material/print';
1369
+
1370
+ pdfDocument({
1371
+ language: 'fr',
1372
+ meta: { title: 'Facture INV-42' }, // shown as the window title
1373
+ outline: true, // the bookmarks panel
1374
+ pages: [page({ body: [heading('Facture'), heading('Lignes', 2), text('…')] })],
1375
+ });
1376
+ ```
1377
+
1378
+ ### Embedding a font and archiving
1379
+
1380
+ pdfkit's standard fonts (Helvetica, Times-Roman, Courier) are **not embedded**
1381
+ in the file: a viewer draws its own Helvetica, so the glyph widths, the euro
1382
+ sign and the narrow no-break space of French amounts can differ, and PDF/A
1383
+ refuses the file. To ship the font, register one family once and name it in
1384
+ the theme (`registerPdfFont` is a thin call to react-pdf's `Font.register`):
1385
+
1386
+ ```ts
1387
+ import { pdfDocument, registerPdfFont } from '@nxgt/material/print';
1388
+ import regular from '@fontsource/inter/files/inter-latin-400-normal.woff?url';
1389
+ import bold from '@fontsource/inter/files/inter-latin-700-normal.woff?url';
1390
+
1391
+ const sans = registerPdfFont({ family: 'Inter', regular, bold }); // once per process
1392
+ pdfDocument({
1393
+ theme: { fonts: { sans } }, // or <PrintDocument theme={{ fonts: { sans } }}>
1394
+ conformance: 'PDF/A-2b', // optional: the archive marker
1395
+ pages,
1396
+ });
1397
+ ```
1398
+
1399
+ The same two options on the component:
1400
+
1401
+ ```tsx
1402
+ <PrintDocument theme={{ fonts: { sans } }} conformance="PDF/A-2b" title="Invoice 2026-001">
1403
+ {/* pages */}
1404
+ </PrintDocument>
1405
+ ```
1406
+
1407
+ - **Give static files** (TTF, OTF or WOFF), one per style: `regular`, `bold`,
1408
+ `italic`, `boldItalic`. A variable font (`inter-latin-wght-normal.woff2`)
1409
+ has one weight in a PDF, so its bold looks regular, and its bold subset can
1410
+ fail.
1411
+ - The file embeds the glyphs it uses (a subset), so a family adds tens of
1412
+ kilobytes to a document, not its whole size. The library does **not** bundle
1413
+ a font: the size of `@nxgt/material` and `@nxgt/material-core` stays as it
1414
+ is, and the choice (and its licence) is yours. `@fontsource/inter` (OFL) is a
1415
+ static, free option.
1416
+ - A source is a URL or a path react-pdf can fetch where it renders: a file
1417
+ path on a server, a URL (or a bundler's `?url` import) in the browser.
1418
+ - On a server bundle, the registered font is read at render time; nothing
1419
+ needs `pdfkitStandardFonts()` for it.
1420
+ - Register before the first render; registering the same family again adds
1421
+ its faces.
1422
+
1423
+ ### Tabular figures
1424
+
1425
+ Figures that should line up (an invoice's amounts, a report card's marks) need
1426
+ digits of one width. The standard fonts' digits already are; a proportional
1427
+ font such as Inter has them as the OpenType `tnum` feature. A `numeric`
1428
+ column, an `amount`, `number` or `percent` column of a `dataTable` or a
1429
+ `PrintDataTable`, the values of `totals` and a `metric` turn it on, and a
1430
+ `style: { tabularNums: true }` does it on any `text`:
1431
+
1432
+ ```ts
1433
+ table({
1434
+ columns: [{ label: 'Item', flex: 3 }, { label: 'Qty', numeric: true, width: 50 }],
1435
+ rows: [['Design', 12], ['Review', 140]],
1436
+ });
1437
+ text('1 234,50', { style: { tabularNums: true } });
1438
+ ```
1439
+
1440
+ A font without the feature (the standard ones, Liberation Sans) draws the same
1441
+ as before. On screen, a `DataTable` column takes `meta: { numeric: true }`
1442
+ (flush to the end, `tabular-nums`; see
1443
+ [DataTable](../components/data-table-filter-and-lists.md)), and a `DataGrid`'s
1444
+ cells are `tabular-nums` already.
1445
+
1335
1446
  ## Why it is a separate entry
1336
1447
 
1337
1448
  `@react-pdf/renderer` depends on `pdfkit`, and pdfkit loads its fourteen
package/docs/roadmap.md CHANGED
@@ -168,8 +168,9 @@ version they shipped in, and planned entries may name the major they target.
168
168
  accessibility (PDF/UA), archival (PDF/A-3) and electronic invoicing (Factur-X,
169
169
  EN 16931). It also brings the items below.
170
170
  - **Your brand on the page** — a print theme whose defaults follow the screen
171
- theme, and your own font files registered in one call (the library bundles
172
- no fonts).
171
+ theme, and your own font files registered in one call (the font half ships:
172
+ `registerPdfFont` and `theme.fonts` on the current renderer; the library
173
+ bundles no fonts).
173
174
  - **QR codes, barcodes and charts in a PDF** — pass the value or the chart
174
175
  options and get vector output, no data URL to prepare.
175
176
  - **Rich text and Markdown in a PDF** — the rich-text editor's content and
@@ -178,7 +179,8 @@ version they shipped in, and planned entries may name the major they target.
178
179
  renders it itself, refreshing a moment after its data stops changing.
179
180
  - **Test helpers for generated PDFs** — read a generated PDF's text, page count
180
181
  and metadata in a unit test, without a browser.
181
- - **Archive-ready PDFs** — PDF/A-3 output through one option, for invoices and
182
+ - **Archive-ready PDFs** — PDF/A-3 output through one option (the PDF/A-1/2/3
183
+ marker ships already through `conformance` on the current renderer), for invoices and
182
184
  records that must be kept.
183
185
  - **Electronic invoices (Factur-X)** — an invoice PDF that carries its
184
186
  machine-readable data (EN 16931), built on archive-ready PDFs and the
@@ -3384,6 +3384,51 @@ renders.
3384
3384
  **Fix:** build the document with `pdfDocument()` or `definePdfDocument()`,
3385
3385
  which stamp `version: 1`, rather than by hand.
3386
3386
 
3387
+ ### A PDF accessibility checker says the PDF is not tagged
3388
+
3389
+ **When:** a checker (Acrobat's accessibility check, PAC, veraPDF with the PDF/UA profile) reports "document is not tagged" or "no structure tree" on a PDF made with `@nxgt/material/print`.
3390
+ **Why:** `@react-pdf/renderer` 4.9 writes no structure tree, so no PDF made here is a tagged PDF, and none claims PDF/UA. This is the renderer's limit, not a missing option. What the library does write is the language, the title (shown as the window title), the outline and the reading order of the nodes.
3391
+ **Fix:** set what can be set, and say the rest in the page: `language` and `meta.title` on the document, `outline: true` for a long one, text beside a drawing or an image. If a tagged PDF is a requirement, offer an accessible HTML version of the same data. See [what is written, and what is not tagged](guides/printing.md#what-is-written-and-what-is-not-tagged).
3392
+
3393
+ ```ts
3394
+ pdfDocument({ language: 'fr', meta: { title: 'Facture INV-42' }, outline: true, pages });
3395
+ ```
3396
+
3397
+ ### The PDF's font is substituted, or a PDF/A validator rejects the file
3398
+
3399
+ **When:** a viewer shows a different Helvetica than the one you designed with (other widths, a missing euro sign, or the narrow no-break space of French thousands drawn as a wrong glyph), or a PDF/A validator reports "font not embedded".
3400
+ **Why:** the default fonts (Helvetica, Times-Roman, Courier) are pdfkit's standard fonts: the file names them and the viewer draws its own.
3401
+ **Fix:** embed a family with `registerPdfFont` and name it in the theme; see [Embedding a font and archiving](guides/printing.md#embedding-a-font-and-archiving).
3402
+
3403
+ ```ts
3404
+ const sans = registerPdfFont({ family: 'Inter', regular: interRegular, bold: interBold });
3405
+ pdfDocument({ theme: { fonts: { sans } }, conformance: 'PDF/A-2b', pages });
3406
+ ```
3407
+
3408
+ ### An embedded font has no bold
3409
+
3410
+ **When:** `registerPdfFont` was given a variable font (`inter-latin-wght-normal.woff2`): bold text looks regular, and in some builds rendering fails while writing the font.
3411
+ **Why:** react-pdf draws a variable font at its default instance, so bold has nothing to switch to (a bold request on a variable woff2 also failed in our tests).
3412
+ **Fix:** give one static file per style (`@fontsource/inter` ships `inter-latin-400-normal.woff` and `inter-latin-700-normal.woff`).
3413
+
3414
+ ### A `Drawing` `description` is never read
3415
+
3416
+ **When:** a `Drawing` with `description` writes no `<desc>`, or a screen reader says only the name.
3417
+ **Why:** a description is read after the name, so it needs a `title`; a `decorative` drawing is hidden from assistive technology, and the type refuses `description` on it. `toSVG` takes the same `description` option and throws without a `title` or with `decorative`, so an exported file or an e-mail carries it too.
3418
+ **Fix:** give the drawing a `title`, and keep the title short and the description long (`toSVG(scene, { title, description })` does the same for a file):
3419
+
3420
+ ```tsx
3421
+ <Drawing scene={chart} title="Sales by month" description="January 12, February 18, March 25: rising." />
3422
+ ```
3423
+
3424
+ ### `iconMotif.quiet` changes nothing
3425
+
3426
+ A theme that defines `iconMotif.quiet` with a value other than `start`, `end` or `center` is rejected by `defineTheme`; see the core troubleshooting.
3427
+
3428
+ **When:** text over `<Background preset="icons">` still sits on icons although `iconMotif={{ quiet: 'start' }}` is set.
3429
+ **Why:** `quiet` is a CSS mask the `icons` preset's stylesheet draws: another preset ignores it, the band is `--background-quiet` wide (42% by default) and fades back in over 30% more, and on a phone the text can be wider than the band.
3430
+ **Fix:** use `preset="icons"`, import `@nxgt/material/backgrounds.css`, and widen the band: `className="[--background-quiet:60%]"`. See [Keeping text clear of the icons](guides/backgrounds.md#keeping-text-clear-of-the-icons-quiet).
3431
+
3387
3432
  ### `'invoice' is of type 'unknown'` in a `definePdfDocument` template
3388
3433
 
3389
3434
  **When:** compiling a template whose data parameter has no type,