@zeyos/zx 4.3.4 → 5.0.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 (231) hide show
  1. package/CHANGELOG.md +438 -0
  2. package/README.md +49 -10
  3. package/dist/types/components/activity-item/activity-item.d.ts +351 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +217 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +33 -3
  6. package/dist/types/components/avatar/avatar.d.ts +46 -7
  7. package/dist/types/components/badge/badge.d.ts +21 -1
  8. package/dist/types/components/button/button.d.ts +23 -0
  9. package/dist/types/components/calendar/calendar.d.ts +19 -0
  10. package/dist/types/components/card/card.d.ts +7 -0
  11. package/dist/types/components/card-view/card-view.d.ts +11 -3
  12. package/dist/types/components/card-view/record-card.d.ts +7 -1
  13. package/dist/types/components/chart/chart.d.ts +10 -0
  14. package/dist/types/components/checklist/checklist.d.ts +9 -0
  15. package/dist/types/components/code-editor/code-editor.d.ts +149 -0
  16. package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
  17. package/dist/types/components/context-menu/context-menu.d.ts +18 -2
  18. package/dist/types/components/date-picker/date-picker.d.ts +8 -0
  19. package/dist/types/components/date-picker/time-picker.d.ts +7 -0
  20. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  21. package/dist/types/components/datebox/datebox.d.ts +7 -2
  22. package/dist/types/components/dialog/dialog.d.ts +75 -6
  23. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  24. package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
  25. package/dist/types/components/field/field.d.ts +24 -2
  26. package/dist/types/components/fieldset/fieldset.d.ts +19 -1
  27. package/dist/types/components/file-item/file-item.d.ts +262 -0
  28. package/dist/types/components/file-list/file-list.d.ts +171 -0
  29. package/dist/types/components/filter/filter.d.ts +91 -36
  30. package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
  31. package/dist/types/components/form/form.d.ts +12 -1
  32. package/dist/types/components/kanban-view/kanban-card.d.ts +146 -0
  33. package/dist/types/components/kanban-view/kanban-view.d.ts +170 -4
  34. package/dist/types/components/launcher/launcher.d.ts +278 -26
  35. package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
  36. package/dist/types/components/menu-button/menu-button.d.ts +37 -42
  37. package/dist/types/components/message/message.d.ts +126 -17
  38. package/dist/types/components/modal/modal.d.ts +61 -2
  39. package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
  40. package/dist/types/components/panel/panel.d.ts +7 -0
  41. package/dist/types/components/search/search.d.ts +7 -0
  42. package/dist/types/components/select/color.d.ts +12 -0
  43. package/dist/types/components/select/glyphs.d.ts +124 -0
  44. package/dist/types/components/select/select.d.ts +15 -3
  45. package/dist/types/components/sort-control/sort-control.d.ts +105 -0
  46. package/dist/types/components/stat-tile/stat-tile.d.ts +288 -0
  47. package/dist/types/components/tabbox/tabbox.d.ts +16 -0
  48. package/dist/types/components/table/table.d.ts +190 -1
  49. package/dist/types/components/table-view/table-view.d.ts +11 -5
  50. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  51. package/dist/types/components/timebox/timebox.d.ts +7 -0
  52. package/dist/types/components/tree/tree.d.ts +13 -0
  53. package/dist/types/components/value-list/value-list.d.ts +8 -0
  54. package/dist/types/components/view/record-view.d.ts +96 -5
  55. package/dist/types/core/breakpoint.d.ts +1 -1
  56. package/dist/types/core/icons.d.ts +37 -22
  57. package/dist/types/core/motion.d.ts +59 -0
  58. package/dist/types/core/overlay-host.d.ts +15 -1
  59. package/dist/types/index.d.ts +16 -3
  60. package/dist/types/internal/app-rail.d.ts +10 -0
  61. package/dist/types/internal/menu-items.d.ts +134 -0
  62. package/dist/types/zeyos/connect.d.ts +6 -0
  63. package/dist/types/zeyos/form.d.ts +50 -9
  64. package/dist/types/zeyos/index.d.ts +1 -1
  65. package/dist/types/zeyos/query.d.ts +79 -0
  66. package/dist/types/zeyos/schema.d.ts +24 -1
  67. package/dist/types/zeyos/table.d.ts +100 -14
  68. package/dist/types/zeyos/view.d.ts +10 -0
  69. package/dist/zx-zeyos.esm.js +2206 -489
  70. package/dist/zx-zeyos.esm.min.js +56 -1
  71. package/dist/zx.css +3322 -724
  72. package/dist/zx.esm.js +12244 -5809
  73. package/dist/zx.esm.min.js +58 -3
  74. package/dist/zx.global.js +12246 -5811
  75. package/dist/zx.global.min.js +58 -3
  76. package/dist/zx.min.css +1 -1
  77. package/docs/api.json +1683 -146
  78. package/docs/llms.md +777 -110
  79. package/docs/llms.txt +14 -8
  80. package/package.json +1 -1
  81. package/src/components/account-menu/account-menu.css +35 -10
  82. package/src/components/activity-item/activity-item.css +137 -0
  83. package/src/components/activity-item/activity-item.js +602 -0
  84. package/src/components/activity-list/activity-list.css +99 -0
  85. package/src/components/activity-list/activity-list.js +409 -0
  86. package/src/components/app-icon/app-icon.css +1 -1
  87. package/src/components/app-rail/app-rail.css +191 -22
  88. package/src/components/app-sidebar/app-sidebar.css +349 -24
  89. package/src/components/app-sidebar/app-sidebar.js +86 -11
  90. package/src/components/aurora/aurora.css +5 -3
  91. package/src/components/avatar/avatar.css +32 -7
  92. package/src/components/avatar/avatar.js +72 -8
  93. package/src/components/badge/badge.css +54 -2
  94. package/src/components/badge/badge.js +29 -2
  95. package/src/components/button/button.css +74 -8
  96. package/src/components/button/button.js +44 -1
  97. package/src/components/calendar/calendar.css +46 -46
  98. package/src/components/calendar/calendar.js +33 -3
  99. package/src/components/card/card.css +2 -2
  100. package/src/components/card/card.js +12 -1
  101. package/src/components/card-view/card-view.css +10 -7
  102. package/src/components/card-view/card-view.js +25 -9
  103. package/src/components/card-view/record-card.js +7 -3
  104. package/src/components/chart/chart.css +4 -1
  105. package/src/components/chart/chart.js +12 -0
  106. package/src/components/check-button/check-button.css +10 -2
  107. package/src/components/checklist/checklist.css +9 -2
  108. package/src/components/checklist/checklist.js +11 -5
  109. package/src/components/checklist/field-adapter.js +3 -1
  110. package/src/components/code-editor/code-editor.css +68 -0
  111. package/src/components/code-editor/code-editor.js +306 -0
  112. package/src/components/code-editor/field-adapter.js +34 -0
  113. package/src/components/context-menu/context-menu.css +87 -5
  114. package/src/components/context-menu/context-menu.js +22 -30
  115. package/src/components/copy/copy-button.css +6 -0
  116. package/src/components/copy/copy-input.css +8 -3
  117. package/src/components/data-filter/data-filter.css +5 -4
  118. package/src/components/date-picker/date-picker.css +6 -6
  119. package/src/components/date-picker/date-picker.js +22 -8
  120. package/src/components/date-picker/date-range-picker.css +3 -3
  121. package/src/components/date-picker/month-picker.js +3 -3
  122. package/src/components/date-picker/time-picker.css +1 -1
  123. package/src/components/date-picker/time-picker.js +20 -8
  124. package/src/components/datebox/date-range-box.css +79 -2
  125. package/src/components/datebox/date-range-box.js +13 -2
  126. package/src/components/datebox/datebox.css +79 -2
  127. package/src/components/datebox/datebox.js +13 -2
  128. package/src/components/dialog/dialog.css +26 -0
  129. package/src/components/dialog/dialog.js +174 -70
  130. package/src/components/dropdown/dropdown.css +1 -1
  131. package/src/components/dropdown/dropdown.js +7 -5
  132. package/src/components/empty-state/empty-state.css +2 -2
  133. package/src/components/entity-ref/entity-ref.css +182 -0
  134. package/src/components/entity-ref/entity-ref.js +496 -0
  135. package/src/components/field/field.css +28 -9
  136. package/src/components/field/field.js +68 -26
  137. package/src/components/field-upload/field-upload.css +1 -1
  138. package/src/components/fieldset/fieldset.css +5 -3
  139. package/src/components/fieldset/fieldset.js +28 -5
  140. package/src/components/file-item/file-item.css +88 -0
  141. package/src/components/file-item/file-item.js +505 -0
  142. package/src/components/file-list/file-list.css +51 -0
  143. package/src/components/file-list/file-list.js +320 -0
  144. package/src/components/filter/filter.css +27 -1
  145. package/src/components/filter/filter.js +205 -47
  146. package/src/components/filter-panel/filter-panel.css +104 -0
  147. package/src/components/filter-panel/filter-panel.js +964 -0
  148. package/src/components/finder/finder.css +15 -13
  149. package/src/components/form/form.css +5 -4
  150. package/src/components/form/form.js +11 -2
  151. package/src/components/grid/grid.css +1 -1
  152. package/src/components/groupbox/groupbox.css +1 -1
  153. package/src/components/kanban-view/kanban-card.js +247 -0
  154. package/src/components/kanban-view/kanban-view.css +226 -8
  155. package/src/components/kanban-view/kanban-view.js +255 -12
  156. package/src/components/launcher/launcher.css +107 -25
  157. package/src/components/launcher/launcher.js +557 -63
  158. package/src/components/list-toolbar/list-toolbar.css +194 -0
  159. package/src/components/list-toolbar/list-toolbar.js +851 -0
  160. package/src/components/master-panel/master-panel.css +2 -0
  161. package/src/components/menu-button/menu-button.css +87 -5
  162. package/src/components/menu-button/menu-button.js +86 -46
  163. package/src/components/message/message.css +38 -5
  164. package/src/components/message/message.js +230 -17
  165. package/src/components/modal/modal.css +15 -0
  166. package/src/components/modal/modal.js +159 -3
  167. package/src/components/navigation-bar/navigation-bar.css +95 -11
  168. package/src/components/navigation-bar/navigation-bar.js +171 -4
  169. package/src/components/number-field/field-adapter.js +1 -0
  170. package/src/components/number-field/number-field.css +70 -0
  171. package/src/components/number-field/number-field.js +2 -0
  172. package/src/components/pagination/pagination.css +8 -6
  173. package/src/components/panel/panel.css +1 -1
  174. package/src/components/panel/panel.js +13 -2
  175. package/src/components/rating/rating.css +7 -0
  176. package/src/components/search/search.js +17 -5
  177. package/src/components/select/color.js +22 -0
  178. package/src/components/select/field-adapter.js +6 -1
  179. package/src/components/select/glyphs.js +165 -0
  180. package/src/components/select/select.css +138 -45
  181. package/src/components/select/select.js +86 -47
  182. package/src/components/sheet/sheet.css +11 -8
  183. package/src/components/slider/slider.css +16 -2
  184. package/src/components/slider/slider.js +1 -1
  185. package/src/components/sort-control/sort-control.css +20 -0
  186. package/src/components/sort-control/sort-control.js +140 -0
  187. package/src/components/stat-tile/stat-tile.css +208 -0
  188. package/src/components/stat-tile/stat-tile.js +601 -0
  189. package/src/components/tabbox/tabbox.css +32 -27
  190. package/src/components/tabbox/tabbox.js +17 -2
  191. package/src/components/table/table.css +155 -31
  192. package/src/components/table/table.js +386 -33
  193. package/src/components/table-view/table-view.css +8 -2
  194. package/src/components/table-view/table-view.js +21 -6
  195. package/src/components/tag-picker/tag-picker.css +86 -6
  196. package/src/components/tag-picker/tag-picker.js +25 -8
  197. package/src/components/timebox/timebox.css +22 -2
  198. package/src/components/timebox/timebox.js +18 -5
  199. package/src/components/toggle/toggle.css +12 -1
  200. package/src/components/toolbar/toolbar.js +2 -2
  201. package/src/components/tooltip/tooltip.css +4 -0
  202. package/src/components/tooltip/tooltip.js +10 -3
  203. package/src/components/tree/tree.css +25 -16
  204. package/src/components/tree/tree.js +31 -0
  205. package/src/components/value-list/value-list.css +3 -3
  206. package/src/components/value-list/value-list.js +22 -6
  207. package/src/components/view/record-view.css +5 -0
  208. package/src/components/view/record-view.js +100 -7
  209. package/src/core/breakpoint.js +1 -1
  210. package/src/core/icons.js +259 -57
  211. package/src/core/motion.js +147 -0
  212. package/src/core/overlay-host.js +21 -2
  213. package/src/index.js +30 -3
  214. package/src/internal/app-rail.js +18 -1
  215. package/src/internal/menu-items.js +142 -0
  216. package/src/zeyos/connect.js +6 -2
  217. package/src/zeyos/form.js +67 -27
  218. package/src/zeyos/index.js +1 -1
  219. package/src/zeyos/query.js +153 -0
  220. package/src/zeyos/schema.js +144 -27
  221. package/src/zeyos/table.js +68 -5
  222. package/src/zeyos/view.js +6 -0
  223. package/styles/base.css +56 -0
  224. package/styles/icons.css +11 -1
  225. package/styles/print.css +192 -0
  226. package/styles/tokens/dark.css +113 -37
  227. package/styles/tokens/global.css +121 -27
  228. package/styles/tokens/semantic.css +100 -31
  229. package/styles/tokens/themes.css +7 -6
  230. package/styles/zx.css +11 -1
  231. package/styles/tokens/density.css +0 -9
@@ -4,7 +4,10 @@ import { uid } from '../../core/util.js';
4
4
 
5
5
  /**
6
6
  * @typedef {Object} FieldOptions
7
- * @property {string|null} [id=null] Control ID and logical field identifier.
7
+ * @property {string|null} [id=null] DOM id of the control. Generated when omitted; a Fieldset
8
+ * derives it from its `idPrefix` and the field key, so a key never becomes a DOM id by itself.
9
+ * @property {string|null} [name=null] Field name: the `name` of a native control and the key the
10
+ * field is filed under. A Fieldset sets it to the field key; a standalone field falls back to `id`.
8
11
  * @property {string} [type='text'] Registered field type.
9
12
  * @property {string} [label=''] Visible label.
10
13
  * @property {string} [description=''] Supporting description.
@@ -28,6 +31,9 @@ import { uid } from '../../core/util.js';
28
31
  * @property {() => void} focus Focus the control.
29
32
  * @property {(disabled: boolean) => void} setDisabled Set disabled state.
30
33
  * @property {() => void} [destroy] Release adapter-owned resources.
34
+ * @property {Element} [labelTarget] The element the field's label, description, and validity
35
+ * belong to when it is not `el` — the text input inside a composite control. Left out, a
36
+ * wrapper that cannot be labelled hands them to the first unnamed form control inside it.
31
37
  */
32
38
 
33
39
  /** @typedef {(field: Field, options: Readonly<FieldOptions>) => FieldAdapter} FieldAdapterFactory */
@@ -45,6 +51,7 @@ export class Field extends Component {
45
51
  /** @type {FieldOptions & {msg: Record<string, string>}} */
46
52
  static defaults = {
47
53
  id: null,
54
+ name: null,
48
55
  type: 'text',
49
56
  label: '',
50
57
  description: '',
@@ -88,9 +95,10 @@ export class Field extends Component {
88
95
  return Field.#adapters.has(String(type).trim().toLowerCase());
89
96
  }
90
97
 
91
- // `adapter` (FieldAdapter), `initialValue`, and `controlId` are assigned in render();
92
- // declaring them as class fields would re-initialize them to undefined after the base
93
- // constructor (which calls render()) returns — see AGENTS.md.
98
+ // `adapter` (FieldAdapter), `initialValue`, `controlId` (the control's DOM id), and `name` (the
99
+ // field name, '' when there is none) are assigned in render(); declaring them as class fields
100
+ // would re-initialize them to undefined after the base constructor (which calls render())
101
+ // returns — see AGENTS.md.
94
102
 
95
103
  /** @returns {HTMLElement} */
96
104
  render() {
@@ -104,7 +112,10 @@ export class Field extends Component {
104
112
  root.dataset.layout = layout;
105
113
  root.dataset.fieldType = type;
106
114
 
115
+ /** @type {string} The control's DOM id: `options.id`, or a generated one. */
107
116
  this.controlId = this.options.id ? String(this.options.id) : uid('zx-field');
117
+ /** @type {string} The field name (`options.name`, else `options.id`), '' when there is none. */
118
+ this.name = String(this.options.name ?? this.options.id ?? '');
108
119
  const labelId = `${this.controlId}-label`;
109
120
  const descriptionId = `${this.controlId}-description`;
110
121
  const highlightId = `${this.controlId}-highlight`;
@@ -148,9 +159,14 @@ export class Field extends Component {
148
159
 
149
160
  const control = this.adapter.el;
150
161
  if (!control.id) control.id = this.controlId;
151
- label.htmlFor = control.id;
152
- if (!isLabelable(control)) control.setAttribute('aria-labelledby', labelId);
153
- this._baseDescribedBy = control.getAttribute('aria-describedby') ?? '';
162
+ // A wrapper (a `custom` adapter's box) cannot carry a name; the control inside it can.
163
+ const labelled = this.adapter.labelTarget
164
+ ?? (isLabelable(control) ? control : unnamedControlWithin(control)) ?? control;
165
+ this._labelled = labelled;
166
+ if (labelled !== control && isLabelable(labelled) && !labelled.id) labelled.id = `${this.controlId}-input`;
167
+ label.htmlFor = isLabelable(labelled) ? labelled.id : control.id;
168
+ if (!isLabelable(labelled)) labelled.setAttribute('aria-labelledby', labelId);
169
+ this._baseDescribedBy = labelled.getAttribute('aria-describedby') ?? '';
154
170
  controlSlot.append(control);
155
171
  this._descriptionId = String(this.options.description ?? '') ? descriptionId : '';
156
172
  this._highlightId = highlightId;
@@ -231,11 +247,11 @@ export class Field extends Component {
231
247
  this._highlight.hidden = !message;
232
248
  if (normalizedKind === 'danger' && message) {
233
249
  this._highlight.setAttribute('role', 'alert');
234
- this.adapter.el.setAttribute('aria-invalid', 'true');
250
+ this._labelled.setAttribute('aria-invalid', 'true');
235
251
  this.emit('invalid', { message: String(message) });
236
252
  } else {
237
253
  this._highlight.removeAttribute('role');
238
- this.adapter.el.removeAttribute('aria-invalid');
254
+ this._labelled.removeAttribute('aria-invalid');
239
255
  }
240
256
  this._syncDescription(Boolean(message));
241
257
  return this;
@@ -250,7 +266,7 @@ export class Field extends Component {
250
266
  this._highlight.textContent = '';
251
267
  this._highlight.hidden = true;
252
268
  this._highlight.removeAttribute('role');
253
- this.adapter.el.removeAttribute('aria-invalid');
269
+ this._labelled.removeAttribute('aria-invalid');
254
270
  this._syncDescription(false);
255
271
  return this;
256
272
  }
@@ -279,8 +295,8 @@ export class Field extends Component {
279
295
  _syncDescription(highlighted = false) {
280
296
  const ids = [this._baseDescribedBy, this._descriptionId, highlighted ? this._highlightId : '']
281
297
  .flatMap((value) => value.split(/\s+/)).filter(Boolean);
282
- if (ids.length) this.adapter.el.setAttribute('aria-describedby', [...new Set(ids)].join(' '));
283
- else this.adapter.el.removeAttribute('aria-describedby');
298
+ if (ids.length) this._labelled.setAttribute('aria-describedby', [...new Set(ids)].join(' '));
299
+ else this._labelled.removeAttribute('aria-describedby');
284
300
  }
285
301
 
286
302
  /** @returns {void} */
@@ -400,6 +416,22 @@ function isLabelable(element) {
400
416
  return ['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA'].includes(element.tagName);
401
417
  }
402
418
 
419
+ /**
420
+ * The first form control inside `root` that nothing names yet — no `aria-label`, no
421
+ * `aria-labelledby`, no `<label>` with any text — or null.
422
+ * @param {Element} root
423
+ * @returns {Element|null}
424
+ */
425
+ function unnamedControlWithin(root) {
426
+ for (const candidate of root.querySelectorAll('input:not([type="hidden"]), select, textarea')) {
427
+ if (candidate.hasAttribute('aria-label') || candidate.hasAttribute('aria-labelledby')) continue;
428
+ const labels = [.../** @type {HTMLInputElement} */ (candidate).labels ?? []];
429
+ if (labels.some((label) => label.textContent?.trim())) continue;
430
+ return candidate;
431
+ }
432
+ return null;
433
+ }
434
+
403
435
  /** @param {HTMLElement} element @param {string[]} names @returns {Map<string, string|null>} */
404
436
  function rememberAttributes(element, names) {
405
437
  return new Map(names.map((name) => [name, element.getAttribute(name)]));
@@ -413,19 +445,26 @@ function restoreAttributes(element, attributes) {
413
445
  }
414
446
  }
415
447
 
416
- /** @param {Readonly<FieldOptions>} options @param {Record<string, unknown>} base @returns {Record<string, unknown>} */
417
- function controlProps(options, base) {
448
+ /**
449
+ * The properties a native control is built with: the adapter's own `base` over the caller's
450
+ * `props`, with the field name as the control's `name` unless `props` gives one.
451
+ * @param {Field} field Field the control belongs to.
452
+ * @param {Readonly<FieldOptions>} options Field options.
453
+ * @param {Record<string, unknown>} base Properties the adapter owns.
454
+ * @returns {Record<string, unknown>}
455
+ */
456
+ function controlProps(field, options, base) {
418
457
  const props = { ...(options.props ?? {}) };
419
458
  for (const key of Object.keys(props)) {
420
459
  if (/^on[a-z]/i.test(key) || key === 'content') delete props[key];
421
460
  }
422
- return { ...props, ...base };
461
+ return { name: field.name || undefined, ...props, ...base };
423
462
  }
424
463
 
425
464
  /** @param {'input'|'textarea'} tag @param {string} type @returns {FieldAdapterFactory} */
426
465
  function textAdapter(tag, type) {
427
- return (_field, options) => {
428
- const el = h(tag, controlProps(options, {
466
+ return (field, options) => {
467
+ const el = h(tag, controlProps(field, options, {
429
468
  type: tag === 'input' ? type : undefined,
430
469
  placeholder: options.placeholder,
431
470
  required: options.required,
@@ -450,7 +489,7 @@ function elementValueAdapter(el) {
450
489
  function numericAdapter(kind) {
451
490
  return (field, options) => {
452
491
  const coerce = kind === 'int' ? coerceInteger : coerceFloat;
453
- const input = h('input', controlProps(options, {
492
+ const input = h('input', controlProps(field, options, {
454
493
  type: 'text',
455
494
  inputMode: kind === 'int' ? 'numeric' : 'decimal',
456
495
  placeholder: options.placeholder,
@@ -474,8 +513,8 @@ function numericAdapter(kind) {
474
513
  }
475
514
 
476
515
  /** @type {FieldAdapterFactory} */
477
- const checkboxAdapter = (_field, options) => {
478
- const input = h('input', controlProps(options, {
516
+ const checkboxAdapter = (field, options) => {
517
+ const input = h('input', controlProps(field, options, {
479
518
  type: 'checkbox',
480
519
  required: options.required,
481
520
  disabled: options.disabled
@@ -490,8 +529,8 @@ const checkboxAdapter = (_field, options) => {
490
529
  };
491
530
 
492
531
  /** @type {FieldAdapterFactory} */
493
- const selectAdapter = (_field, options) => {
494
- const select = h('select', controlProps(options, {
532
+ const selectAdapter = (field, options) => {
533
+ const select = h('select', controlProps(field, options, {
495
534
  required: options.required,
496
535
  disabled: options.disabled
497
536
  }));
@@ -502,12 +541,15 @@ const selectAdapter = (_field, options) => {
502
541
  };
503
542
 
504
543
  /** @type {FieldAdapterFactory} */
505
- const optionListAdapter = (_field, options) => {
544
+ const optionListAdapter = (field, options) => {
506
545
  const root = h('div', {
507
546
  class: 'zx-field__option-list',
508
547
  role: 'radiogroup'
509
548
  });
510
- const name = options.id ? String(options.id) : uid('zx-option-list');
549
+ // The radios are grouped by the control's id, not by the field key: radios outside a <form>
550
+ // share one group per name across the whole document, so the same fieldset mounted twice
551
+ // would otherwise check one copy's option by clicking the other's.
552
+ const name = field.controlId;
511
553
  const radios = normalizeOptions(options.options).map(([value, label], index) => {
512
554
  const input = h('input', {
513
555
  type: 'radio',
@@ -534,8 +576,8 @@ const optionListAdapter = (_field, options) => {
534
576
  };
535
577
 
536
578
  /** @type {FieldAdapterFactory} */
537
- const htmlAdapter = (_field, options) => {
538
- const el = h('div', controlProps(options, { class: 'zx-field__static' }));
579
+ const htmlAdapter = (field, options) => {
580
+ const el = h('div', controlProps(field, options, { class: 'zx-field__static', name: undefined }));
539
581
  let adopted = null;
540
582
  const set = (value) => {
541
583
  restoreAdopted(adopted);
@@ -32,7 +32,7 @@
32
32
 
33
33
  .zx-field-upload__prompt {
34
34
  color: var(--zx-color-text);
35
- font-weight: 600;
35
+ font-weight: var(--zx-weight-medium);
36
36
  }
37
37
 
38
38
  .zx-field-upload__hint,
@@ -15,11 +15,13 @@
15
15
  inline-size: 100%;
16
16
  }
17
17
 
18
+ /* A caption for the group, in sentence case and the same quiet voice as the field labels inside
19
+ it — the fields are the content. */
18
20
  .zx-fieldset__legend {
19
21
  padding-inline: var(--zx-space-2);
20
- color: var(--zx-color-text);
21
- font-size: var(--zx-text-lg);
22
- font-weight: 600;
22
+ color: var(--zx-color-text-muted);
23
+ font-size: var(--zx-text-sm);
24
+ font-weight: var(--zx-weight-medium);
23
25
  }
24
26
 
25
27
  .zx-fieldset__grid {
@@ -1,6 +1,6 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h } from '../../core/dom.js';
3
- import { isElement } from '../../core/util.js';
3
+ import { isElement, uid } from '../../core/util.js';
4
4
  import { Field } from '../field/field.js';
5
5
 
6
6
  /**
@@ -8,6 +8,9 @@ import { Field } from '../field/field.js';
8
8
  * @property {string} [title=''] Legend text.
9
9
  * @property {1|2|3} [columns=1] Grid column count.
10
10
  * @property {Record<string, FieldOptions|Field|Element|Component>} [fields={}] Initial fields.
11
+ * @property {string|null} [idPrefix=null] Prefix of the DOM ids generated for the fields, as
12
+ * `<idPrefix>-<key>`. Null generates one per instance, so the same fieldset mounted twice
13
+ * never repeats an id and a field key never collides with an element id elsewhere on the page.
11
14
  */
12
15
 
13
16
  /** @typedef {import('../field/field.js').FieldOptions} FieldOptions */
@@ -20,7 +23,7 @@ export class Fieldset extends Component {
20
23
  static cssName = 'fieldset';
21
24
 
22
25
  /** @type {FieldsetOptions} */
23
- static defaults = { title: '', columns: 1, fields: {} };
26
+ static defaults = { title: '', columns: 1, fields: {}, idPrefix: null };
24
27
 
25
28
  /** @returns {HTMLElement} */
26
29
  render() {
@@ -30,6 +33,7 @@ export class Fieldset extends Component {
30
33
  this._origins = new Map();
31
34
  this._fieldIdAttributes = new Map();
32
35
  this._ownedNodes = [];
36
+ this._idPrefix = this.options.idPrefix ? String(this.options.idPrefix) : uid('zx-fieldset');
33
37
  this._columnAttribute = root.getAttribute('data-columns');
34
38
  const columns = [1, 2, 3].includes(Number(this.options.columns)) ? Number(this.options.columns) : 1;
35
39
  root.dataset.columns = String(columns);
@@ -49,7 +53,9 @@ export class Fieldset extends Component {
49
53
  }
50
54
 
51
55
  /**
52
- * Adds a field from options, an existing field, an element, or a component.
56
+ * Adds a field from options, an existing field, an element, or a component. The key becomes the
57
+ * field's `name` and its `data-field-id`; the control's DOM id is `<idPrefix>-<key>`, unless the
58
+ * field options give an `id` of their own that differs from the key.
53
59
  * @param {string} id Logical field identifier.
54
60
  * @param {FieldOptions|Field|Element|Component} field Field source.
55
61
  * @returns {Field}
@@ -64,7 +70,8 @@ export class Fieldset extends Component {
64
70
  } else if (isElement(field) || field instanceof Component) {
65
71
  const source = field;
66
72
  instance = new Field(null, {
67
- id: key,
73
+ id: this._controlId(key),
74
+ name: key,
68
75
  type: 'custom',
69
76
  label: key,
70
77
  value: readExternalValue(source),
@@ -74,7 +81,14 @@ export class Fieldset extends Component {
74
81
  owned = true;
75
82
  } else {
76
83
  const options = field && typeof field === 'object' ? field : {};
77
- instance = new Field(null, { ...options, id: options.id ?? key });
84
+ // An `id` equal to the key is the key restated (schema descriptors carry both), not a
85
+ // request for that DOM id.
86
+ const explicit = options.id != null && String(options.id) !== '' && String(options.id) !== key;
87
+ instance = new Field(null, {
88
+ ...options,
89
+ id: explicit ? String(options.id) : this._controlId(key),
90
+ name: options.name ?? key
91
+ });
78
92
  owned = true;
79
93
  }
80
94
 
@@ -218,6 +232,15 @@ export class Fieldset extends Component {
218
232
  return this;
219
233
  }
220
234
 
235
+ /**
236
+ * The DOM id generated for a field key: unique per instance unless `idPrefix` was given.
237
+ * @param {string} key Field key.
238
+ * @returns {string}
239
+ */
240
+ _controlId(key) {
241
+ return `${this._idPrefix}-${key.trim().replace(/\s+/g, '-')}`;
242
+ }
243
+
221
244
  /** @param {Field} field @returns {void} */
222
245
  _releaseField(field) {
223
246
  const fieldId = this._fieldIdAttributes.get(field);
@@ -0,0 +1,88 @@
1
+ .zx-file-item {
2
+ display: grid;
3
+ grid-template-columns: minmax(0, 1fr) auto;
4
+ gap: var(--zx-space-2) var(--zx-space-3);
5
+ align-items: center;
6
+ min-inline-size: 0;
7
+ }
8
+
9
+ .zx-file-item__entity {
10
+ min-inline-size: 0;
11
+ }
12
+
13
+ .zx-file-item__status {
14
+ justify-self: end;
15
+ }
16
+
17
+ .zx-file-item__status[hidden],
18
+ .zx-file-item__progress[hidden] {
19
+ display: none;
20
+ }
21
+
22
+ .zx-file-item__progress {
23
+ display: grid;
24
+ grid-column: 1 / -1;
25
+ grid-template-columns: minmax(0, 1fr) auto;
26
+ gap: var(--zx-space-2);
27
+ align-items: center;
28
+ padding-inline-start: calc(var(--zx-control-height) + var(--zx-space-2));
29
+ }
30
+
31
+ .zx-file-item__progress-head {
32
+ grid-column: 2;
33
+ color: var(--zx-color-text-muted);
34
+ font-size: var(--zx-text-sm);
35
+ }
36
+
37
+ .zx-file-item__progress-track {
38
+ grid-column: 1 / -1;
39
+ block-size: 4px;
40
+ overflow: hidden;
41
+ border-radius: var(--zx-radius-pill);
42
+ background: var(--zx-color-bg-muted);
43
+ }
44
+
45
+ .zx-file-item__progress-fill {
46
+ inline-size: var(--zx-file-progress, 0%);
47
+ block-size: 100%;
48
+ border-radius: inherit;
49
+ background: var(--zx-color-accent);
50
+ }
51
+
52
+ .zx-file-item[data-status="success"] .zx-file-item__progress-fill {
53
+ background: var(--zx-color-success);
54
+ }
55
+
56
+ .zx-file-item[data-status="error"] .zx-file-item__progress-fill {
57
+ background: var(--zx-color-danger);
58
+ }
59
+
60
+ .zx-file-item__progress[data-indeterminate] .zx-file-item__progress-fill {
61
+ inline-size: 35%;
62
+ transform: translateX(-110%);
63
+ }
64
+
65
+ @media (prefers-reduced-motion: no-preference) {
66
+ .zx-file-item__progress-fill {
67
+ transition: inline-size var(--zx-dur-fast) var(--zx-ease);
68
+ }
69
+
70
+ .zx-file-item__progress[data-indeterminate] .zx-file-item__progress-fill {
71
+ animation: zx-file-progress-slide 1200ms var(--zx-ease) infinite;
72
+ }
73
+ }
74
+
75
+ @keyframes zx-file-progress-slide {
76
+ to { transform: translateX(320%); }
77
+ }
78
+
79
+ @media (max-width: 36rem) {
80
+ .zx-file-item {
81
+ grid-template-columns: minmax(0, 1fr);
82
+ }
83
+
84
+ .zx-file-item__status {
85
+ justify-self: start;
86
+ padding-inline-start: calc(var(--zx-control-height) + var(--zx-space-2));
87
+ }
88
+ }