@nxgt/material 3.68.0 → 3.71.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 (163) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +13 -9
  3. package/dist/{alert-dialog-BhARh_qC.js → alert-dialog-y2pbowVh.js} +1 -1
  4. package/dist/{async-slice-CHyRU5V3.js → async-slice-CGfWBOaV.js} +1 -1
  5. package/dist/{button-Du7fJ9i_.js → button-D54Hrjo0.js} +1 -1
  6. package/dist/charts.js +3 -3
  7. package/dist/{chip-BVB6UYl-.js → chip-B5MhXBfm.js} +2 -2
  8. package/dist/{code-editor-D-GBs2yu.js → code-editor-mULs4SL8.js} +5 -5
  9. package/dist/code-editor.js +5 -5
  10. package/dist/{code-language-items-xrgq1Am0.js → code-language-items-BWDnqUqn.js} +2 -2
  11. package/dist/components/ui/collections/server-table/server-table-phone-row.css +6 -0
  12. package/dist/components/ui/display/code/code-moonlight.css +37 -0
  13. package/dist/{components-iNCh4A6M.js → components-B_taYVcd.js} +19740 -18682
  14. package/dist/components.js +28 -28
  15. package/dist/{dialog-DFY_nsqF.js → dialog-AjCYZ2Vz.js} +4 -4
  16. package/dist/dnd.js +1 -1
  17. package/dist/{dropdown-menu-B6Yh4XJu.js → dropdown-menu-LN60hu_3.js} +1 -1
  18. package/dist/editor.js +673 -673
  19. package/dist/{empty-state-DGtwE50c.js → empty-state-Bo8G8W8G.js} +2 -2
  20. package/dist/{field-blur-Br7ij-a1.js → field-blur-Cnd6FfCH.js} +1 -1
  21. package/dist/{field-blur-context-CucMAZLj.js → field-blur-context-bUFZj8iN.js} +1 -1
  22. package/dist/{hooks-CYQJkKGc.js → hooks-Dt4PkQiZ.js} +2 -2
  23. package/dist/hooks.js +3 -3
  24. package/dist/{hover-card-C2J0GwWf.js → hover-card-QtGnvsnc.js} +1 -1
  25. package/dist/i18n.js +3 -3
  26. package/dist/{icon-badge-LzO5K6Wu.js → icon-badge-DLbpCQIm.js} +3 -3
  27. package/dist/{icon-button-vLJ4YrEW.js → icon-button-C59DqL7L.js} +1 -1
  28. package/dist/lib/components/ui/actions/fab/fab-dock-inset.d.ts +29 -0
  29. package/dist/lib/components/ui/actions/fab/fab-dock.d.ts +6 -1
  30. package/dist/lib/components/ui/collections/data-table/select-checkbox.d.ts +3 -2
  31. package/dist/lib/components/ui/collections/filter/use-filter-field-config.d.ts +3 -3
  32. package/dist/lib/components/ui/collections/index.d.ts +1 -0
  33. package/dist/lib/components/ui/collections/record-actions-menu/index.d.ts +2 -0
  34. package/dist/lib/components/ui/collections/record-actions-menu/record-actions-dialogs.d.ts +15 -0
  35. package/dist/lib/components/ui/collections/record-actions-menu/record-actions-menu.d.ts +72 -0
  36. package/dist/lib/components/ui/collections/record-actions-menu/record-actions-rules.d.ts +68 -0
  37. package/dist/lib/components/ui/collections/record-actions-menu/record-actions-words.d.ts +52 -0
  38. package/dist/lib/components/ui/collections/record-actions-menu/use-record-actions.d.ts +51 -0
  39. package/dist/lib/components/ui/collections/record-actions-menu/use-record-flight.d.ts +16 -0
  40. package/dist/lib/components/ui/collections/row-menu/row-menu-column.d.ts +4 -0
  41. package/dist/lib/components/ui/collections/row-menu/row-menu.d.ts +1 -1
  42. package/dist/lib/components/ui/collections/server-table/partials/server-table-band-classes.d.ts +7 -5
  43. package/dist/lib/components/ui/collections/server-table/partials/server-table-column-sorting.d.ts +7 -0
  44. package/dist/lib/components/ui/collections/server-table/types.d.ts +3 -1
  45. package/dist/lib/components/ui/display/markdown/markdown.d.ts +12 -1
  46. package/dist/lib/components/ui/display/markdown/markdown.utils.d.ts +2 -0
  47. package/dist/lib/components/ui/display/markdown/mermaid-csp-scope.d.ts +8 -0
  48. package/dist/lib/components/ui/display/markdown/mermaid-markup.d.ts +20 -0
  49. package/dist/lib/components/ui/display/markdown/partials/markdown-code.d.ts +5 -1
  50. package/dist/lib/components/ui/feedback/confirm-dialog/action-refusal.d.ts +24 -0
  51. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-alert.d.ts +17 -0
  52. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-description.d.ts +10 -0
  53. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-dialog.d.ts +24 -2
  54. package/dist/lib/components/ui/feedback/confirm-dialog/confirm-refusal.d.ts +7 -0
  55. package/dist/lib/components/ui/feedback/confirm-dialog/index.d.ts +1 -0
  56. package/dist/lib/components/ui/feedback/confirm-dialog/use-confirm-action.d.ts +6 -0
  57. package/dist/lib/components/ui/feedback/index.d.ts +1 -0
  58. package/dist/lib/components/ui/feedback/stale-notice/index.d.ts +1 -0
  59. package/dist/lib/components/ui/feedback/stale-notice/stale-notice.d.ts +15 -0
  60. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  61. package/dist/lib/components/ui/fields/country-field/index.d.ts +2 -0
  62. package/dist/lib/components/ui/fields/country-field/iso-countries.d.ts +2 -0
  63. package/dist/lib/components/ui/fields/index.d.ts +1 -0
  64. package/dist/lib/components/ui/fields/phone-field/index.d.ts +1 -1
  65. package/dist/lib/components/ui/fields/phone-field/parse-phone.d.ts +2 -0
  66. package/dist/lib/components/ui/fields/phone-field/phone-field.d.ts +8 -1
  67. package/dist/lib/components/ui/fields/phone-field/phone-form-field.d.ts +8 -1
  68. package/dist/lib/components/ui/fields/phone-field/use-phone-value.d.ts +2 -1
  69. package/dist/lib/components/ui/fields/postal-address/utils.d.ts +15 -0
  70. package/dist/lib/components/ui/fields/quantity-field/index.d.ts +3 -0
  71. package/dist/lib/components/ui/fields/quantity-field/quantity-field.d.ts +8 -0
  72. package/dist/lib/components/ui/fields/quantity-field/quantity-form-field.d.ts +8 -0
  73. package/dist/lib/components/ui/fields/quantity-field/quantity-row.d.ts +13 -0
  74. package/dist/lib/components/ui/fields/quantity-field/quantity-unit.d.ts +12 -0
  75. package/dist/lib/components/ui/fields/quantity-field/quantity-value.d.ts +9 -0
  76. package/dist/lib/components/ui/fields/quantity-field/types.d.ts +45 -0
  77. package/dist/lib/components/ui/forms/index.d.ts +1 -0
  78. package/dist/lib/components/ui/forms/use-server-field-errors.d.ts +21 -0
  79. package/dist/lib/components/ui/layout/index.d.ts +1 -0
  80. package/dist/lib/components/ui/layout/record-page/index.d.ts +2 -0
  81. package/dist/lib/components/ui/layout/record-page/record-details.d.ts +40 -0
  82. package/dist/lib/components/ui/layout/record-page/record-page.d.ts +27 -0
  83. package/dist/lib/components/ui/media/upload/index.d.ts +1 -0
  84. package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.d.ts +6 -1
  85. package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.errors.d.ts +8 -0
  86. package/dist/lib/components/ui/media/upload/network-upload-field/network-upload-field.utils.d.ts +2 -7
  87. package/dist/lib/components/ui/media/upload/network-upload-field/use-start-upload.d.ts +7 -3
  88. package/dist/lib/components/ui/media/upload/network-upload-field/use-upload-item-handlers.d.ts +2 -1
  89. package/dist/lib/components/ui/media/upload/s3-upload-field/s3-upload-field.d.ts +2 -5
  90. package/dist/lib/components/ui/media/upload/s3-upload-field/s3-upload-field.utils.d.ts +26 -1
  91. package/dist/lib/components/ui/media/upload/upload-error.d.ts +33 -0
  92. package/dist/lib/components/ui/selection/autocomplete/autocomplete-enter.d.ts +18 -0
  93. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-chip-input.d.ts +2 -1
  94. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-text-input.d.ts +2 -1
  95. package/dist/lib/components/ui/selection/autocomplete/types.d.ts +3 -1
  96. package/dist/lib/components/ui/widgets/chat/chat-conversation-item.d.ts +8 -2
  97. package/dist/lib/components/ui/widgets/chat/chat-markdown.d.ts +4 -0
  98. package/dist/lib/components/ui/widgets/chat/chat-message-list.d.ts +8 -1
  99. package/dist/lib/components/ui/widgets/chat/index.d.ts +2 -0
  100. package/dist/lib/components/ui/widgets/chat/partials/chat-conversation-item-text.d.ts +2 -1
  101. package/dist/lib/components/ui/widgets/chat/partials/chat-empty-thread.d.ts +2 -0
  102. package/dist/lib/components/ui/widgets/chat/proposal/chat-action-proposal-parts.d.ts +24 -0
  103. package/dist/lib/components/ui/widgets/chat/proposal/chat-action-proposal.d.ts +39 -0
  104. package/dist/lib/components/ui/widgets/chat/proposal/chat-action-proposal.rules.d.ts +17 -0
  105. package/dist/lib/components/ui/widgets/chat/proposal/chat-tool-activity.d.ts +16 -0
  106. package/dist/lib/components/ui/widgets/education/questions/partials/question-content.d.ts +1 -1
  107. package/dist/lib/components/ui/widgets/education/questions/question-text.d.ts +1 -1
  108. package/dist/lib/components/ui/widgets/education/questions/question.d.ts +1 -1
  109. package/dist/lib/components/ui/widgets/education/questions/types.d.ts +10 -0
  110. package/dist/lib/i18n/resources/en/en.d.ts +49 -0
  111. package/dist/lib/i18n/resources/en/material.json.d.ts +50 -1
  112. package/dist/lib/i18n/resources/fr/fr.d.ts +49 -0
  113. package/dist/lib/i18n/resources/fr/material.json.d.ts +50 -1
  114. package/dist/lib/i18n/resources/resources.d.ts +98 -0
  115. package/dist/lib/i18n/translator.d.ts +1 -1
  116. package/dist/lib/print/output/to-pdf-element.d.ts +6 -0
  117. package/dist/lib.js +1 -1
  118. package/dist/main.js +42 -42
  119. package/dist/map.js +8 -8
  120. package/dist/material.css +1 -1
  121. package/dist/pdf-reader.js +1 -1
  122. package/dist/print.js +135 -131
  123. package/dist/{public-BHthpdpe.js → public-BSaSCwy7.js} +9 -9
  124. package/dist/{rich-code-block-DoW7l2ND.js → rich-code-block-BFVj1GWz.js} +5 -5
  125. package/dist/{sortable-provider-FLNMfr0o.js → sortable-provider-REwyjiOZ.js} +2 -2
  126. package/dist/{spinner-DCgDw8eq.js → spinner-BFZxqdWC.js} +1 -1
  127. package/dist/theme-picker.js +2 -2
  128. package/dist/{theme-toggle-BMDAwMLh.js → theme-toggle-BHi6miz-.js} +1 -1
  129. package/dist/{toolbar-items-CkPEhQq7.js → toolbar-items-BTtoKVlx.js} +3 -3
  130. package/dist/{translate-material-DkcuQBNr.js → translate-material-R-uLthBy.js} +1 -1
  131. package/dist/{translator-DXdVRdMm.js → translator-ClbXOGQO.js} +100 -2
  132. package/dist/{typography-BNS_IbFI.js → typography-v-PJ4Zco.js} +1 -1
  133. package/dist/{use-chat-composer-B-tGRLkI.js → use-chat-composer-Rhan4Xv0.js} +2463 -2405
  134. package/dist/{use-heading-spy-DaqibCCm.js → use-heading-spy-CJnBkCY0.js} +179 -87
  135. package/dist/{use-material-translation-Bxl8sv1n.js → use-material-translation-CCWUeiEn.js} +1 -1
  136. package/dist/{use-return-focus-Y_Kx0QlW.js → use-return-focus-Duk40DZI.js} +1 -1
  137. package/docs/components/buttons-and-actions.md +24 -0
  138. package/docs/components/data-display.md +33 -0
  139. package/docs/components/data-table-filter-and-lists.md +117 -5
  140. package/docs/components/feedback-and-status.md +58 -2
  141. package/docs/components/form-fields.md +97 -3
  142. package/docs/components/media-and-files.md +55 -9
  143. package/docs/components/navigation-and-layout.md +58 -0
  144. package/docs/components/rich-widgets.md +37 -3
  145. package/docs/components/selection-and-choice.md +10 -3
  146. package/docs/guides/cards-and-tiles.md +4 -2
  147. package/docs/guides/csp.md +44 -6
  148. package/docs/guides/forms.md +55 -7
  149. package/docs/guides/i18n.md +6 -5
  150. package/docs/guides/printing.md +17 -7
  151. package/docs/previews/data-table-filter-lists-recordactionsmenu.webp +0 -0
  152. package/docs/previews/feedback-status-stalenotice.webp +0 -0
  153. package/docs/previews/form-fields-quantityfield.webp +0 -0
  154. package/docs/previews/media-files-networkuploadfield.webp +0 -0
  155. package/docs/previews/media-files-s3uploadfield.webp +0 -0
  156. package/docs/previews/media-files-uploadfield.webp +0 -0
  157. package/docs/previews/navigation-layout-recordpage.webp +0 -0
  158. package/docs/previews/rich-widgets-chat-action-proposal.webp +0 -0
  159. package/docs/roadmap.md +14 -1
  160. package/docs/troubleshooting.md +58 -11
  161. package/lib/components/ui/collections/server-table/server-table-phone-row.css +6 -0
  162. package/lib/components/ui/display/code/code-moonlight.css +37 -0
  163. package/package.json +2 -2
@@ -83,8 +83,8 @@ function Conversation({ messages }: { messages: Message[] }) {
83
83
  Parts: `ChatInbox` (`list`, `thread`, `listHeader`, `pane`), `ChatConversationItem`,
84
84
  `ChatThread`, `ChatThreadHeader`, `ChatMessageList`, `ChatMessage`,
85
85
  `ChatMessageSeparator`, `ChatReplyQuote`, `ChatReactionGroup`, `ChatComposer`,
86
- `ChatComposerAttachments`, `ChatComposerAttachmentChip`, `ChatMarkdown`,
87
- `ChatTypingIndicator`, `ChatWidget`, `chatGroupPosition`, `ChatLayoutContext`,
86
+ `ChatComposerAttachments`, `ChatComposerAttachmentChip`, `ChatMarkdown`, `Markdown` documents `math`, `mermaid` and the untrusted-input guarantees (data-display.md#markdown).
87
+ `ChatTypingIndicator`, `ChatActionProposal`, `ChatToolActivity`, `ChatWidget`, `chatGroupPosition`, `ChatLayoutContext`,
88
88
  `useChatComposerAttachments`. Built on [TypingIndicator](feedback-and-status.md#typingindicator),
89
89
  [DaySeparator](data-display.md#dayseparator) and
90
90
  [ScrollToLatestButton](buttons-and-actions.md#scrolltolatestbutton).
@@ -94,6 +94,30 @@ Parts: `ChatInbox` (`list`, `thread`, `listHeader`, `pane`), `ChatConversationIt
94
94
  [<img src="../previews/rich-widgets-chat-composer.webp" height="96" alt="ChatComposer">](#chat-kit)
95
95
  [<img src="../previews/rich-widgets-chat-widget.webp" height="96" alt="ChatWidget">](#chat-kit)
96
96
  [<img src="../previews/rich-widgets-chat-advanced.webp" height="96" alt="ChatComposer with mentions">](#chat-kit)
97
+ [<img src="../previews/rich-widgets-chat-action-proposal.webp" height="96" alt="ChatActionProposal">](#chat-kit)
98
+
99
+ An assistant's proposed action and its tool steps, inside a list:
100
+
101
+ ```tsx
102
+ import {
103
+ ChatActionProposal,
104
+ ChatMessageList,
105
+ ChatToolActivity,
106
+ } from '@nxgt/material/components';
107
+
108
+ <ChatMessageList>
109
+ <ChatToolActivity name="find_absences" state="ok" />
110
+ <ChatActionProposal
111
+ title="Mark 12 absences as justified"
112
+ summary="Class 5B, 3 to 7 November."
113
+ deadline="2026-11-02T16:00:00Z"
114
+ status={status} // 'pending' until answered
115
+ busy={sending}
116
+ onConfirm={confirm}
117
+ onReject={reject}
118
+ />
119
+ </ChatMessageList>;
120
+ ```
97
121
 
98
122
  | Prop | Type | Default | Effect |
99
123
  | --- | --- | --- | --- |
@@ -105,8 +129,16 @@ Parts: `ChatInbox` (`list`, `thread`, `listHeader`, `pane`), `ChatConversationIt
105
129
  | `ChatMessage` `onReply` / `onReact` / `onDelete` / `menuItems` | callbacks / `ReactNode` | — | the toolbar shown on hover or focus (a tap on touch): react, reply, copy, and a "more" menu |
106
130
  | `ChatMessage` `reactions` / `onReactionToggle` | `ChatReaction[]` | — | the reaction pills under the bubble |
107
131
  | `ChatMessage` `isStreaming` / `copyable` | `boolean` | `false` / `true` | streaming caret; Copy in the toolbar |
132
+ | `ChatActionProposal` `title` / `summary` / `toolLabel` / `children` | `ReactNode` | — | a card for an action the assistant proposes: what, its effect, the tool behind it (a slot) and extra detail |
133
+ | `ChatActionProposal` `status` | `'pending' \| 'running' \| 'confirmed' \| 'failed' \| 'rejected' \| 'expired'` | `'pending'` | a toned badge with its icon and words (colour is never the only signal); only `pending` shows the buttons; leaving `pending` moves the focus to the status |
134
+ | `ChatActionProposal` `onConfirm` / `onReject` | `() => void` | — | the Confirm (filled) and Reject (tonal) buttons; `confirmLabel` / `rejectLabel` reword them; `actions` replaces both |
135
+ | `ChatActionProposal` `busy` / `disabled` | `boolean` | `false` | an answer is being sent (both disabled, Confirm spins) / a read-only viewer |
136
+ | `ChatActionProposal` `deadline` | `Date \| string \| number` | — | "Respond by …" in a `<time>`, in the active language; `expired` says "Expired on" |
137
+ | `ChatActionProposal` `failureCode` / `failureMessage` | `string` / `ReactNode` | — | a `failed` card's reason, in an `alert`, with the code |
138
+ | `ChatToolActivity` `name` / `state` / `detail` | `ReactNode` / `'running' \| 'ok' \| 'failed'` / `ReactNode` | — / `'running'` | one compact row for a tool step in a `ChatMessageList`: the name in mono, Running (spinner), Done or Failed |
108
139
  | `ChatThread` / `ChatMessageList` `layout` | `'bubbles' \| 'flat'` | `'bubbles'` | a messenger's bubbles, or a team chat's rows (name and time over each run) |
109
140
  | `ChatThread` / `ChatMessageList` `density` | `'comfortable' \| 'compact'` | `'comfortable'` | tighter bubbles, smaller avatars |
141
+ | `ChatMessageList` `viewportProps` | `ComponentProps` of the viewport | `{ tabIndex: 0 }` | attributes of the scrolling element; it is a tab stop so the keyboard can scroll the transcript (`tabIndex: -1` opts out) |
110
142
  | `ChatThread` `surface` | `'plain' \| 'card'` | `'plain'` | fills its parent, or draws a rounded bordered card |
111
143
  | `ChatThread` `composerProps` | `ChatComposerProps` | — | renders a `ChatComposer` under the list |
112
144
  | `ChatMessageList` `isTyping` / `typingAvatar` | `boolean` / `ReactNode` | `false` | a typing bubble at the end of the list |
@@ -117,6 +149,7 @@ Parts: `ChatInbox` (`list`, `thread`, `listHeader`, `pane`), `ChatConversationIt
117
149
  | `ChatComposer` `sendOnEnter` | `boolean` | `true` | Enter sends and Shift+Enter breaks the line; `false`: Ctrl/⌘+Enter sends. Never while an IME composes |
118
150
  | `ChatComposer` `emojis` / `mentions` | `EmojiItem[]` / `MentionItem[]` | — | enables the emoji picker and the `@` list |
119
151
  | `ChatThreadHeader` | `title`, `subtitle`, `avatarSrc`, `status`, `actions`, `onBack` | — | the title bar; inside a `ChatInbox` it carries the back button on a narrow screen |
152
+ | `ChatConversationItem` `title` / `titleText` | `ReactNode` / `string` | — | the clipped title carries its full text in a `title` attribute when it is a string; `titleText` gives it when `title` is a node |
120
153
  | `ChatConversationItem` `status` / `isTyping` | `StatusTone` / `boolean` | — | presence dot; "Typing…" instead of the preview |
121
154
  | `ChatInbox` `pane` / `defaultPane` | `'list' \| 'thread'` | `'list'` | which pane shows below the `@md` container width |
122
155
  | `ChatWidget` `open` / `defaultOpen` / `onOpenChange` | `boolean` | `false` | the floating launcher's panel (full screen below `sm`); `unreadCount` on the launcher, `side`, `strategy` |
@@ -2457,9 +2490,10 @@ French bundle — see [i18n](../guides/i18n.md).
2457
2490
  | `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 |
2458
2491
  | `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` |
2459
2492
  | `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`) |
2493
+ | `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 |
2460
2494
  | `QuestionCard` `readOnly` / `busy` | `boolean` | `false` | `readOnly`: an answer already submitted, shown without the revealed marks; `busy`: read-only and `aria-busy` while it is saved; every kind honours both |
2461
2495
  | new strings | `material.education.kind-open`, `kind-code`, `code-placeholder` | — | English fallbacks and a French bundle |
2462
- | `QuestionCard` `variant` / `density` / `showShortcuts` / `number` / `showFeedback` | — | `'card'`, `'comfortable'`, `false`, —, `true` | framed or plain, spacing, key badges, "Question 2", the verdict banner |
2496
+ | `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`) |
2463
2497
 
2464
2498
  An open answer and a code answer, with the code editor from its own subpath
2465
2499
  (it is client-only, so load it lazily in an SSR app):
@@ -417,7 +417,7 @@ disabled with the field; Backspace in the empty input removes the last one.
417
417
  | `getOptionValue`, `getOptionLabel` | `(option: T) => string` | `.value`, `.label` | for other option shapes |
418
418
  | `getOptionLeading` | `(option: T) => ReactNode` | — | an avatar (or a flag) before the label, in the list and in the chip; `renderOption` replaces the whole row |
419
419
  | `filter` | `'accent-insensitive' \| 'none' \| (option: T, query: string) => boolean` | `'accent-insensitive'`, `'none'` with `onSearch` | how the list is filtered as the user types: label or value, ignoring case and accents (`e` finds "École") in the provider's language; `'none'` shows `options` as given; a function gets the trimmed query, never an empty one |
420
- | `freeSolo` | `boolean` | — | accept a typed value that is not an option |
420
+ | `freeSolo` | `boolean` | — | accept a typed value that is not an option; Enter adds it |
421
421
  | `onSearch` | `(query: string, { signal }: { signal: AbortSignal }) => Promise<T[]>` | — | server search: called once typing pauses; its results become the list. Each search aborts the previous one, and a response that lands after a newer one is dropped |
422
422
  | `onSearchError` | `(error: unknown) => void` | — | `onSearch` rejected (an abort is not a rejection) |
423
423
  | `minQueryLength` | `number` | `1` | `onSearch` waits for this many characters; below it the list says "Type at least N characters to search." |
@@ -433,6 +433,12 @@ disabled with the field; Backspace in the empty input removes the last one.
433
433
  | `disabled` | `boolean` | — | disables the input and each chip's remove button |
434
434
  | `error` | `boolean` | — | error border on the chips box, error helper text, `aria-invalid` on the input |
435
435
 
436
+ Enter in a form: it acts on the list and does not submit the form while it has
437
+ something to do (WAI-ARIA combobox practice). It picks the highlighted option;
438
+ with `freeSolo` and no option highlighted it adds the typed text; with the list
439
+ open it is only default-prevented. On a closed list with nothing typed it
440
+ submits, as a plain input does.
441
+
436
442
  An avatar in the rows and the chips:
437
443
  `getOptionLeading={(person) => <Avatar src={person.photo} alt="" />}`; see
438
444
  [the field look](../guides/field-look-2-x.md#autocomplete-and-the-chips-box).
@@ -471,8 +477,9 @@ text, as a `Select` shows it, and keeps the search:
471
477
  gets the label back on leave: the value can only be replaced by another pick.
472
478
  - The option's `getOptionLeading` (or `renderSelectedLeading`) avatar shows
473
479
  before the text while it is picked.
474
- - `freeSolo` still adds through its "Add" button; text left in the input and
475
- not added is dropped on leave, as any other typed text.
480
+ - `freeSolo` adds through its "Add" button, or with Enter when no option is
481
+ highlighted; text left in the input and not added is dropped on leave, as
482
+ any other typed text.
476
483
  - Keep the selected option in `options` (as with `onSearch` and chips): a value
477
484
  with no matching option has no label, so the input stays empty.
478
485
  - The input is the combobox: `aria-expanded` follows the list,
@@ -146,7 +146,9 @@ sort control writes it), the cursor paging in the footer, `bulkActions` and the
146
146
  selection kept across pages, the empty and error pages. An empty page has no
147
147
  list bar over it, and the focus a bulk action's `clear()` hands back goes to the
148
148
  list. The sort control names its columns as in a `DataTable`: set `meta.label`
149
- on a column whose header is a render function (see [Sort](#sort)).
149
+ on a column whose header is a render function (see [Sort](#sort)). Without
150
+ `sorting`, no column is sortable (the server sorts, so pass `sorting` to let people
151
+ sort), and the phone list bar shows only for selection.
150
152
 
151
153
  ```tsx
152
154
  import { ServerTable } from '@nxgt/material/components';
@@ -341,7 +343,7 @@ the `Guides/Recipes/Status board` story, and the Claude Code plugin's
341
343
 
342
344
  ## Troubleshooting
343
345
 
344
- **The sort control is missing, or an option reads like the column id.** The control needs a column that can be sorted (an accessor, `enableSorting` not `false`); an option is the column's `meta.label`, else a string `header`. Set `meta.label`.
346
+ **The sort control is missing, or an option reads like the column id.** The control needs a column that can be sorted (an accessor, `enableSorting` not `false`); an option is the column's `meta.label`, else a string `header`. Set `meta.label`. On a `ServerTable` the control is also missing when `sorting` is not passed: no column is sortable without it. Pass `sorting` (the server sorts).
345
347
 
346
348
  **The phone shows the table for a moment.** `{ phone, wide }` renders `wide` on
347
349
  the server and for the first client render, so that hydration matches; a phone
@@ -28,7 +28,7 @@ Content-Security-Policy:
28
28
  | `style-src-elem 'self' 'nonce-X'` | Your stylesheet and the `<style>` elements that carry the nonce. No component needs `'unsafe-inline'` here, apart from the [known gaps](#known-gaps): see [the `<style>` elements the components inject](#style-elements-the-components-inject) |
29
29
  | `style-src-attr 'unsafe-inline'` | The `style="…"` attributes in the server-rendered HTML and those set through `setAttribute('style', …)`. CSP blocks only these two; `el.style.x = …` and `style.setProperty` are not blocked. An attribute cannot carry a nonce, so this is the only way to allow it. It does not allow `<style>` elements or scripts |
30
30
  | `font-src 'self'` | The library ships no web font (host Inter yourself: [Inter / the sans font](#inter--the-sans-font)), and KaTeX's fonts are files, never `data:` URIs: see [KaTeX fonts](#katex-fonts) for the one Vite setting the Tailwind route needs |
31
- | `img-src 'self' data: blob:` | `data:` for the cropper's output, a captured `Camera` photo, a `Markdown` mermaid diagram and the PDF reader's annotation icons (SVG `data:` images; the icons the components draw are inline `<svg>` elements and need none); `blob:` for previews of a file the user picked. Drop what you do not use |
31
+ | `img-src 'self' data: blob:` | `data:` for the cropper's output, a captured `Camera` photo, and the PDF reader's annotation icons (SVG `data:` images; the icons the components draw are inline `<svg>` elements and need none); `blob:` for previews of a file the user picked and for a `Markdown` mermaid diagram (an SVG image from a `blob:` URL). Drop what you do not use |
32
32
  | `worker-src 'self'` | The pdf.js worker of `PdfReader`: the build ships it as a file, `dist/pdf.worker.min.mjs`, referenced relative to the module that loads it, so `'self'` is enough with no setting and no `data:`. `configurePdfReader({ workerSrc })` overrides it ([PDF reader guide](pdf-reader.md#content-security-policy)). Monaco: see below |
33
33
  | `connect-src 'self'` | `PdfReader` fetches the PDF from its `url` and its pdf.js assets from `assetsUrl`: add those origins when they are not yours |
34
34
 
@@ -38,7 +38,7 @@ Components outside this table need nothing more. Measured per component:
38
38
  | --- | --- | --- |
39
39
  | `PdfReader` | `worker-src 'self'` (the worker is a file of the package), `connect-src` for the PDF's and the assets' origins, and `script-src 'wasm-unsafe-eval'` for the JPEG 2000 and ICC WebAssembly decoders (without it: a JavaScript fallback for JPEG 2000, device colours for ICC profiles) | worker checked in the same Playwright run (Vite production build under `worker-src 'self'`), the rest read from pdf.js's code: [PDF reader guide](pdf-reader.md#content-security-policy) |
40
40
  | `CodeEditor`, `CodeDiff` (Monaco) | `@monaco-editor/react` loads Monaco from `https://cdn.jsdelivr.net/npm/monaco-editor@…/min/vs` by default: that host in `style-src-elem` (Monaco's CSS) and `font-src` (its icon font), plus `worker-src blob:` | not yet verified in a browser. Safe default: allow `https://cdn.jsdelivr.net` in `script-src`, `style-src-elem` and `font-src`, and `worker-src blob:`; or stay on `'self'` by [serving Monaco yourself](#monaco-on-self) |
41
- | `Markdown` with a `mermaid` block | mermaid is imported lazily and runs with `securityLevel: 'strict'` | whether mermaid needs `'unsafe-eval'` is not yet verified. Safe default: keep `'unsafe-eval'` out and report any violation you see; its SVG carries a `<style>` element, see [gaps](#known-gaps) |
41
+ | `Markdown` (maths, mermaid, code) | nothing beyond the policy above, in a client render | checked in Chromium under that policy with `style-src-attr 'none'`, no `img-src data:` and no `'unsafe-eval'`: [Markdown under a strict CSP](#markdown-katex-mermaid-and-code-under-a-strict-csp) |
42
42
  | `RichTextViewer` (`@nxgt/material/editor/viewer`) | none, but it emits `style` attributes: a text colour, a highlight, a text alignment or a table cell's background in the saved document becomes an inline `style` (only allow-listed properties with plain values survive), so a page that shows such a document needs `style-src-attr 'unsafe-inline'`. A document with none sends no style | measured by reading the renderers; a strict `style-src-attr 'none'` page shows the document without those colours |
43
43
  | `Camera` | none: `getUserMedia` is not governed by CSP; the captured photo is a `data:` image | `media-src` for the live preview is not yet verified. Safe default: `media-src 'self' blob:` (a `srcObject` stream usually needs nothing) |
44
44
  | `MediaPlayer` with `hlsStreaming()` | `connect-src` and `media-src blob:` for the stream; `worker-src blob:` only with `config: { enableWorker: true }` (the adapter defaults to no worker) | see the [streaming guide](media-streaming.md#content-security-policy) |
@@ -201,8 +201,8 @@ instead.
201
201
  `style-src-elem 'self' 'nonce-X'` is enough for every component below: your
202
202
  stylesheet carries the CSS the dependencies used to inject, and under a
203
203
  `NonceProvider` the few `<style>` elements left carry the nonce. No route
204
- needs `'unsafe-inline'` in `style-src-elem` for them; mermaid and pdf.js's
205
- font fallback are the [known gaps](#known-gaps).
204
+ needs `'unsafe-inline'` in `style-src-elem` for them; the mindmap diagrams of
205
+ mermaid and pdf.js's font fallback are the [known gaps](#known-gaps).
206
206
 
207
207
  | Component | What it needs | How |
208
208
  | --- | --- | --- |
@@ -261,12 +261,50 @@ when the page carries it nowhere. `setCspNonce` does nothing on a server (no
261
261
  `document`), so a request's nonce never lands in a module global there: see
262
262
  [SSR](#ssr).
263
263
 
264
+ ### Markdown (KaTeX, mermaid and code) under a strict CSP
265
+
266
+ The full `Markdown` (and `ChatMarkdown`) needs no relaxed directive: no
267
+ `'unsafe-inline'`, no `'unsafe-eval'`, no `style-src-attr`, no `img-src data:`.
268
+ Only `img-src blob:` (already in the policy above) for the diagrams. The
269
+ `Data display/Markdown > StrictCsp` story plays maths, two flowcharts (one with
270
+ an `<img>` in a label, removed before the render, since mermaid would load it in the page) and a code block in a document served
271
+ `script-src 'self' 'nonce-X'` (no `'unsafe-eval'`),
272
+ `style-src-elem 'self' 'nonce-X'`, `style-src-attr 'none'`,
273
+ `img-src 'self' blob:` and `font-src 'self'`, and expects not one violation.
274
+ What the app does: wrap the app in [`NonceProvider`](#nonceprovider) (or let
275
+ the page carry the nonce in its meta tag).
276
+
277
+ - **Code blocks**: the scheme's colours are classes of the stylesheet
278
+ (`data-scheme`), and `maxHeight` is a custom property written through the
279
+ CSSOM in a layout effect (a server-rendered block is full height until
280
+ hydration).
281
+ - **KaTeX**: on a client render React writes its styles through the CSSOM.
282
+ Its fonts are files ([KaTeX fonts](#katex-fonts)).
283
+ - **Mermaid**: the diagram stays an image, now from a `blob:` URL (revoked when
284
+ the diagram goes), so the SVG's own `<style>` and `style` attributes live in
285
+ the image and the page needs neither `data:` nor `style-src-attr` for them.
286
+ While a diagram renders, mermaid builds it in a temporary element of the
287
+ page and sanitises it with DOMPurify, which a strict policy reports. For
288
+ that render only, and only for the elements of that diagram, a short-lived
289
+ scope gives mermaid's `<style>` the nonce, writes its `style` attributes
290
+ through the CSSOM and parks the `style` attributes of the string DOMPurify
291
+ parses (patches of `Document.prototype.createElement`,
292
+ `Element.prototype.setAttribute` and `DOMParser.prototype.parseFromString`;
293
+ each is a pass-through once the last render ends and is restored only if
294
+ no other script patched it since). Unrelated calls, such as an editor
295
+ parsing HTML, are left alone.
296
+ - **Left**: a page whose HTML the server renders with math carries KaTeX's
297
+ `style="…"` attributes in the markup, which `style-src-attr 'none'` blocks
298
+ (the formula loses its spacing until `style-src-attr 'unsafe-inline'`, or a
299
+ client-only render of the `Markdown`). Mermaid's mindmap and architecture
300
+ diagrams add a `<style>` to `<head>` ([known gaps](#known-gaps)).
301
+
264
302
  ### Known gaps
265
303
 
266
304
  Not fixable by configuration yet, and still reported by a strict policy:
267
305
 
268
- - the `<style>` element inside mermaid's SVG, and the one cytoscape adds to
269
- `<head>` for mermaid's mindmap and architecture diagrams;
306
+ - the `<style>` element cytoscape adds to `<head>` for mermaid's mindmap and
307
+ architecture diagrams (flowcharts, sequences and the like add none);
270
308
  - the font fallback of pdf.js, when a PDF font is not embedded;
271
309
  - this repository's Storybook, or an app that imports react-use's barrel
272
310
  itself on a dev server: react-use served as one module, whose `useCss`
@@ -115,7 +115,7 @@ export function PlanForm() {
115
115
  | The field | Touched when |
116
116
  | --- | --- |
117
117
  | one input (`TextFormField`, `EmailFormField`, `TextareaFormField`, `SwitchFormField`, `CheckboxFormField`, …) | its input loses focus, as a native `blur` |
118
- | several controls (`OpeningHoursFormField`, inline by default: focus leaves the week editor and its phone sheet; `ContactsFormField`, inline by default: focus leaves the rows and the phone sheet; `AttributesFormField`, inline by default: focus leaves the rows, their open fields and the phone sheet; `PasswordFormField` and its eye button, `NumberFormField` and its steppers, `PhoneFormField`, `OtpFormField`, `SliderFormField` with two thumbs, `DurationFormField`, `RatingFormField`, `RadioGroupFormField`, `CheckboxGroupFormField`, `ChipGroupFormField`, `ChoiceCardGroupFormField`, `SortableListFormField`, `TransferListFormField`, `InlineEditFormField`, `LocalizedTextFormField`, `AttributeValuesFormFieldSet`, …) | focus leaves all of them; moving between them is not leaving |
118
+ | several controls (`OpeningHoursFormField`, inline by default: focus leaves the week editor and its phone sheet; `ContactsFormField`, inline by default: focus leaves the rows and the phone sheet; `AttributesFormField`, inline by default: focus leaves the rows, their open fields and the phone sheet; `PasswordFormField` and its eye button, `NumberFormField` and its steppers, `QuantityFormField`, `PhoneFormField`, `OtpFormField`, `SliderFormField` with two thumbs, `DurationFormField`, `RatingFormField`, `RadioGroupFormField`, `CheckboxGroupFormField`, `ChipGroupFormField`, `ChoiceCardGroupFormField`, `SortableListFormField`, `TransferListFormField`, `InlineEditFormField`, `LocalizedTextFormField`, `AttributeValuesFormFieldSet`, …) | focus leaves all of them; moving between them is not leaving |
119
119
  | a picker (`SelectFormField`, `ComboboxFormField`, `AutocompleteFormField`, `TreeSelectFormField`, `CountryFormField`, `TimezoneFormField`, `ColorFormField`, `DateFormField`, `DateRangeFormField`, `TimeFormField`, …) | focus leaves the trigger **and** the popover it opened. Opening the popover, or picking in it, is not leaving; closing it hands focus back to the trigger, and the next Tab or click elsewhere is. A typed `DateFormField`, `DateRangeFormField` or `TimeFormField` is one trigger: moving between its segments (both ends of a range) and its buttons is not leaving |
120
120
  | a dialog editor (`PostalAddressFormField`, `ContactsFormField` with `editing="dialog"`, `OpeningHoursFormField` with `editing="dialog"`, `AttributesFormField` with `editing="dialog"`, and `LocationFormField` from `@nxgt/material/map`) | focus leaves its buttons and the dialog they open. Closing the dialog hands focus back to the button that opened it |
121
121
  | an upload (`UploadFormField`, `NetworkUploadFormField`, `S3UploadFormField`, `ImageFormField`, `ImageS3FormField`, `FileListFormField`, `FileListS3FormField`) | focus leaves the drop zone and its buttons. Choosing a file in the browser's own dialog is not leaving |
@@ -485,8 +485,9 @@ or fails validation.
485
485
  | `PasswordField` · `PasswordFormField` | `string` | `''` |
486
486
  | `NumberField` · `NumberFormField` | `number \| null`. `null` when cleared; clamped to `min` / `max` on blur | `null` |
487
487
  | `PercentField` · `PercentFormField` | `number \| null`, `min` 0, `max` 100, `step` 1 by default | `null` |
488
+ | `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: '' }` |
488
489
  | `CurrencyField` · `CurrencyFormField` | `number \| null`, the amount only, not rounded. `currency` (default `'EUR'`) and `fractionDigits` (the currency's own decimals by default) are display | `null` |
489
- | `PhoneField` · `PhoneFormField` | E.164 `string` (`'+33612345678'`), or `null` while the number is not yet a possible one. `defaultCountry` is `'FR'` | `null` |
490
+ | `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` |
490
491
  | `UrlField` · `UrlFormField` | `string` (as typed) | `''` |
491
492
  | `SearchField` · `SearchFormField` | `string`, after `debounceMs` (default `0`) | `''` |
492
493
  | `ColorField` · `ColorFormField` | `string` (hex, `'#1e293b'`) | `''` |
@@ -535,7 +536,7 @@ or fails validation.
535
536
  | Field · wrapper | Form value | Empty default |
536
537
  | --- | --- | --- |
537
538
  | `NetworkUploadField` · `NetworkUploadFormField` | `mode="single"`: the `string` your `onUpload` resolved (a URL or a key), or `null` after removal. `mode="multiple"`: `string[]` | `undefined` / `[]` |
538
- | `S3UploadField` · `S3UploadFormField` | as above; the value is `publicUrl` from `getPresignedUrl` | `undefined` / `[]` |
539
+ | `S3UploadField` · `S3UploadFormField` | as above; the value is `publicUrl` (PUT) or `value` (POST) from `getPresignedUrl` | `undefined` / `[]` |
539
540
  | `ImageField`, `ImageS3Field` · `ImageFormField`, `ImageS3FormField` | `string \| null` (the uploaded URL) | `undefined` |
540
541
  | `FileListField`, `FileListS3Field` · `FileListFormField`, `FileListS3FormField` | `string[]` | `[]` |
541
542
  | `FilesFormField` | `mode="single"`: `string \| null`. `mode="multiple"`: `string[]` (typed or picked URLs) | `null` / `[]` |
@@ -785,8 +786,9 @@ form.setFocus('email'); // or without an error
785
786
 
786
787
  ## A form that is quick to fill
787
788
 
788
- `useFastForm` and `useFocusOnRefusal` (`@nxgt/material/components`) are the
789
- two habits of a record form, in one place. `useFastForm(props, first, active?)`
789
+ `useFastForm`, `useFocusOnRefusal` and `useServerFieldErrors`
790
+ (`@nxgt/material/components`) are the three habits of a record form, in one
791
+ place; the first two are below, the third under [Server errors](#server-errors). `useFastForm(props, first, active?)`
790
792
  is `useForm` with `mode: 'onTouched'` (an error shows once the user leaves the
791
793
  field, then follows each change; a `mode` in `props` wins) that also focuses
792
794
  the `first` field when the form opens, so typing starts at once. It waits a
@@ -820,8 +822,24 @@ Storybook: `Guides/Forms/useFastForm`. If the focus does not move, see
820
822
 
821
823
  ## Server errors
822
824
 
823
- Map an API's field errors onto the form with `setError`, and put anything that
824
- belongs to no field under `root`, shown in an `Alert`:
825
+ `useServerFieldErrors(form, fieldErrors)` (`@nxgt/material/components`) puts an
826
+ action's `{ path: message }` field errors on the form, each under its field
827
+ with `type: 'server'`, and focuses the first. It runs when the `fieldErrors`
828
+ object changes, so pass the answer's own object (one per answer) and
829
+ `undefined` while a request runs or when the answer is not a refusal; entries
830
+ with an empty message are skipped, and the keys must be names of the form's
831
+ fields.
832
+
833
+ ```tsx
834
+ import { useFastForm, useServerFieldErrors } from '@nxgt/material/components';
835
+
836
+ const form = useFastForm<Values>({ defaultValues }, 'email');
837
+ useServerFieldErrors(form, actionData?.fieldErrors); // { email: 'Already used' }
838
+ ```
839
+
840
+ It pairs with `useFastForm` and `useFocusOnRefusal` above. To write the same
841
+ mapping by hand, or to put anything that belongs to no field under `root`,
842
+ shown in an `Alert`, use `setError`:
825
843
 
826
844
  ```tsx
827
845
  import { useForm } from 'react-hook-form';
@@ -869,6 +887,36 @@ export function SignInForm() {
869
887
  - `Alert` `variant` is one of `primary`, `secondary`, `error`, `success`,
870
888
  `info`, `warning`, `foreground`.
871
889
 
890
+ ### A save refused because another write came first
891
+
892
+ When the API refuses a versioned save (`stale-version`), keep the form as it is
893
+ and show a `StaleNotice` above it. It takes the focus and scrolls into view on
894
+ mount, and its one action reads the record again. There is no overwrite.
895
+
896
+ ```tsx
897
+ import type { FormEventHandler } from 'react';
898
+ import type { UseFormReturn } from 'react-hook-form';
899
+ import { Button, StaleNotice, TextFormField } from '@nxgt/material/components';
900
+
901
+ type Props = {
902
+ form: UseFormReturn<{ title: string }>;
903
+ stale: boolean; // the save was refused with `stale-version`
904
+ reload: () => void; // read the record again
905
+ reloading: boolean;
906
+ onSubmit: FormEventHandler<HTMLFormElement>;
907
+ };
908
+
909
+ function EditNote({ form, stale, reload, reloading, onSubmit }: Props) {
910
+ return (
911
+ <form onSubmit={onSubmit} className="grid gap-4">
912
+ {stale && <StaleNotice onReload={reload} busy={reloading} />}
913
+ <TextFormField control={form.control} name="title" label="Title" />
914
+ <Button type="submit">Save</Button>
915
+ </form>
916
+ );
917
+ }
918
+ ```
919
+
872
920
  ## Dialogs and steps
873
921
 
874
922
  ### A form in a dialog
@@ -236,7 +236,7 @@ the camera's button, toggle and image (`material.camera.capture`, `captured`,
236
236
  barcode accessible names, the calendar's navigation tooltips
237
237
  (`material.calendar.*`), the data table's selection checkboxes, the filter's
238
238
  presets and `Min`/`Max` fields, the `variant="4"` spinner's label,
239
- `ScrollToTop`, the network upload field's `Retry`/`Remove`, the map's
239
+ `ScrollToTop`, the network upload field's `Retry` and `Remove {name}`, the map's
240
240
  `You are here` and zoom buttons, and the icon-only close button of
241
241
  `DialogClose`, `DrawerClose`, `SheetClose` and `SheetContent`'s
242
242
  `withCloseButton`. A range `Slider`'s readout inputs read
@@ -320,9 +320,9 @@ those words as an optional third argument); the `aria-label` of `Breadcrumb`
320
320
  and `Pagination` and the breadcrumb ellipsis's "More"
321
321
  (`material.breadcrumb.*`, `material.pagination.label`), and `DocHeader`'s
322
322
  breadcrumb (`material.doc.breadcrumb`); `MessageCard`'s "Unread"
323
- (`material.cards.unread`); the network upload field's "Replace {name}", "Upload
323
+ (`material.cards.unread`); the network upload field's "Replace {name}", "Remove {name}", "Upload
324
324
  failed", its type, size and count errors (`material.upload.replace-named`,
325
- `failed`, `invalid-type`, `too-large`, `max-files`; an error keeps the language
325
+ `remove-named`, `failed`, `invalid-type`, `too-large`, `max-files`; an error keeps the language
326
326
  it was raised in) and the drop zone's "Max {size}" and "Up to {count} files"
327
327
  (`material.upload.dropzone.max-size` / `max-files`); the editor's
328
328
  placeholder, which follows a language change without rebuilding the editor,
@@ -485,8 +485,9 @@ and description and against the English ones, ignoring accents: in French,
485
485
 
486
486
  Some strings are still English in every language, among them the filter's
487
487
  validation messages, the camera's error messages, the editor's callout type
488
- names and default block content. File sizes (`2 KB`) and the filter chips' dates are not formatted for
489
- the provider's language.
488
+ names and default block content. The filter chips' dates, and file sizes outside the upload field (`2 KB`),
489
+ are not formatted for the provider's language; the upload field writes its
490
+ size in it (`10,4 ko`).
490
491
 
491
492
  The emoji picker's category tabs are icon-only; their name, read by screen
492
493
  readers and shown as a tooltip, comes from `material.emoji.groups.<group>`.
@@ -1013,7 +1013,7 @@ it:
1013
1013
  | `heading` | `heading(text, level?)` | A title, level `1`, `2` or `3` |
1014
1014
  | `text` | `text(content, { variant?, style? })` | A paragraph: a string, or runs `{ text, bold?, italic?, color?, href? }`; `variant` is `'body'`, `'muted'`, `'caption'` or `'label'` |
1015
1015
  | `stack` / `row` | `stack(children, { gap?, style?, keepTogether? })`, `row(children, { gap?, align?, justify?, style? })` | Children one under the other, or side by side |
1016
- | `section` | `section(title, children, { keepTogether? })` | A titled block, as `PrintSection`; `keepTogether` as on a `stack` |
1016
+ | `section` | `section(title, children, { keepTogether? })` | A titled block, as `PrintSection`; `keepTogether` as on a `stack`; a block taller than a page breaks normally, see [Keeping a block on one page](#keeping-a-block-on-one-page) |
1017
1017
  | `table` | `table({ columns, rows, footer?, labelTotals?, striped?, repeatHeader?, keepRowsWhole?, emptyText?, hideWhenEmpty? })`, or `dataTable(columns, rows, options?)` from [column definitions](#tables-from-column-definitions-datatable-and-keyvalueof) | A table of `PrintValue`s, as `PrintDataTable`; a row or a cell may carry a `style`, see [Row and cell styles](#row-and-cell-styles-in-a-table); with `labelTotals`, an empty first cell of a `footer` row reads "Total" in the document's language |
1018
1018
  | `keyValue` | `keyValue(items, { columns?, keepEmpty? })`, or `keyValueOf(fields, data, options?)` from column definitions | Label and value pairs, as `PrintKeyValue` |
1019
1019
  | `totals` | `totals(rows)` | Label and value rows; `emphasis` makes one bold |
@@ -1140,12 +1140,22 @@ section(group.title, rows, { keepTogether: true });
1140
1140
  stack([heading('Result'), table(result)], { keepTogether: true });
1141
1141
  ```
1142
1142
 
1143
- Use it on a block you know is shorter than a page. react-pdf cannot split a
1144
- block that opted out of wrapping: a block taller than the page area is not
1145
- broken, it overflows the page and is cut off at the edge (react-pdf logs
1146
- "can't wrap between pages and it's bigger than available page height").
1147
- Leave `keepTogether` off for a block that may grow past a page, and use
1148
- `keepRowsWhole` and `repeatHeader` on a long table instead.
1143
+ A block taller than a page breaks normally. react-pdf cannot split a block
1144
+ that opted out of wrapping (it overflows and is cut off at the edge), and
1145
+ cannot measure ahead, so for a document as data `renderPdfBuffer`,
1146
+ `renderPdfBlob`, `renderPdfStream`, `downloadPdf`, `openPdf` and `pdfResponse`
1147
+ first render each `keepTogether` block alone, on a page like the one it sits
1148
+ on, and drop the option of those that need more than one page. A block that
1149
+ fits keeps it, so its output does not change; a document with no
1150
+ `keepTogether` pays for no extra pass. The measure is made at the page's
1151
+ width: a block inside a `row` is measured with that row, but a parent's padding or narrower style is not applied.
1152
+
1153
+ This holds for a document as data only. `<PrintSection keepTogether>` in a
1154
+ `PrintDocument` element, and a document you render yourself with
1155
+ `renderPdfElement`, are not measured: use `keepTogether` there on a block
1156
+ shorter than a page, or call `resolveKeepTogether(doc)` from
1157
+ `@nxgt/material-core/pdf/render` first and render its result. For a long
1158
+ table, `keepRowsWhole` and `repeatHeader` keep the rows apart from each other.
1149
1159
 
1150
1160
  ### Row and cell styles in a table
1151
1161
 
package/docs/roadmap.md CHANGED
@@ -6,6 +6,8 @@ version they shipped in, and planned entries may name the major they target.
6
6
 
7
7
  ## Now
8
8
 
9
+ - **Calculator** — `@nxgt/material/calculator` adds `Calculator`, in six variants (Standard, Scientific, point of sale, Financial, Unit conversion, Programmer) chosen by `variant` or by its own switcher, and `CalculatorField` / `CalculatorFormField`, a number field whose button opens it in a popover, or a bottom sheet on a phone. Every variant shows the whole expression above the result, honours operator precedence and computes in exact decimals (0.1 + 0.2 = 0.3) without `eval`, so a strict CSP holds. A history tape (persisted, with an opt-out), memory keys, the physical keyboard, copy and paste, locale formatting and screen-reader announcements come with each; haptic and sound feedback are opt-in. Point of sale charges through `onCharge(total)`; currency rates come from the app. React only for now; the Vue port is tracked in [#2100](https://github.com/softistx/nxgt-material/issues/2100). Tracked in [#2101](https://github.com/softistx/nxgt-material/issues/2101).
10
+
9
11
  - **Barcodes and label sheets** — `@nxgt/material/barcode` draws Code 128
10
12
  and Data Matrix as accessible SVG (themed, dark-aware, black on white in
11
13
  print) and lays labels out on a sheet to print: label size, columns, rows,
@@ -15,6 +17,10 @@ version they shipped in, and planned entries may name the major they target.
15
17
  - **Toast texts follow app translations** — the toast presets read the
16
18
  app's `I18nProvider` `resources`, so an override under `material.*`
17
19
  changes their wording ([#1855](https://github.com/softistx/nxgt-material/issues/1855)).
20
+ - **A record page layout** — `RecordPage` and `RecordDetails` lay out one
21
+ record: a header card, the content, and its facts as a side panel from `xl`
22
+ that stacks after or before the content, with a created / updated line
23
+ ([#1689](https://github.com/softistx/nxgt-material/issues/1689)).
18
24
  - **A decision bar for phone pages** — `StickyActionBar` holds a long page's
19
25
  main decision at the bottom of a phone, above the keyboard and clear of the
20
26
  safe areas, and inline from `md`
@@ -157,6 +163,8 @@ version they shipped in, and planned entries may name the major they target.
157
163
 
158
164
  ## Next
159
165
 
166
+ - **Layouts: adaptive navigation, headers and page layouts** — `ActivityLayout` takes `navigation` (a bottom bar, a rail with a FAB, standard and modal drawers, a collapsible sidebar, a top navigation with dropdowns, two-level navigation or a floating toolbar, chosen for phone, tablet and desktop) and `header` (small, center-aligned, medium and large collapsing on scroll, a search bar with full-screen search on a phone, sticky tabs, hide on scroll); each piece is also exported. Page layouts grow with it: an inspector pane and URL state for `ListDetailsLayout`, phone modes for `PaneLayout`, spans and masonry for `ResponsiveGrid`, tabs for `Kanban` on a phone, and new settings, dashboard (rearranging tiles), editor and landing layouts, each with a screen recipe. Everything is opt-in; the shell's default is unchanged. Tracked in [#2103](https://github.com/softistx/nxgt-material/issues/2103).
167
+
160
168
  - **FilterBar "Add a filter" stays on its row** — the button no longer wraps
161
169
  alone onto a second row on a desktop screen ([#1752](https://github.com/softistx/nxgt-material/issues/1752)).
162
170
 
@@ -241,11 +249,13 @@ version they shipped in, and planned entries may name the major they target.
241
249
  slot; the bar's `views` slot is planned with it. They are meant to replace
242
250
  the legacy `FilterPresets`. Tracked in [#1876](https://github.com/softistx/nxgt-material/issues/1876).
243
251
 
244
- - **RecordActionsMenu** — one menu for a record's lifecycle: archive with an Undo toast (or confirm first with `archivePolicy="confirm"`), restore, and delete that always confirms and shows the backend's refusal, with `ConfirmDialog` gaining `details` and `refusal` slots.
245
252
  - **Cursor trail cap** — `ServerTable` and `PageFooter` keep cursor URLs short: the trail is encoded once and capped by `maxTrail` (20 by default) with an exact page count, and `totalCount` becomes optional for APIs that do not count.
246
253
 
247
254
  ## Later
248
255
 
256
+ - **One `onUpload` options object (next major)** — the upload fields'
257
+ `onUpload(file, onProgress?, signal?)` becomes `onUpload(file, { onProgress,
258
+ signal })`, like the editor's upload context.
249
259
  - **A strict theme `motif` (4.0)** — a theme background's `motif` with an
250
260
  unknown key, a `layout` or `density` outside its list or a bad `icons` is
251
261
  dropped in 3.22 (`@nxgt/material/themes`'s `defineTheme` warns for each one in
@@ -361,6 +371,9 @@ version they shipped in, and planned entries may name the major they target.
361
371
 
362
372
  ## Shipped
363
373
 
374
+ - **RecordActionsMenu (next minor)** — one menu for a record's lifecycle: archive with an Undo toast (or confirm first with `archivePolicy="confirm"`), restore, and delete that always confirms and shows the backend's refusal, with `ConfirmDialog` gaining `details` and `refusal` slots.
375
+ - **Assistant approval card (next minor)** — `ChatActionProposal` shows an action an assistant proposes, with its status, deadline and Confirm / Reject, and `ChatToolActivity` is a compact row for a tool step in a chat thread ([#2134](https://github.com/softistx/nxgt-material/issues/2134)).
376
+
364
377
  - **Link navigation (3.66.0)** — `TabsNav` and `SegmentedNav` render route tabs and segmented navigation as a `nav` of links with `aria-current="page"`, looking like Tabs and the segmented ToggleGroup, plus `useTabParam`, `skipTabChange` and `tabIds` to keep the current tab in the URL. Tracked in [#2029](https://github.com/softistx/nxgt-material/issues/2029).
365
378
 
366
379
  - **Smaller, easier-to-read components (3.58.0)** — the library's own source