@nxgt/material 3.34.0 → 3.35.1

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 (172) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -4
  3. package/dist/{async-slice-C9q_eMJD.js → async-slice-BNI_F0jz.js} +2 -2
  4. package/dist/{button-CoAu_GuF.js → button-EMWOFSDB.js} +1 -1
  5. package/dist/charts.js +3 -3
  6. package/dist/{chip-Ch21Qbnj.js → chip-B6m3yagf.js} +1 -1
  7. package/dist/{code-editor-CEdEmolU.js → code-editor-BdiufnpC.js} +4 -4
  8. package/dist/code-editor.js +4 -4
  9. package/dist/{code-language-items-BNMM7YlZ.js → code-language-items-v5Rnq425.js} +2 -2
  10. package/dist/{components-BM9WHviO.js → components-jh9l5bIA.js} +16962 -14252
  11. package/dist/components.js +26 -26
  12. package/dist/{dialog-C_U4twIJ.js → dialog-Cdv9IXVo.js} +1 -1
  13. package/dist/dnd.js +1 -1
  14. package/dist/{dropdown-menu-DSgkmogX.js → dropdown-menu-CpSnRXd-.js} +1 -1
  15. package/dist/editor.js +7 -7
  16. package/dist/{empty-state-CJZ0sVE8.js → empty-state-DMr94lsF.js} +1 -1
  17. package/dist/hooks.js +2 -2
  18. package/dist/{i18n-provider-CLq-1Qgc.js → i18n-provider-A4mI4vc1.js} +1 -1
  19. package/dist/i18n.js +5 -5
  20. package/dist/{icon-badge-Dssl4rfn.js → icon-badge-BFScQnp9.js} +1 -1
  21. package/dist/lib/components/ui/collections/server-table/bulk-action-targets.d.ts +6 -5
  22. package/dist/lib/components/ui/collections/server-table/bulk-action.d.ts +6 -5
  23. package/dist/lib/components/ui/collections/server-table/partials/server-table-bulk-buttons.d.ts +6 -5
  24. package/dist/lib/components/ui/collections/server-table/selected-rows.d.ts +17 -0
  25. package/dist/lib/components/ui/collections/server-table/use-query-selection.d.ts +6 -1
  26. package/dist/lib/components/ui/collections/server-table/use-server-table-selection.d.ts +3 -1
  27. package/dist/lib/components/ui/collections/server-table/use-server-table.d.ts +1 -0
  28. package/dist/lib/components/ui/dates/picker-trigger/typed-box-compact.d.ts +12 -0
  29. package/dist/lib/components/ui/dates/picker-trigger/typed-box.d.ts +6 -2
  30. package/dist/lib/components/ui/dates/picker-trigger/typed-trigger.d.ts +3 -2
  31. package/dist/lib/components/ui/dates/time-field/index.d.ts +1 -0
  32. package/dist/lib/components/ui/dates/time-field/partials/time-field-body.d.ts +2 -1
  33. package/dist/lib/components/ui/dates/time-field/partials/time-field-entry.d.ts +1 -0
  34. package/dist/lib/components/ui/dates/time-field/partials/time-field-typed.d.ts +2 -2
  35. package/dist/lib/components/ui/dates/time-field/time-field.d.ts +6 -1
  36. package/dist/lib/components/ui/dates/time-field/time-string-field.d.ts +14 -0
  37. package/dist/lib/components/ui/fields/attributes/attribute-dialog.d.ts +3 -1
  38. package/dist/lib/components/ui/fields/attributes/attribute-form/attribute-form.d.ts +3 -1
  39. package/dist/lib/components/ui/fields/attributes/attribute-form/attribute-text-form-fields.d.ts +6 -0
  40. package/dist/lib/components/ui/fields/attributes/attribute-form/consts.d.ts +7 -6
  41. package/dist/lib/components/ui/fields/attributes/attribute-form/use-attribute-form.d.ts +44 -0
  42. package/dist/lib/components/ui/fields/attributes/attributes-editor.d.ts +22 -0
  43. package/dist/lib/components/ui/fields/attributes/attributes-field/attributes-dialog-field.d.ts +14 -0
  44. package/dist/lib/components/ui/fields/attributes/attributes-field/attributes-field.d.ts +14 -12
  45. package/dist/lib/components/ui/fields/attributes/attributes-field/attributes-inline-field.d.ts +8 -0
  46. package/dist/lib/components/ui/fields/attributes/attributes-field/use-dialog-field.d.ts +15 -0
  47. package/dist/lib/components/ui/fields/attributes/attributes-form-field/attributes-form-field.d.ts +7 -1
  48. package/dist/lib/components/ui/fields/attributes/attributes-summary.d.ts +22 -0
  49. package/dist/lib/components/ui/fields/attributes/index.d.ts +3 -0
  50. package/dist/lib/components/ui/fields/attributes/logic/names.d.ts +13 -0
  51. package/dist/lib/components/ui/fields/attributes/logic/order.d.ts +15 -0
  52. package/dist/lib/components/ui/fields/attributes/logic/reorder-keys.d.ts +9 -0
  53. package/dist/lib/components/ui/fields/attributes/logic/value.d.ts +42 -0
  54. package/dist/lib/components/ui/fields/attributes/models/attribute.d.ts +5 -1
  55. package/dist/lib/components/ui/fields/attributes/partials/attribute-row.d.ts +18 -0
  56. package/dist/lib/components/ui/fields/attributes/partials/default-choices.d.ts +5 -0
  57. package/dist/lib/components/ui/fields/attributes/partials/default-field.d.ts +12 -0
  58. package/dist/lib/components/ui/fields/attributes/partials/default-inputs.d.ts +16 -0
  59. package/dist/lib/components/ui/fields/attributes/partials/editor-notes.d.ts +11 -0
  60. package/dist/lib/components/ui/fields/attributes/partials/labeled.d.ts +10 -0
  61. package/dist/lib/components/ui/fields/attributes/partials/name-field.d.ts +15 -0
  62. package/dist/lib/components/ui/fields/attributes/partials/options-field.d.ts +11 -0
  63. package/dist/lib/components/ui/fields/attributes/partials/phone-sheet.d.ts +12 -0
  64. package/dist/lib/components/ui/fields/attributes/partials/reorder-handle.d.ts +19 -0
  65. package/dist/lib/components/ui/fields/attributes/partials/row-fields.d.ts +16 -0
  66. package/dist/lib/components/ui/fields/attributes/partials/row-footer.d.ts +12 -0
  67. package/dist/lib/components/ui/fields/attributes/partials/row-head.d.ts +12 -0
  68. package/dist/lib/components/ui/fields/attributes/partials/row-panel.d.ts +15 -0
  69. package/dist/lib/components/ui/fields/attributes/partials/rows-dnd.d.ts +11 -0
  70. package/dist/lib/components/ui/fields/attributes/partials/simple-fields.d.ts +20 -0
  71. package/dist/lib/components/ui/fields/attributes/partials/summary-item.d.ts +9 -0
  72. package/dist/lib/components/ui/fields/attributes/partials/type-icon.d.ts +13 -0
  73. package/dist/lib/components/ui/fields/attributes/partials/value-text.d.ts +12 -0
  74. package/dist/lib/components/ui/fields/attributes/schema.d.ts +32 -0
  75. package/dist/lib/components/ui/fields/attributes/use-announce.d.ts +14 -0
  76. package/dist/lib/components/ui/fields/attributes/use-attribute-text.d.ts +13 -0
  77. package/dist/lib/components/ui/fields/attributes/use-attributes-editor.d.ts +34 -0
  78. package/dist/lib/components/ui/fields/attributes/use-reorder.d.ts +26 -0
  79. package/dist/lib/components/ui/fields/contacts/contacts-dialog-form-field.d.ts +9 -0
  80. package/dist/lib/components/ui/fields/contacts/contacts-editor.d.ts +29 -0
  81. package/dist/lib/components/ui/fields/contacts/contacts-form-field.d.ts +14 -1
  82. package/dist/lib/components/ui/fields/contacts/contacts-summary.d.ts +23 -0
  83. package/dist/lib/components/ui/fields/contacts/index.d.ts +3 -0
  84. package/dist/lib/components/ui/fields/contacts/logic/check-contact.d.ts +11 -0
  85. package/dist/lib/components/ui/fields/contacts/logic/contact-action.d.ts +11 -0
  86. package/dist/lib/components/ui/fields/contacts/logic/duplicates.d.ts +17 -0
  87. package/dist/lib/components/ui/fields/contacts/logic/issues.d.ts +7 -0
  88. package/dist/lib/components/ui/fields/contacts/logic/primary.d.ts +33 -0
  89. package/dist/lib/components/ui/fields/contacts/models.d.ts +7 -0
  90. package/dist/lib/components/ui/fields/contacts/partials/contact-fields.d.ts +29 -0
  91. package/dist/lib/components/ui/fields/contacts/partials/contact-links.d.ts +15 -0
  92. package/dist/lib/components/ui/fields/contacts/partials/contact-row.d.ts +14 -0
  93. package/dist/lib/components/ui/fields/contacts/partials/phone-sheet.d.ts +14 -0
  94. package/dist/lib/components/ui/fields/contacts/partials/quick-add.d.ts +12 -0
  95. package/dist/lib/components/ui/fields/contacts/partials/row-actions.d.ts +16 -0
  96. package/dist/lib/components/ui/fields/contacts/partials/summary-item.d.ts +11 -0
  97. package/dist/lib/components/ui/fields/contacts/partials/summary-row.d.ts +14 -0
  98. package/dist/lib/components/ui/fields/contacts/partials/type-group.d.ts +15 -0
  99. package/dist/lib/components/ui/fields/contacts/partials/type-icon.d.ts +12 -0
  100. package/dist/lib/components/ui/fields/contacts/partials/value-field.d.ts +25 -0
  101. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +2 -0
  102. package/dist/lib/components/ui/fields/contacts/schema.d.ts +18 -0
  103. package/dist/lib/components/ui/fields/contacts/use-contact-text.d.ts +9 -0
  104. package/dist/lib/components/ui/fields/contacts/use-contacts-editor.d.ts +33 -0
  105. package/dist/lib/components/ui/fields/inline-editor/action-tone.d.ts +2 -0
  106. package/dist/lib/components/ui/fields/inline-editor/editor-header.d.ts +19 -0
  107. package/dist/lib/components/ui/fields/inline-editor/editor-sheet.d.ts +20 -0
  108. package/dist/lib/components/ui/fields/inline-editor/empty-summary.d.ts +19 -0
  109. package/dist/lib/components/ui/fields/inline-editor/use-focus-request.d.ts +13 -0
  110. package/dist/lib/components/ui/fields/inline-editor/use-row-ids.d.ts +13 -0
  111. package/dist/lib/components/ui/fields/opening-hours/consts.d.ts +17 -3
  112. package/dist/lib/components/ui/fields/opening-hours/index.d.ts +2 -0
  113. package/dist/lib/components/ui/fields/opening-hours/logic/edit-days.d.ts +9 -0
  114. package/dist/lib/components/ui/fields/opening-hours/logic/first-day.d.ts +14 -0
  115. package/dist/lib/components/ui/fields/opening-hours/logic/format.d.ts +6 -0
  116. package/dist/lib/components/ui/fields/opening-hours/logic/group.d.ts +13 -0
  117. package/dist/lib/components/ui/fields/opening-hours/logic/range-errors.d.ts +20 -0
  118. package/dist/lib/components/ui/fields/opening-hours/logic/status.d.ts +19 -0
  119. package/dist/lib/components/ui/fields/opening-hours/logic/week.d.ts +32 -0
  120. package/dist/lib/components/ui/fields/opening-hours/logic/zoned.d.ts +10 -0
  121. package/dist/lib/components/ui/fields/opening-hours/opening-hours-dialog-form-field.d.ts +10 -0
  122. package/dist/lib/components/ui/fields/opening-hours/opening-hours-editor.d.ts +26 -0
  123. package/dist/lib/components/ui/fields/opening-hours/opening-hours-form-field.d.ts +17 -1
  124. package/dist/lib/components/ui/fields/opening-hours/opening-hours-summary.d.ts +32 -0
  125. package/dist/lib/components/ui/fields/opening-hours/partials/copy-chips.d.ts +12 -0
  126. package/dist/lib/components/ui/fields/opening-hours/partials/copy-menu.d.ts +10 -0
  127. package/dist/lib/components/ui/fields/opening-hours/partials/day-controls.d.ts +14 -0
  128. package/dist/lib/components/ui/fields/opening-hours/partials/day-row.d.ts +15 -0
  129. package/dist/lib/components/ui/fields/opening-hours/partials/day-summary-row.d.ts +15 -0
  130. package/dist/lib/components/ui/fields/opening-hours/partials/group-row.d.ts +13 -0
  131. package/dist/lib/components/ui/fields/opening-hours/partials/phone-sheet.d.ts +18 -0
  132. package/dist/lib/components/ui/fields/opening-hours/partials/range-field-props.d.ts +28 -0
  133. package/dist/lib/components/ui/fields/opening-hours/partials/range-row.d.ts +24 -0
  134. package/dist/lib/components/ui/fields/opening-hours/partials/status-pill.d.ts +13 -0
  135. package/dist/lib/components/ui/fields/opening-hours/use-first-day.d.ts +8 -0
  136. package/dist/lib/components/ui/fields/opening-hours/use-hours-editor.d.ts +26 -0
  137. package/dist/lib/components/ui/fields/opening-hours/use-hours-text.d.ts +8 -0
  138. package/dist/lib/components/ui/fields/opening-hours/use-now.d.ts +6 -0
  139. package/dist/lib/i18n/resources/en/en.d.ts +75 -0
  140. package/dist/lib/i18n/resources/en/material.json.d.ts +86 -11
  141. package/dist/lib/i18n/resources/fr/fr.d.ts +75 -0
  142. package/dist/lib/i18n/resources/fr/material.json.d.ts +86 -11
  143. package/dist/lib/i18n/resources/resources.d.ts +150 -0
  144. package/dist/lib/i18n/translator.d.ts +1 -1
  145. package/dist/lib.js +1 -1
  146. package/dist/main.js +43 -43
  147. package/dist/map.js +4 -4
  148. package/dist/material.css +1 -1
  149. package/dist/pdf-reader.js +1 -1
  150. package/dist/print.js +2 -2
  151. package/dist/{public-CX7FN97X.js → public-CYdnlHqA.js} +6 -6
  152. package/dist/{rich-code-block-cc1NZtbD.js → rich-code-block-Bwhfhu7J.js} +4 -4
  153. package/dist/{sortable-provider-D43VdFMP.js → sortable-provider-CY_YD3Ee.js} +1 -1
  154. package/dist/{spinner-wKfFAVU9.js → spinner-CKlDba5I.js} +1 -1
  155. package/dist/{stored-language-D73ujVW4.js → stored-language-CfQGHLq9.js} +176 -22
  156. package/dist/theme-picker.js +2 -2
  157. package/dist/{theme-toggle-Drab-hke.js → theme-toggle--Zcf5AM8.js} +1 -1
  158. package/dist/{toolbar-items-D7R9YtYx.js → toolbar-items-B1W-uVHS.js} +2 -2
  159. package/dist/{translate-material-CdpLjkcD.js → translate-material-xDh10aWZ.js} +2 -2
  160. package/dist/{translator-BE-yEb0p.js → translator-BbOZntEF.js} +1 -1
  161. package/dist/{use-chat-composer-CpKh8564.js → use-chat-composer-CoyqPKbt.js} +12 -12
  162. package/dist/{use-heading-spy-DgWhvYfh.js → use-heading-spy-DXYCeegh.js} +1 -1
  163. package/dist/{use-material-translation-Da9Ponn5.js → use-material-translation-BK2YYCY3.js} +2 -2
  164. package/docs/components/data-table-filter-and-lists.md +10 -3
  165. package/docs/components/date-and-time.md +9 -1
  166. package/docs/components/form-fields.md +298 -67
  167. package/docs/guides/forms.md +18 -12
  168. package/docs/guides/i18n.md +1 -1
  169. package/docs/guides/ssr.md +1 -0
  170. package/docs/previews/form-fields-contacts.webp +0 -0
  171. package/docs/previews/form-fields-openinghours.webp +0 -0
  172. package/package.json +1 -1
@@ -32,10 +32,10 @@ for the value type each one writes. Everything is imported from
32
32
  | <img src="../previews/form-fields-ratingfield.webp" width="96" alt=""> | [RatingField](#ratingfield) | Star rating |
33
33
  | <img src="../previews/form-fields-slider.webp" width="96" alt=""> | [Slider](#slider) | Single or range slider with a readout |
34
34
  | <img src="../previews/form-fields-switch.webp" width="96" alt=""> | [Switch](#switch) | Toggle switch, optionally in a labelled row |
35
- | <img src="../previews/form-fields-attributes.webp" width="96" alt=""> | [Attributes](#attributes) | Editor for typed custom attributes |
36
- | <img src="../previews/form-fields-contacts.webp" width="96" alt=""> | [Contacts](#contacts) | Typed contact list with a dialog |
35
+ | <img src="../previews/form-fields-attributes.webp" width="96" alt=""> | [Attributes](#attributes) | Typed custom attributes edited in place, and a read-only summary |
36
+ | <img src="../previews/form-fields-contacts.webp" width="96" alt=""> | [Contacts](#contacts) | Typed contacts edited in place, and a read-only summary |
37
37
  | <img src="../previews/form-fields-postaladdress.webp" width="96" alt=""> | [PostalAddress](#postaladdress) | Address card with an edit dialog |
38
- | <img src="../previews/form-fields-openinghours.webp" width="96" alt=""> | [OpeningHours](#openinghours) | Weekly opening hours with a dialog |
38
+ | <img src="../previews/form-fields-openinghours.webp" width="96" alt=""> | [OpeningHours](#openinghours) | Opening hours edited in place, and an open-now summary |
39
39
 
40
40
  ## Field
41
41
 
@@ -180,8 +180,8 @@ row.
180
180
 
181
181
  ![RepeatableGroup](../previews/form-fields-repeatablegroup.webp)
182
182
 
183
- Rows of fields a user adds, removes and, optionally, reorders — a school
184
- year's periods, a list of contacts. `RepeatableGroupFormField` wires it to
183
+ Rows of fields a user adds, removes and, optionally, reorders — a plan's
184
+ periods, a list of contacts. `RepeatableGroupFormField` wires it to
185
185
  react-hook-form's `useFieldArray`; each row's fields are your own form
186
186
  wrappers, rendered by `renderRow`.
187
187
 
@@ -197,7 +197,7 @@ import {
197
197
  type Period = { name: string; start?: Date; end?: Date };
198
198
  type Values = { periods: Period[] };
199
199
 
200
- export function SchoolYearForm({ onSave }: { onSave: (values: Values) => void }) {
200
+ export function PlanForm({ onSave }: { onSave: (values: Values) => void }) {
201
201
  const { control, handleSubmit } = useForm<Values>({
202
202
  defaultValues: { periods: [{ name: 'Term 1' }] },
203
203
  });
@@ -1312,41 +1312,150 @@ libraries; neither name is deprecated, and both keep working.
1312
1312
 
1313
1313
  ![Attributes](../previews/form-fields-attributes.webp)
1314
1314
 
1315
- Editor for typed custom attributes (string, number, date, select, rich text…).
1315
+ Typed custom attributes (string, number, boolean, date, select, rich text…),
1316
+ edited in place or shown read-only. One value model throughout: `Attribute[]`,
1317
+ `{ name, label, type, options?, unit?, defaultValue?, description?, priority? }`.
1318
+ `type` is an `AttributeType`: `STRING`, `TEXT`, `NUMBER`, `BOOLEAN`, `COLOR`,
1319
+ `DATE`, `DATETIME`, `TIME`, `SELECT`, `MULTI_SELECT` or `RICH_TEXT`. The
1320
+ `BOOLEAN` default is a `boolean`. `priority` is the place in the list, `1`
1321
+ first.
1322
+
1323
+ Three parts, each for one job:
1324
+
1325
+ | Part | Use it for |
1326
+ | --- | --- |
1327
+ | `AttributesField` · `AttributesFormField` | the field, and its React Hook Form wrapper. Inline by default; `editing="dialog"` keeps the list and dialog |
1328
+ | `AttributesEditor` | the same inline editor, controlled (`value`, `onValueChange`), outside a form |
1329
+ | `AttributesSummary` | the read-only display: type icons, labels and default values |
1330
+
1331
+ `AttributesList` (the list with `onAdd` / `onEdit`), `AttributeDialog` and
1332
+ `AttributeForm` stay: they are what `editing="dialog"` renders. `AttributeValuesForm`,
1333
+ `AttributeValuesFormFieldSet` and `AttributeValueFormField` fill the values of
1334
+ a set of attributes, and now draw a `BOOLEAN` attribute as a switch.
1316
1335
 
1317
1336
  ```tsx
1318
- import { AttributesField, type Attribute } from '@nxgt/material/components';
1337
+ import { AttributesFormField } from '@nxgt/material/components';
1338
+
1339
+ <AttributesFormField control={control} name="attributes" label="Attributes" />
1340
+ ```
1341
+
1342
+ **The inline editor** is one row per attribute: a grip, the type's icon, the
1343
+ label with `name · Type` under it, the unit as a badge and the default value
1344
+ (a Yes or No, a colour swatch, a number with its unit). A row opens to its
1345
+ fields: label, name, type, unit (for a number), options (for a select), the
1346
+ default value edited the way its type is, and a description; Delete removes
1347
+ it and Done closes it. Nothing is toasted: an edit is in the value at once.
1348
+ Changing the type resets the default value, and the options unless both types
1349
+ take options. "Add attribute" appends a row, opens it and focuses its label.
1350
+
1351
+ Outside a form, control it yourself; `editing="dialog"` is the way back to
1352
+ the earlier list and dialog:
1353
+
1354
+ ```tsx
1355
+ import { useState } from 'react';
1356
+ import { AttributesEditor, AttributesFormField, type Attribute } from '@nxgt/material/components';
1357
+
1358
+ const [attributes, setAttributes] = useState<Attribute[]>([]);
1359
+ <AttributesEditor label="Attributes" value={attributes} onValueChange={setAttributes} />
1360
+
1361
+ <AttributesFormField control={control} name="attributes" label="Attributes" editing="dialog" />
1362
+ ```
1363
+
1364
+ *Reorder.* Drag the grip, or focus it and press Space (or Enter) to lift the
1365
+ row, the arrow keys to move it, Space (or Enter) to drop it and Escape to put
1366
+ it back. Each step is said in a live region ("Active moved to position 2 of
1367
+ 6"). A drop rewrites the list in its new order and sets every `priority` to
1368
+ its place (`1`, `2`, `3`…); the editor shows the attributes by `priority`
1369
+ (lowest first, none after the others), so a list without priorities keeps its
1370
+ order until the first move.
1371
+
1372
+ *Names.* A name must be unique and not empty (spaces around it are ignored;
1373
+ there is no minimum length any more). A name another attribute has is a
1374
+ `role="alert"` under the field at once ("Another attribute already has this
1375
+ name."); an empty one once the field was left. The header counts the problems
1376
+ ("2 issues to fix"), and a row with a bad name shows a warning mark.
1319
1377
 
1320
- const attributes: Attribute[] = [
1321
- { name: 'color', label: 'Color', type: 'COLOR' },
1322
- { name: 'size', label: 'Size', type: 'SELECT', options: ['S', 'M', 'L'] },
1323
- ];
1378
+ *Phone.* Below 640px each attribute is a row with its icon, title and a
1379
+ chevron; tapping it opens a bottom sheet with the same fields (and Delete and
1380
+ Done). The grip stays, so a row can be moved on a phone too.
1324
1381
 
1325
- <AttributesField label="Attributes" attributes={attributes} onChange={(next) => console.log(next)} />
1382
+ **The summary** takes the same `data`:
1383
+
1384
+ ```tsx
1385
+ import { AttributesSummary } from '@nxgt/material/components';
1386
+
1387
+ <AttributesSummary data={attributes} label="Item" />
1326
1388
  ```
1327
1389
 
1328
- Parts: `AttributesField` (the editable list), `AttributesList` (display with
1329
- `onAdd` / `onEdit`), `AttributeDialog` and `AttributeForm` (define one
1330
- attribute), `AttributeValuesForm` (fill values for a set of attributes).
1331
- Form wrappers: `AttributesFormField` (the definitions),
1332
- `AttributeValuesFormFieldSet` and `AttributeValueFormField` (the values).
1333
- `AttributeForm` builds its zod schema when it first renders, not when the
1334
- module loads ([CSP](../guides/csp.md#zod-and-unsafe-eval)).
1390
+ It is a description list in `priority` order (list order without one), each
1391
+ attribute with its type icon, its label and its default value, a dash when it
1392
+ has none. It is a new component, not a read-only mode of `AttributesList`, so
1393
+ the list keeps its callbacks. With nothing to list it shows a quiet "No attributes"
1394
+ (`empty` replaces the text, `empty={null}` renders nothing). A `DATETIME` or
1395
+ `TIME` default stored as an instant is written in UTC on the server and in the
1396
+ first client render, and in the viewer's time zone and locale once the page
1397
+ has mounted, so hydration matches; a bare `yyyy-MM-dd` or `HH:mm` reads the
1398
+ same everywhere.
1335
1399
 
1336
1400
  | Prop | Type | Default | Effect |
1337
1401
  | --- | --- | --- | --- |
1402
+ | `data` | `Attribute[] \| null` | `[]` | the attributes |
1403
+ | `label` | `string` | "Attributes" | names the region |
1404
+ | `empty` | `ReactNode` | "No attributes" | shown when there is nothing to list; `null` renders nothing |
1405
+
1406
+ | Prop (field) | Type | Default | Effect |
1407
+ | --- | --- | --- | --- |
1338
1408
  | `attributes` | `Attribute[]` | `[]` | the definitions; a new value (a form `reset`) replaces the list |
1339
- | `onChange` | `(values: Attribute[]) => void` | — | after each add, edit or delete |
1340
- | `readonly` | `boolean` | — | hides add and edit |
1409
+ | `onChange` | `(values: Attribute[]) => void` | — | after each edit |
1410
+ | `editing` | `'inline' \| 'dialog'` | `'inline'` | in place, or the list and dialog |
1411
+ | `readonly` | `boolean` | — | the list without add and edit (also with `editing="inline"`) |
1412
+ | `disabled` | `boolean` | — | disables the controls |
1341
1413
  | `label` | `ReactNode` | — | required; the section title |
1342
- | `helperText` / `error` | `ReactNode` / `boolean` | — | text under the list; red when `error` (`AttributesFormField` sets both from the field's error, and shows your own `helperText` while there is none) |
1414
+ | `helperText` / `error` | `ReactNode` / `boolean` | — | text under the field; red when `error` (`AttributesFormField` sets both from the field's error, and shows your own `helperText` while there is none) |
1415
+
1416
+ | `AttributesEditor` prop | Type | Default | Effect |
1417
+ | --- | --- | --- | --- |
1418
+ | `value` | `Attribute[]` | `[]` | the attributes |
1419
+ | `onValueChange` | `(value: Attribute[]) => void` | — | after each edit |
1420
+ | `label` | `ReactNode` | "Attributes" | the heading |
1421
+ | `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | `3` | the heading's level |
1422
+ | `disabled` | `boolean` | — | disables the controls |
1423
+ | `className` | `string` | — | on the section |
1424
+
1425
+ `AttributesSummary` also takes `className`. `AttributesFormField` takes
1426
+ `control` and `name` in place of `attributes` and `onChange`, an optional
1427
+ `value` that overrides the form's, and the props above; its `disabled` also
1428
+ follows `useForm({ disabled })`.
1429
+
1430
+ `AttributeDialog` and `AttributeForm` take `otherNames`: saving a
1431
+ name in it is an inline error on the name. `AttributeForm` builds its zod
1432
+ schema when it first renders, not when the module loads
1433
+ ([CSP](../guides/csp.md#zod-and-unsafe-eval)); its `name` is only required to
1434
+ be non-empty (it was at least four characters).
1435
+
1436
+ `attributesListSchema()` is the zod schema of a whole list, with the editor's
1437
+ own rules (the same functions): a name that is set and that no other attribute
1438
+ has (`[1, 'name']`), and a default value that fits the type, a listed option, a
1439
+ boolean, a number or a date (`[1, 'defaultValue']`). Built on the first call
1440
+ ([CSP](../guides/csp.md#zod-and-unsafe-eval)). Each issue's `message` is an
1441
+ English default, and its `params.code` (`'empty'`, `'duplicate'`, `'option'`,
1442
+ `'boolean'`, `'number'`, `'date'`; typed as `AttributeIssueCode`) lets a form show its own translated text.
1343
1443
 
1344
- **Emits:** `Attribute[]` (`{ name, label, type, options?, unit?, defaultValue?, … }`);
1345
- `AttributeValuesFormFieldSet` writes an object keyed by attribute `name`.
1444
+ ```ts
1445
+ import { attributesListSchema } from '@nxgt/material/components';
1446
+ import { z } from 'zod';
1447
+
1448
+ const schema = z.object({ attributes: attributesListSchema() });
1449
+ ```
1450
+
1451
+ **Emits:** `Attribute[]`; the inline editor writes a date as `yyyy-MM-dd`, a
1452
+ date-time as an ISO instant and a time as `HH:mm`. `AttributeValuesFormFieldSet`
1453
+ writes an object keyed by attribute `name`; `AttributeValuesForm` reads a bare
1454
+ `yyyy-MM-dd` default as that local day.
1346
1455
  **Form wrapper:** `AttributesFormField` — `control` and `name`, see
1347
1456
  [forms](../guides/forms.md#the-pair), [what each field emits](../guides/forms.md#what-each-field-emits)
1348
1457
  and [composite values](../guides/forms.md#composite-values).
1349
- **Storybook:** `Form fields/Attributes` · **Vue:** `AttributesField`, `AttributesList`, `AttributeDialog`, `AttributeForm`, `AttributeValuesForm`, `AttributesFormField`, `AttributeValuesFormFieldSet`, `AttributeValueFormField`
1458
+ **Storybook:** `Form fields/Attributes`, `Form fields/Attributes/Editor`, `Form fields/Attributes/Editor reorder`, `Form fields/Attributes/Editor on a phone`, `Form fields/Attributes/Summary` · **Vue:** `AttributesField`, `AttributesList`, `AttributeDialog`, `AttributeForm`, `AttributeValuesForm`, `AttributesFormField`, `AttributeValuesFormFieldSet`, `AttributeValueFormField`
1350
1459
 
1351
1460
  ---
1352
1461
 
@@ -1354,35 +1463,105 @@ and [composite values](../guides/forms.md#composite-values).
1354
1463
 
1355
1464
  ![Contacts](../previews/form-fields-contacts.webp)
1356
1465
 
1357
- Typed contact list (phone, email, URL…) with an add / edit dialog.
1466
+ Typed contacts (phone, mobile, fax, email, website), edited in place or shown
1467
+ read-only. One value model throughout: `Contact[]`, `{ type, value, label?,
1468
+ primary? }` with `type` a `ContactType`. `primary` is optional and additive:
1469
+ **one per type**, the contact to use first among those of its type. A value
1470
+ that marks several of a type is read as marking the first only.
1471
+
1472
+ Three parts, each for one job:
1473
+
1474
+ | Part | Use it for |
1475
+ | --- | --- |
1476
+ | `ContactsFormField` | the React Hook Form field. Inline by default; `editing="dialog"` keeps the list and dialog |
1477
+ | `ContactsEditor` | the same inline editor, controlled (`value`, `onValueChange`), outside a form |
1478
+ | `ContactsSummary` | the read-only display: icons, values as links, copy, the primary one marked |
1479
+
1480
+ `Contacts` (the list with `onAdd` / `onEdit`) and `ContactDialog` stay as they
1481
+ were: they are what `editing="dialog"` renders.
1358
1482
 
1359
1483
  ```tsx
1360
- import { type Contact, Contacts, ContactType } from '@nxgt/material/components';
1484
+ import { ContactsFormField } from '@nxgt/material/components';
1361
1485
 
1362
- const contacts: Contact[] = [
1363
- { type: ContactType.Phone, value: '+33 1 23 45 67 89', label: 'Front desk' },
1364
- { type: ContactType.Email, value: 'contact@example.com' },
1365
- ];
1486
+ <ContactsFormField control={control} name="contacts" label="Contacts" />
1487
+ ```
1488
+
1489
+ **The inline editor** groups the contacts by type (phone, mobile, email,
1490
+ website, fax), the primary one first in its group. A row is the type icon, a
1491
+ type select, the value, an optional label and its actions: call (`tel:`),
1492
+ write (`mailto:`) or open the site (links, shown only when the value is
1493
+ well-formed), copy (announces "Copied"), a star that makes it the primary
1494
+ contact of its type, and remove. "Add Contact" chips add an empty contact of a
1495
+ type and focus its value. A wrong format (an email without a domain, a number
1496
+ that is not 7 to 15 digits, a website that is not `http(s)`) or a duplicate
1497
+ (the same type and value, ignoring case and number formatting) is a
1498
+ `role="alert"` under its value. A duplicate shows and counts at once; a wrong
1499
+ format shows and counts once its field was left. The header counts them
1500
+ ("2 issues to fix").
1501
+ The editor checks the shape of a value, not that the line exists. Below 640px
1502
+ each contact is a summary row; tapping it opens a bottom sheet with the same
1503
+ fields and actions, and closing it with an empty value drops the contact.
1504
+
1505
+ `variant="cards"` is the Expressive variant: each contact is its own tinted
1506
+ card with a round tile for the primary one. The fields keep the library's
1507
+ field radius in both. `variant` is on `ContactsFormField`, `ContactsEditor`
1508
+ and `ContactsSummary`; it is the only prop that picks the look, so the two
1509
+ are one component, not two.
1510
+
1511
+ ```tsx
1512
+ <ContactsFormField control={control} name="contacts" variant="cards" />
1513
+ ```
1514
+
1515
+ **The summary** takes the same `data`:
1516
+
1517
+ ```tsx
1518
+ import { ContactsSummary } from '@nxgt/material/components';
1366
1519
 
1367
- <Contacts label="Contacts" data={contacts} onAdd={() => {}} onEdit={() => {}} />
1520
+ <ContactsSummary data={contacts} label="Acme headquarters" />
1368
1521
  ```
1369
1522
 
1370
- Parts: `Contacts` (the list) and `ContactDialog` (add / edit). `Contacts` only
1371
- displays and calls back; `ContactsFormField` owns the dialog and the array.
1523
+ It is a new component, not a read-only mode of `Contacts`, so the list keeps
1524
+ its callbacks and nothing changes for code that uses it. With nothing to list
1525
+ it shows a quiet "No contacts" (`empty` replaces the text, `empty={null}`
1526
+ renders nothing); it skips contacts without a value. A fax number offers copy
1527
+ only, and a value that fails its check is shown without a link.
1372
1528
 
1373
1529
  | Prop | Type | Default | Effect |
1374
1530
  | --- | --- | --- | --- |
1375
- | `data` | `Contact[] \| null` | `[]` | `{ type, value, label? }`, `type` a `ContactType` |
1376
- | `onAdd` | `() => void` | — | shows the add button |
1377
- | `onEdit` | `(contact: Contact, index: number) => void` | — | shows an edit button per row |
1378
- | `disabled` | `boolean` | — | hides add and edit |
1531
+ | `data` | `Contact[] \| null` | `[]` | the contacts |
1532
+ | `label` | `string` | "Contacts" | names the region |
1533
+ | `variant` | `'list' \| 'cards'` | `'list'` | a bordered list, or tinted cards |
1534
+ | `empty` | `ReactNode` | "No contacts" | shown when there is nothing to list; `null` renders nothing |
1535
+
1536
+ `ContactsEditor` takes `value`, `onValueChange`, `label`, `headingLevel` (`3`
1537
+ by default; the type headings sit one below it), `variant` and `disabled`. A
1538
+ wrong format is said once the field was left, a duplicate at once. `ContactsFormField` takes `editing` (`'inline' \| 'dialog'`,
1539
+ default `'inline'`) and `variant` (inline only). `Contacts` takes `data`,
1540
+ `onAdd`, `onEdit` and `disabled`.
1541
+
1542
+ `contactsListSchema()` is the zod schema of a whole list, with the editor's
1543
+ own rules (the same functions): the value's format by type (an email, an
1544
+ `http(s)` address, 7 to 15 digits for a phone, mobile or fax) and no value
1545
+ repeated within a type, each issue on the entry's `value` (`[1, 'value']`). A
1546
+ blank value is not an issue, as in the editor. Each issue's `message` is an
1547
+ English default, and its `params.code` (`'email'`, `'phone'`, `'website'`,
1548
+ `'duplicate'`; typed as `ContactIssueCode`) lets a form show its own translated text.
1379
1549
 
1380
- **Emits:** `ContactsFormField` stores `Contact[]`, and shows the array's
1381
- error (`errors.<name>.message` or `.root.message`) under the list.
1550
+ ```ts
1551
+ import { contactsListSchema } from '@nxgt/material/components';
1552
+ import { z } from 'zod';
1553
+
1554
+ const schema = z.object({ contacts: contactsListSchema() });
1555
+ ```
1556
+
1557
+ **Emits:** `ContactsFormField` stores `Contact[]` (the inline editor writes
1558
+ `primary: true` on one contact per type and leaves the flag off elsewhere), and
1559
+ shows the array's error (`errors.<name>.message` or `.root.message`) under the
1560
+ editor.
1382
1561
  **Form wrapper:** `ContactsFormField` — `control` and `name`, see
1383
1562
  [forms](../guides/forms.md#the-pair), [what each field emits](../guides/forms.md#what-each-field-emits)
1384
1563
  and [composite values](../guides/forms.md#composite-values).
1385
- **Storybook:** `Form fields/Contacts` · **Vue:** `Contacts`, `ContactDialog`, `ContactsFormField`
1564
+ **Storybook:** `Form fields/Contacts`, `Form fields/Contacts/Editor`, `Form fields/Contacts/Summary` · **Vue:** `Contacts`, `ContactDialog`, `ContactsFormField`
1386
1565
 
1387
1566
  ---
1388
1567
 
@@ -1426,45 +1605,97 @@ and [composite values](../guides/forms.md#composite-values).
1426
1605
 
1427
1606
  ![OpeningHours](../previews/form-fields-openinghours.webp)
1428
1607
 
1429
- Weekly opening hours with an add / edit dialog.
1608
+ A week of opening hours, edited in place or shown read-only. One value
1609
+ model throughout: `OpeningHour[]`, **one entry per range** (`{ dayOfWeek,
1610
+ openTime?, closeTime?, isClosed? }`, `dayOfWeek` `0` Sunday, times as
1611
+ `'HH:mm'`), so a day with a lunch break is two entries.
1612
+
1613
+ Three parts, each for one job:
1614
+
1615
+ | Part | Use it for |
1616
+ | --- | --- |
1617
+ | `OpeningHoursFormField` | the React Hook Form field. Inline by default; `editing="dialog"` keeps the list and dialog |
1618
+ | `OpeningHoursEditor` | the same inline editor, controlled (`value`, `onValueChange`), outside a form |
1619
+ | `OpeningHoursSummary` | the read-only display: open now, and the grouped week |
1620
+
1621
+ `OpeningHours` (the list with `onAdd` / `onEdit`) and `OpeningHoursDialog`
1622
+ stay as they were: they are what `editing="dialog"` renders.
1430
1623
 
1431
1624
  ```tsx
1432
- import { type OpeningHour, OpeningHours } from '@nxgt/material/components';
1625
+ import { OpeningHoursFormField } from '@nxgt/material/components';
1626
+
1627
+ <OpeningHoursFormField control={control} name="hours" label="Opening hours" />
1628
+ ```
1629
+
1630
+ **The inline editor** shows the seven days. Each has an open / closed switch
1631
+ and one or more ranges (add and remove), and a "Copy to" menu: Monday to
1632
+ Friday, the weekend, or all days, with a "Copied to Sat, Sun" note. A range
1633
+ that is wrong says so under itself (`role="alert"`): the close is not after the
1634
+ open, it overlaps another range of the day, or a time is missing; the header
1635
+ counts them ("2 issues to fix"). Times are `TimeStringField`s at `size="sm"`.
1636
+ `weekStartsOn` (`0` Sunday to `6`) defaults to the locale's first day: pass `1`
1637
+ for a Monday start under `en`. Below 640px each day is a summary row with a
1638
+ chevron; tapping it opens a bottom sheet with the same controls and day chips
1639
+ to copy to.
1433
1640
 
1434
- const hours: OpeningHour[] = [
1435
- { dayOfWeek: 1, openTime: '09:00', closeTime: '18:00' },
1436
- { dayOfWeek: 6, isClosed: true },
1437
- ];
1641
+ **The summary** takes the same `data`:
1438
1642
 
1439
- <OpeningHours label="Opening hours" data={hours} onAdd={() => {}} onEdit={() => {}} />
1643
+ ```tsx
1644
+ import { OpeningHoursSummary } from '@nxgt/material/components';
1645
+
1646
+ <OpeningHoursSummary data={hours} timeZone="America/Toronto" label="Opening hours" />
1440
1647
  ```
1441
1648
 
1442
- Parts: `OpeningHours` (the list) and `OpeningHoursDialog` (add / edit).
1649
+ It prints a status pill, "Open · closes at 6:00 PM" or "Closed · opens Monday
1650
+ at 9:00 AM", then the week with identical consecutive days grouped ("Mon–Fri
1651
+ 9:00 AM – 6:00 PM"), today's group tagged "Today" with the selected-row look.
1652
+ It is a new component, not a mode of `OpeningHours`, so the list keeps its
1653
+ callbacks and nothing changes for code that uses it. Without `now` the status
1654
+ and the "Today" mark appear once the page has mounted (a server render and its
1655
+ hydration then match); pass `now` to render them on the server or in a test.
1656
+ The week starts on the language's first day until the page has mounted, then
1657
+ on the locale's own (`weekStartsOn` sets it for both). With no hours, the
1658
+ summary shows a quiet "Hours not set" (`empty` replaces the text,
1659
+ `empty={null}` renders nothing).
1660
+
1661
+ | Prop | Type | Default | Effect |
1662
+ | --- | --- | --- | --- |
1663
+ | `data` | `OpeningHour[] \| null` | `[]` | the hours |
1664
+ | `now` | `Date` | the clock | the moment read as now |
1665
+ | `timeZone` | `string` | the viewer's | the IANA zone of the place, resolved with `Intl.DateTimeFormat` |
1666
+ | `locale` | `string` | the `I18nProvider` language, else `en` | day and time names |
1667
+ | `weekStartsOn` | `0`-`6` | the locale's | first day of the week |
1668
+ | `label` | `string` | "Opening Hours" | names the region |
1669
+ | `empty` | `ReactNode` | "Hours not set" | shown when no day is open; `null` renders nothing |
1670
+
1671
+ `OpeningHoursEditor` takes `value`, `onValueChange`, `label`, `headingLevel` (`3` by default), `weekStartsOn`,
1672
+ `locale` and `disabled`. `OpeningHoursFormField` takes `editing`
1673
+ (`'inline' \| 'dialog'`, default `'inline'`), `weekStartsOn` and `locale`
1674
+ (inline), and `showAddAllDays` (dialog, default `false`).
1675
+
1443
1676
  `openingHoursSchema()` returns the zod schema of one `OpeningHour`
1444
- (`dayOfWeek` coerced from a string), built on the first call. Under a
1445
- strict CSP, call `z.config({ jitless: true })` first: see
1677
+ (`dayOfWeek` coerced from a string), built on the first call. It is unchanged.
1678
+ `openingHoursListSchema()` is the schema of the whole week: the entries, plus
1679
+ the cross-field rules of the editor (close after open, both times set, no
1680
+ overlap on a day), each issue on its entry and field (`[2, 'closeTime']`). A
1681
+ blank time (`''`) is accepted by the entry schema and reported once, as
1682
+ `missing`. Each issue's `message` is an English default, and its `params.code`
1683
+ (`'close'`, `'overlap'`, `'missing'`) lets a form show the translated
1684
+ `material.opening-hours.errors.*` text the editor uses.
1685
+ Under a strict CSP, call `z.config({ jitless: true })` first: see
1446
1686
  [CSP](../guides/csp.md#zod-and-unsafe-eval).
1447
1687
 
1448
1688
  ```ts
1449
- import { openingHoursSchema } from '@nxgt/material/components';
1689
+ import { openingHoursListSchema } from '@nxgt/material/components';
1450
1690
  import { z } from 'zod';
1451
1691
 
1452
- const schema = z.object({ hours: z.array(openingHoursSchema()) });
1692
+ const schema = z.object({ hours: openingHoursListSchema() });
1453
1693
  ```
1454
1694
 
1455
- | Prop | Type | Default | Effect |
1456
- | --- | --- | --- | --- |
1457
- | `data` | `OpeningHour[] \| null` | `[]` | `{ dayOfWeek, openTime?, closeTime?, isClosed? }`, times as `'HH:mm'` |
1458
- | `onAdd` | `() => void` | — | shows the add button |
1459
- | `onAddAll` | `() => void` | — | shows "Add all days" |
1460
- | `onEdit` | `(value: OpeningHour, index: number) => void` | — | shows an edit button per day |
1461
-
1462
- `OpeningHoursFormField` takes `showAddAllDays` (default `false`) to offer the
1463
- "Add all days" button.
1464
-
1465
- **Emits:** `OpeningHoursFormField` stores `OpeningHour[]`, and shows the
1466
- array's error (`errors.<name>.message` or `.root.message`) under the list.
1695
+ **Emits:** `OpeningHoursFormField` stores `OpeningHour[]`: the inline editor
1696
+ writes a closed day as one `{ dayOfWeek, isClosed: true }` entry, and shows the
1697
+ array's error (`errors.<name>.message` or `.root.message`) under the editor.
1467
1698
  **Form wrapper:** `OpeningHoursFormField` — `control` and `name`, see
1468
1699
  [forms](../guides/forms.md#the-pair), [what each field emits](../guides/forms.md#what-each-field-emits)
1469
1700
  and [composite values](../guides/forms.md#composite-values).
1470
- **Storybook:** `Form fields/OpeningHours` · **Vue:** `OpeningHours`, `OpeningHoursDialog`, `OpeningHoursFormField`
1701
+ **Storybook:** `Form fields/OpeningHours`, `Form fields/OpeningHours/Editor`, `Form fields/OpeningHours/Summary` · **Vue:** `OpeningHours`, `OpeningHoursDialog`, `OpeningHoursFormField`
@@ -115,9 +115,9 @@ 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 (`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, `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
- | a dialog editor (`PostalAddressFormField`, `ContactsFormField`, `OpeningHoursFormField`, `AttributesFormField`, 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 |
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 |
122
122
  | `RichTextEditorFormField`, `CodeEditorFormField` | focus leaves the editor and its toolbar |
123
123
  | `FormField` (config-driven) | the rule of the field its `type` renders |
@@ -308,16 +308,16 @@ shows them right under its fields.
308
308
  `p-4` are recognised) sets `--form-actions-fill` to it:
309
309
 
310
310
  ```css
311
- .edit-school-form {
311
+ .edit-record-form {
312
312
  --form-actions-clearance: 9rem;
313
313
  }
314
- .edit-school-dialog {
314
+ .edit-record-dialog {
315
315
  --form-actions-fill: 2rem; /* its pb-8 */
316
316
  }
317
317
  ```
318
318
 
319
319
  ```tsx
320
- <form className="form-grid-layout edit-school-form">
320
+ <form className="form-grid-layout edit-record-form">
321
321
  ```
322
322
  - To scroll a form in a box of your own, give it a height and the scroll
323
323
  (`className="form-grid-layout max-h-[60vh] overflow-y-auto"`); to keep the
@@ -500,7 +500,7 @@ or fails validation.
500
500
  | `Slider` · `SliderFormField` | `number` when the default is a number; `number[]` when the default is an array (a range) | `0` / `[20, 80]` |
501
501
  | `Switch` · `SwitchFormField` | `boolean` | `false` |
502
502
  | `AttributesField` · `AttributesFormField` | `Attribute[]` | `[]` |
503
- | `ContactsFormField` | `Contact[]`: `{ type: ContactType; value: string; label?: string \| null }` | `[]` |
503
+ | `ContactsFormField` | `Contact[]`: `{ type: ContactType; value: string; label?: string \| null; primary?: boolean \| null }` | `[]` |
504
504
  | `PostalAddressFormField` | `PostalAddressFieldValue`: `{ street?, locality?, region?, postalCode?, country? }`; `{}` after delete | `{}` |
505
505
  | `OpeningHoursFormField` | `OpeningHour[]`: `{ dayOfWeek: 0–6; openTime?: 'HH:mm'; closeTime?: 'HH:mm'; isClosed?: boolean }` | `[]` |
506
506
 
@@ -607,16 +607,22 @@ export function SupplierForm({ onSave }: { onSave: (values: Values) => void }) {
607
607
 
608
608
  | Wrapper | Value | Notes |
609
609
  | --- | --- | --- |
610
- | `ContactsFormField` | `Contact[]` | Add, edit and delete go through `ContactDialog`. `ContactType` is `EMAIL`, `FAX`, `MOBILE`, `PHONE`, `WEBSITE` |
611
- | `OpeningHoursFormField` | `OpeningHour[]` | `showAddAllDays` adds Monday–Friday 09:00–17:00 and a closed weekend in one click |
610
+ | `ContactsFormField` | `Contact[]` | Edited in place by default (one `primary` per type); `editing="dialog"` goes through `ContactDialog`. `ContactType` is `EMAIL`, `FAX`, `MOBILE`, `PHONE`, `WEBSITE` |
611
+ | `OpeningHoursFormField` | `OpeningHour[]` | Edits the week inline by default (`weekStartsOn` and `locale` apply there); `editing="dialog"` keeps the list and dialog, where `showAddAllDays` adds Monday–Friday 09:00–17:00 and a closed weekend in one click |
612
612
  | `PostalAddressFormField` | `PostalAddressFieldValue` | Deleting the address writes `{}`, not `null` |
613
- | `AttributesFormField` | `Attribute[]` | A typed attribute editor. Its `label` prop is required |
614
-
613
+ | `AttributesFormField` | `Attribute[]` | A typed attribute editor, in place by default (rows that open, drag and keyboard reorder that sets `priority`, unique names); `editing="dialog"` keeps the list and dialog. Its `label` prop is required |
614
+
615
+ - **The list schemas carry the editors' rules.** `contactsListSchema()`,
616
+ `attributesListSchema()` and `openingHoursListSchema()` are zod schemas of
617
+ the whole array (see [Contacts](../components/form-fields.md#contacts),
618
+ [Attributes](../components/form-fields.md#attributes) and
619
+ [OpeningHours](../components/form-fields.md#openinghours)). Each issue has an
620
+ English `message` and a `params.code` to translate from.
615
621
  - **The array's own error is shown under the field.** `ContactsFormField`,
616
622
  `OpeningHoursFormField` and `AttributesFormField` display
617
623
  `errors.<name>.message` (where a Zod `.min(1)` on the array lands) and
618
624
  `errors.<name>.root` (where a `useFieldArray` rule lands). Errors of one
619
- item are shown in its dialog.
625
+ item are shown in its dialog, or under its range or name in the inline editor.
620
626
  - `AttributesFormField` follows the form value: `form.reset()` and
621
627
  `form.setValue()` replace the list it shows.
622
628
  - `AttributeValuesFormFieldSet` is the other half: given `attributes`, it
@@ -625,7 +631,7 @@ export function SupplierForm({ onSave }: { onSave: (values: Values) => void }) {
625
631
 
626
632
  ### Rows a user adds and removes
627
633
 
628
- When the rows are fields of their own — a school year's periods, each with a
634
+ When the rows are fields of their own — a plan's periods, each with a
629
635
  name, a start date and an end date — use
630
636
  [`RepeatableGroupFormField`](../components/form-fields.md#repeatablegroup)
631
637
  over `useFieldArray`, and render each row's fields with the form's own
@@ -255,7 +255,7 @@ their chevron, "Show the options" / "Afficher les options"
255
255
  (`material.combobox.toggle`),
256
256
  the `ThemePicker`'s group and mode names (`material.theme-picker.label` "Theme" / "Thème", `material.theme-picker.mode` "Mode"; the Light, Dark and System names are `ThemeToggle`'s own), the `LocationPreview` trigger
257
257
  (`material.map.preview` / `preview-named`), and the delete buttons of the
258
- `Contacts` / `OpeningHours` rows. `FacetGroup`'s toggle is named "Level, 2 selected" / "Level, 2 sélectionnés" (`material.facet-group.name`, over `{title}` and `{count}`: plain in English, an ICU plural in French) and its Clear button "Clear Level" / "Effacer Level" (`material.facet-group.clear-label`, with the visible `material.facet-group.clear`). `AccountMenu`'s avatar button is named "Account menu, {name}" / "Menu du compte, {name}" (`material.account-menu.trigger`, over `{name}`) and its last item reads "Sign out" / "Se déconnecter" (`material.account-menu.sign-out`), unless you pass `signOutLabel`; with `themeChoice` its "Theme" group reads "Theme" / "Thème", "Light" / "Clair", "Dark" / "Sombre", "System" / "Système" (`material.account-menu.theme.label`, `light`, `dark`, `system`). `LocalizedTextFields`' reset button is named "Reset {language} to the default" / "Rétablir la valeur par défaut ({language})" (`material.localized-text-fields.reset`), its tooltip reads "Default: {value}" (`reset-tooltip`) and its polite status "{language}: back to the default" (`reset-done`). Its "Other languages" trigger reads "Other languages ({count})" / "Autres langues ({count})" (`others-count`; `others` names the folded rows' group), and while closed over errors it is described by "{count} errors" / "{count} erreurs" (`others-errors`, an ICU plural over `{count}`). The language names come from `material.language.<code>` (English and French ship), then `Intl.DisplayNames`. `PrintDocument`'s page numbers
258
+ `Contacts` / `OpeningHours` rows, and the empty text of `ContactsSummary` and `AttributesSummary` (`material.contacts.labels.none` "No contacts" / "Aucun contact", `material.attributes.labels.none` "No attributes" / "Aucun attribut"; `@nxgt/material-vue` carries the same keys). `FacetGroup`'s toggle is named "Level, 2 selected" / "Level, 2 sélectionnés" (`material.facet-group.name`, over `{title}` and `{count}`: plain in English, an ICU plural in French) and its Clear button "Clear Level" / "Effacer Level" (`material.facet-group.clear-label`, with the visible `material.facet-group.clear`). `AccountMenu`'s avatar button is named "Account menu, {name}" / "Menu du compte, {name}" (`material.account-menu.trigger`, over `{name}`) and its last item reads "Sign out" / "Se déconnecter" (`material.account-menu.sign-out`), unless you pass `signOutLabel`; with `themeChoice` its "Theme" group reads "Theme" / "Thème", "Light" / "Clair", "Dark" / "Sombre", "System" / "Système" (`material.account-menu.theme.label`, `light`, `dark`, `system`). `LocalizedTextFields`' reset button is named "Reset {language} to the default" / "Rétablir la valeur par défaut ({language})" (`material.localized-text-fields.reset`), its tooltip reads "Default: {value}" (`reset-tooltip`) and its polite status "{language}: back to the default" (`reset-done`). Its "Other languages" trigger reads "Other languages ({count})" / "Autres langues ({count})" (`others-count`; `others` names the folded rows' group), and while closed over errors it is described by "{count} errors" / "{count} erreurs" (`others-errors`, an ICU plural over `{count}`). The language names come from `material.language.<code>` (English and French ship), then `Intl.DisplayNames`. `PrintDocument`'s page numbers
259
259
  read `material.print.page-of` ("Page {page} of {pages}", "Page {page} sur
260
260
  {pages}") and `material.print.page` ("Page {page}"), and a `PrintDataTable`
261
261
  reads `material.print.table.total` ("Total", "Total") for its totals row and
@@ -168,6 +168,7 @@ State the server cannot know must be resolved the same way on both sides:
168
168
  | Theme | no preference: light | An inline boot script in `<head>`, and `suppressHydrationWarning` on `<html>`. See [theming](theming.md#no-flash-on-load-a-boot-script) |
169
169
  | `formatAmount`, `formatDate` | the server's locale and time zone, unless you pass them | Use `useFormatters({ timeZone })`, which follows the `I18nProvider` language, or pass `locale` and `timeZone` (`createFormatters` binds them). A date-only `YYYY-MM-DD` is the same everywhere. See [printing](printing.md#formatamount-and-formatdate) |
170
170
  | Screen width, `prefers-color-scheme` (`useIsMobile`, `useTheme`, `AppHeader`, `DataTable`, `TimeField`, `Stepper`, `PaneLayout`, `ListDetailsLayout`, the code blocks, …) | no screen: a desktop, light | Nothing: a hydrating client renders the same, then switches on a phone or a dark screen |
171
+ | An attribute's `DATETIME` / `TIME` instant (`AttributesSummary`, `AttributesEditor`), the opening hours' first day of the week | the server's time zone and Intl data differ from the browser's | Nothing: the instant is written in UTC and the week starts on the language's first day until mount, then the viewer's zone and the locale's week info take over, so hydration matches |
171
172
  | Step form (`createStepFormSlice`) | no `localStorage`: step 0 | Nothing with `useSliceReducer`: a hydrating client renders step 0, then resumes. A slice in a Redux store: see [forms](forms.md#a-multi-step-form) |
172
173
  | Zod messages | no `localStorage`: English | Pass the request's language, `zodLocaleError({ language })`, where the server formats messages. See [forms](forms.md#validation-messages) |
173
174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.34.0",
3
+ "version": "3.35.1",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"