@zeyos/zx 4.4.0 → 5.2.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 (252) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/README.md +44 -11
  3. package/dist/types/components/account-menu/account-menu.d.ts +8 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +31 -1
  5. package/dist/types/components/app-icon/app-icon.d.ts +14 -2
  6. package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
  7. package/dist/types/components/avatar/avatar.d.ts +46 -7
  8. package/dist/types/components/badge/badge.d.ts +21 -1
  9. package/dist/types/components/button/button.d.ts +15 -0
  10. package/dist/types/components/calendar/calendar.d.ts +67 -5
  11. package/dist/types/components/card-view/card-view.d.ts +7 -4
  12. package/dist/types/components/card-view/record-card.d.ts +11 -0
  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 +6 -17
  16. package/dist/types/components/context-menu/context-menu.d.ts +6 -2
  17. package/dist/types/components/data-filter/data-filter.d.ts +4 -4
  18. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  19. package/dist/types/components/datebox/datebox.d.ts +7 -2
  20. package/dist/types/components/dialog/dialog.d.ts +46 -6
  21. package/dist/types/components/dropdown/dropdown.d.ts +24 -9
  22. package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
  23. package/dist/types/components/field/field.d.ts +54 -2
  24. package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
  25. package/dist/types/components/fieldset/fieldset.d.ts +26 -1
  26. package/dist/types/components/file-item/file-item.d.ts +34 -6
  27. package/dist/types/components/file-list/file-list.d.ts +35 -3
  28. package/dist/types/components/filter/filter.d.ts +127 -3
  29. package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
  30. package/dist/types/components/form/form.d.ts +12 -1
  31. package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
  32. package/dist/types/components/launcher/launcher.d.ts +268 -15
  33. package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
  34. package/dist/types/components/menu-button/menu-button.d.ts +39 -5
  35. package/dist/types/components/message/message.d.ts +132 -17
  36. package/dist/types/components/modal/modal.d.ts +43 -2
  37. package/dist/types/components/module-bar/module-bar.d.ts +395 -0
  38. package/dist/types/components/pagination/pagination.d.ts +17 -3
  39. package/dist/types/components/select/color.d.ts +12 -0
  40. package/dist/types/components/select/glyphs.d.ts +124 -0
  41. package/dist/types/components/select/select.d.ts +17 -3
  42. package/dist/types/components/sheet/sheet.d.ts +15 -0
  43. package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
  44. package/dist/types/components/tabbox/tabbox.d.ts +9 -0
  45. package/dist/types/components/table/table.d.ts +311 -18
  46. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  47. package/dist/types/components/tree/tree.d.ts +13 -0
  48. package/dist/types/components/view/record-view.d.ts +59 -2
  49. package/dist/types/components/view-settings/view-settings.d.ts +879 -0
  50. package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
  51. package/dist/types/core/breakpoint.d.ts +1 -1
  52. package/dist/types/core/component.d.ts +12 -2
  53. package/dist/types/core/i18n.d.ts +27 -3
  54. package/dist/types/core/icons.d.ts +23 -25
  55. package/dist/types/core/keyboard.d.ts +98 -1
  56. package/dist/types/core/messages.d.ts +382 -0
  57. package/dist/types/core/motion.d.ts +59 -0
  58. package/dist/types/core/overlay-host.d.ts +42 -1
  59. package/dist/types/core/overlay-stack.d.ts +260 -0
  60. package/dist/types/index.d.ts +10 -5
  61. package/dist/types/internal/app-rail.d.ts +38 -0
  62. package/dist/types/internal/menu-items.d.ts +71 -3
  63. package/dist/types/zeyos/connect.d.ts +6 -0
  64. package/dist/types/zeyos/form.d.ts +50 -9
  65. package/dist/types/zeyos/index.d.ts +2 -1
  66. package/dist/types/zeyos/launcher.d.ts +9 -0
  67. package/dist/types/zeyos/module-bar.d.ts +176 -0
  68. package/dist/types/zeyos/query.d.ts +110 -5
  69. package/dist/types/zeyos/schema.d.ts +24 -1
  70. package/dist/types/zeyos/table.d.ts +345 -22
  71. package/dist/types/zeyos/view.d.ts +26 -2
  72. package/dist/zx-zeyos.esm.js +6178 -1008
  73. package/dist/zx-zeyos.esm.min.js +53 -5
  74. package/dist/zx.css +2861 -827
  75. package/dist/zx.esm.js +8909 -1480
  76. package/dist/zx.esm.min.js +55 -7
  77. package/dist/zx.global.js +8911 -1482
  78. package/dist/zx.global.min.js +55 -7
  79. package/dist/zx.min.css +1 -1
  80. package/docs/api.json +1079 -117
  81. package/docs/llms.md +956 -154
  82. package/docs/llms.txt +9 -5
  83. package/package.json +1 -1
  84. package/src/components/account-menu/account-menu.css +35 -10
  85. package/src/components/account-menu/account-menu.js +12 -0
  86. package/src/components/activity-item/activity-item.css +1 -1
  87. package/src/components/activity-item/activity-item.js +3 -0
  88. package/src/components/activity-list/activity-list.css +38 -3
  89. package/src/components/activity-list/activity-list.js +61 -1
  90. package/src/components/app-icon/app-icon.css +14 -3
  91. package/src/components/app-icon/app-icon.js +11 -1
  92. package/src/components/app-rail/app-rail.css +59 -79
  93. package/src/components/app-sidebar/app-sidebar.css +304 -168
  94. package/src/components/app-sidebar/app-sidebar.js +667 -46
  95. package/src/components/aurora/aurora.css +5 -3
  96. package/src/components/avatar/avatar.css +32 -7
  97. package/src/components/avatar/avatar.js +72 -8
  98. package/src/components/badge/badge.css +54 -2
  99. package/src/components/badge/badge.js +29 -2
  100. package/src/components/breadcrumb/breadcrumb.css +1 -1
  101. package/src/components/button/button.css +67 -9
  102. package/src/components/button/button.js +38 -0
  103. package/src/components/calendar/calendar.css +47 -47
  104. package/src/components/calendar/calendar.js +26 -52
  105. package/src/components/card/card.css +4 -4
  106. package/src/components/card-view/card-view.css +14 -12
  107. package/src/components/card-view/card-view.js +13 -9
  108. package/src/components/card-view/record-card.js +14 -3
  109. package/src/components/chart/chart.css +4 -1
  110. package/src/components/chart/chart.js +12 -0
  111. package/src/components/check-button/check-button.css +10 -2
  112. package/src/components/checklist/checklist.css +9 -2
  113. package/src/components/checklist/checklist.js +11 -5
  114. package/src/components/checklist/field-adapter.js +3 -1
  115. package/src/components/code-editor/code-editor.css +7 -6
  116. package/src/components/code-editor/code-editor.js +19 -1
  117. package/src/components/code-editor/field-adapter.js +9 -2
  118. package/src/components/context-menu/context-menu.css +9 -4
  119. package/src/components/context-menu/context-menu.js +8 -2
  120. package/src/components/copy/copy-button.css +6 -0
  121. package/src/components/copy/copy-input.css +9 -4
  122. package/src/components/data-filter/data-filter.css +5 -4
  123. package/src/components/data-filter/data-filter.js +2 -2
  124. package/src/components/date-picker/date-picker.css +6 -6
  125. package/src/components/date-picker/date-range-picker.css +3 -3
  126. package/src/components/date-picker/time-picker.css +3 -3
  127. package/src/components/datebox/date-range-box.css +79 -2
  128. package/src/components/datebox/date-range-box.js +13 -2
  129. package/src/components/datebox/datebox.css +79 -2
  130. package/src/components/datebox/datebox.js +13 -2
  131. package/src/components/dialog/dialog.css +26 -0
  132. package/src/components/dialog/dialog.js +99 -17
  133. package/src/components/dock/dock.css +5 -4
  134. package/src/components/dropdown/dropdown.css +1 -1
  135. package/src/components/dropdown/dropdown.js +159 -72
  136. package/src/components/empty-state/empty-state.css +2 -2
  137. package/src/components/entity-ref/entity-ref.css +1 -1
  138. package/src/components/entity-ref/entity-ref.js +1 -1
  139. package/src/components/field/field.css +28 -9
  140. package/src/components/field/field.js +101 -29
  141. package/src/components/field-upload/field-upload.css +1 -1
  142. package/src/components/fieldset/external-adapter.js +363 -0
  143. package/src/components/fieldset/fieldset.css +5 -3
  144. package/src/components/fieldset/fieldset.js +38 -64
  145. package/src/components/file-item/file-item.js +144 -10
  146. package/src/components/file-list/file-list.js +144 -21
  147. package/src/components/filter/filter.css +86 -2
  148. package/src/components/filter/filter.js +307 -11
  149. package/src/components/filter-panel/filter-panel.css +1 -1
  150. package/src/components/filter-panel/filter-panel.js +149 -32
  151. package/src/components/finder/finder.css +16 -14
  152. package/src/components/form/form.css +5 -4
  153. package/src/components/form/form.js +11 -2
  154. package/src/components/grid/grid.css +1 -1
  155. package/src/components/groupbox/groupbox.css +1 -1
  156. package/src/components/kanban-view/kanban-card.js +11 -3
  157. package/src/components/kanban-view/kanban-view.css +42 -18
  158. package/src/components/kanban-view/kanban-view.js +9 -6
  159. package/src/components/launcher/launcher.css +108 -26
  160. package/src/components/launcher/launcher.js +530 -42
  161. package/src/components/list-toolbar/list-toolbar.css +13 -8
  162. package/src/components/list-toolbar/list-toolbar.js +40 -14
  163. package/src/components/master-panel/master-panel.css +2 -0
  164. package/src/components/master-panel/master-panel.js +0 -1
  165. package/src/components/menu-button/menu-button.css +72 -4
  166. package/src/components/menu-button/menu-button.js +160 -18
  167. package/src/components/message/message.css +33 -3
  168. package/src/components/message/message.js +264 -22
  169. package/src/components/modal/modal.css +1 -1
  170. package/src/components/modal/modal.js +83 -15
  171. package/src/components/module-bar/module-bar.css +486 -0
  172. package/src/components/module-bar/module-bar.js +1074 -0
  173. package/src/components/multi-value-editor/multi-value-editor.css +1 -1
  174. package/src/components/navigation-bar/navigation-bar.css +18 -9
  175. package/src/components/number-field/field-adapter.js +3 -1
  176. package/src/components/number-field/number-field.css +70 -0
  177. package/src/components/number-field/number-field.js +2 -0
  178. package/src/components/pagination/pagination.css +13 -11
  179. package/src/components/pagination/pagination.js +20 -4
  180. package/src/components/panel/panel.css +4 -3
  181. package/src/components/questionnaire/questionnaire.css +4 -4
  182. package/src/components/rating/field-adapter.js +2 -1
  183. package/src/components/rating/rating.css +8 -1
  184. package/src/components/search/search.css +4 -1
  185. package/src/components/select/color.js +22 -0
  186. package/src/components/select/field-adapter.js +2 -0
  187. package/src/components/select/glyphs.js +165 -0
  188. package/src/components/select/select.css +138 -45
  189. package/src/components/select/select.js +92 -47
  190. package/src/components/sheet/sheet.css +12 -9
  191. package/src/components/sheet/sheet.js +74 -32
  192. package/src/components/slider/field-adapter.js +6 -1
  193. package/src/components/slider/slider.css +18 -4
  194. package/src/components/slider/slider.js +1 -1
  195. package/src/components/sort-control/sort-control.css +4 -1
  196. package/src/components/split-view/split-view.css +1 -1
  197. package/src/components/stat-tile/stat-tile.css +42 -47
  198. package/src/components/stat-tile/stat-tile.js +17 -6
  199. package/src/components/stepper/stepper.css +1 -1
  200. package/src/components/tabbox/tabbox.css +41 -32
  201. package/src/components/tabbox/tabbox.js +5 -1
  202. package/src/components/table/table.css +187 -37
  203. package/src/components/table/table.js +718 -73
  204. package/src/components/tag-picker/field-adapter.js +2 -1
  205. package/src/components/tag-picker/tag-picker.css +83 -5
  206. package/src/components/tag-picker/tag-picker.js +25 -8
  207. package/src/components/timebox/timebox.css +22 -2
  208. package/src/components/toggle/toggle.css +12 -1
  209. package/src/components/toolbar/toolbar.js +2 -2
  210. package/src/components/tooltip/tooltip.css +4 -0
  211. package/src/components/tooltip/tooltip.js +44 -19
  212. package/src/components/tree/tree.css +26 -17
  213. package/src/components/tree/tree.js +31 -0
  214. package/src/components/value-list/value-list.css +3 -3
  215. package/src/components/view/record-view.css +1 -1
  216. package/src/components/view/record-view.js +178 -18
  217. package/src/components/view-settings/view-settings.css +285 -0
  218. package/src/components/view-settings/view-settings.js +1830 -0
  219. package/src/components/workspace-switcher/workspace-switcher.css +188 -0
  220. package/src/components/workspace-switcher/workspace-switcher.js +463 -0
  221. package/src/core/breakpoint.js +1 -1
  222. package/src/core/component.js +88 -17
  223. package/src/core/fontawesome.js +5 -1
  224. package/src/core/i18n.js +47 -4
  225. package/src/core/icons.js +204 -61
  226. package/src/core/keyboard.js +206 -11
  227. package/src/core/messages.js +427 -0
  228. package/src/core/motion.js +147 -0
  229. package/src/core/overlay-host.js +77 -3
  230. package/src/core/overlay-stack.js +487 -0
  231. package/src/core/position.js +22 -3
  232. package/src/index.js +10 -5
  233. package/src/internal/app-rail.js +135 -55
  234. package/src/internal/menu-items.js +61 -3
  235. package/src/zeyos/connect.js +6 -2
  236. package/src/zeyos/form.js +69 -27
  237. package/src/zeyos/index.js +4 -1
  238. package/src/zeyos/launcher.js +8 -3
  239. package/src/zeyos/module-bar.js +181 -0
  240. package/src/zeyos/query.js +197 -13
  241. package/src/zeyos/schema.js +144 -27
  242. package/src/zeyos/table.js +287 -84
  243. package/src/zeyos/view.js +49 -93
  244. package/styles/base.css +72 -1
  245. package/styles/icons.css +11 -1
  246. package/styles/print.css +1 -1
  247. package/styles/tokens/dark.css +117 -37
  248. package/styles/tokens/global.css +121 -27
  249. package/styles/tokens/semantic.css +140 -43
  250. package/styles/tokens/themes.css +7 -6
  251. package/styles/zx.css +3 -1
  252. package/styles/tokens/density.css +0 -9
@@ -161,6 +161,11 @@ export class RecordView extends Component {
161
161
  this._viewSelectionAnchor = null;
162
162
  this._viewFieldControls = null;
163
163
  this._viewFieldList = null;
164
+ this._viewFieldStatus = null;
165
+ this._viewFieldHome = null;
166
+ this._viewNativeMounted = null;
167
+ /** @type {{id: string, action: 'toggle'|'up'|'down'}|null} */
168
+ this._viewFieldFocus = null;
164
169
  this._viewFieldSettings = normalizeViewFieldControls(this.options.fieldControls);
165
170
  // Resolved once, here, so the mount point cannot drift between render and destroy.
166
171
  this._viewFieldControlsTarget = resolveElement(this._viewFieldSettings.target);
@@ -286,6 +291,57 @@ export class RecordView extends Component {
286
291
  return [...this._viewFieldOrder];
287
292
  }
288
293
 
294
+ /**
295
+ * Returns the element that lets a reader show, hide, and reorder this view's fields: the shared
296
+ * field chooser, or — for a `TableView` with `fieldControls: true` — the column chooser of its
297
+ * composed `Table`. Null when the view renders none (`fieldControls: false`).
298
+ *
299
+ * The element stays owned by the view: move it with {@link RecordView#mountFieldControls}
300
+ * rather than appending it elsewhere yourself, so `destroy()` can still find and remove it.
301
+ * @returns {HTMLElement|null}
302
+ */
303
+ getFieldControls() {
304
+ if (this._viewFieldControls) return this._viewFieldControls;
305
+ return this._nativeFieldControls();
306
+ }
307
+
308
+ /**
309
+ * Moves the view's field chooser into a host element — a list toolbar, a pane header — or, with
310
+ * `null`, back to where the view first rendered it. It keeps driving this view wherever it
311
+ * sits, and `destroy()` removes it from the host. `ListToolbar.bind()` calls this for every
312
+ * record view, which is how its field chooser ends up in the bar.
313
+ * @param {Element|string|null} target Host element or selector; null returns the chooser home.
314
+ * @returns {HTMLElement|null} The chooser, or null when the view renders none or the target
315
+ * does not resolve.
316
+ */
317
+ mountFieldControls(target) {
318
+ const controls = this.getFieldControls();
319
+ if (!controls) return null;
320
+ if (target === null || target === undefined) {
321
+ const home = this._viewFieldHome;
322
+ if (!home) return controls;
323
+ this._viewFieldHome = null;
324
+ if (controls === this._viewNativeMounted) this._viewNativeMounted = null;
325
+ if (home.parent) home.parent.insertBefore(controls, home.next?.parentNode === home.parent ? home.next : null);
326
+ else controls.remove();
327
+ if (home.mounted === undefined) delete controls.dataset.mounted;
328
+ else controls.dataset.mounted = home.mounted;
329
+ return controls;
330
+ }
331
+ const host = resolveElement(target);
332
+ if (!host) return null;
333
+ if (controls.parentNode === host) return controls;
334
+ if (!this._viewFieldHome) {
335
+ this._viewFieldHome = {
336
+ parent: controls.parentNode, next: controls.nextSibling, mounted: controls.dataset.mounted
337
+ };
338
+ }
339
+ if (controls !== this._viewFieldControls) this._viewNativeMounted = controls;
340
+ controls.dataset.mounted = 'external';
341
+ host.append(controls);
342
+ return controls;
343
+ }
344
+
289
345
  /**
290
346
  * Shows or hides one field. The final visible field cannot be hidden.
291
347
  * @param {string} id Field id.
@@ -347,7 +403,7 @@ export class RecordView extends Component {
347
403
  if (arraysEqual(next, this._viewFieldOrder)) return this;
348
404
  this._viewFieldOrder = next;
349
405
  this._refreshView('fields');
350
- this._renderViewFieldControls();
406
+ this._renderViewFieldControls(this._viewFieldFocus ?? null);
351
407
  if (!options.silent) {
352
408
  this.emit('fieldorderchange', { order: this.getFieldOrder() });
353
409
  this._emitViewState('fieldorder');
@@ -499,12 +555,30 @@ export class RecordView extends Component {
499
555
  _createViewFieldControls(label = null) {
500
556
  const text = label ?? this._viewFieldControlsLabel();
501
557
  const list = h('div', { class: 'zx-record-view__field-list' });
502
- const details = h('details', { class: 'zx-record-view__field-controls' },
503
- h('summary', { class: 'zx-button zx-button--sm' }, icon('fields', { size: 13 }), text),
504
- list);
558
+ // Present from the start and only ever re-worded: a live region inserted together with its
559
+ // text is frequently missed. Clipped inline because it has no visual role at all.
560
+ const status = h('div', {
561
+ class: 'zx-record-view__field-status', role: 'status', ariaLive: 'polite',
562
+ style: {
563
+ position: 'absolute', inlineSize: '1px', blockSize: '1px', overflow: 'hidden',
564
+ clipPath: 'inset(50%)', whiteSpace: 'nowrap'
565
+ }
566
+ });
567
+ const summary = h('summary', { class: 'zx-btn', dataset: { size: 'sm', kind: 'ghost' } }, icon('fields'), text);
568
+ const details = /** @type {HTMLDetailsElement} */ (h('details', { class: 'zx-record-view__field-controls' }, summary, list, status));
505
569
  this._viewFieldControls = details;
506
570
  this._viewFieldList = list;
571
+ this._viewFieldStatus = status;
507
572
  this._renderViewFieldControls();
573
+ this.listen(details, 'keydown', (event) => {
574
+ const key = /** @type {KeyboardEvent} */ (event);
575
+ if (key.key !== 'Escape' || !details.open) return;
576
+ // Handled here so an enclosing sheet or dialog does not close along with the disclosure.
577
+ key.preventDefault();
578
+ key.stopPropagation();
579
+ details.open = false;
580
+ summary.focus();
581
+ });
508
582
  this.listen(list, 'change', (event) => {
509
583
  const input = /** @type {HTMLInputElement|null} */ (event.target);
510
584
  if (input?.classList.contains('zx-record-view__field-toggle')) {
@@ -516,11 +590,7 @@ export class RecordView extends Component {
516
590
  if (!button) return;
517
591
  const id = /** @type {HTMLElement} */ (button).dataset.fieldId;
518
592
  const action = /** @type {HTMLElement} */ (button).dataset.viewFieldAction;
519
- const index = this._viewFieldOrder.indexOf(String(id));
520
- if (action === 'up' && index > 0) this.moveField(String(id), this._viewFieldOrder[index - 1], 'before');
521
- if (action === 'down' && index >= 0 && index < this._viewFieldOrder.length - 1) {
522
- this.moveField(String(id), this._viewFieldOrder[index + 1], 'after');
523
- }
593
+ this._moveViewFieldFromChooser(String(id), action === 'up' ? -1 : 1);
524
594
  });
525
595
  return details;
526
596
  }
@@ -549,11 +619,68 @@ export class RecordView extends Component {
549
619
  */
550
620
  _removeViewFieldControls() {
551
621
  this._viewFieldControls?.remove?.();
622
+ // A native chooser moved out by mountFieldControls() would outlive its owner in the host.
623
+ if (this._viewNativeMounted && !this.el?.contains(this._viewNativeMounted)) {
624
+ this._viewNativeMounted.remove();
625
+ }
626
+ this._viewNativeMounted = null;
627
+ this._viewFieldHome = null;
552
628
  this._viewFieldControls = null;
553
629
  this._viewFieldList = null;
630
+ this._viewFieldStatus = null;
554
631
  this._viewFieldControlsTarget = null;
555
632
  }
556
633
 
634
+ /**
635
+ * A field chooser the concrete view renders natively instead of the shared one. The base class
636
+ * recognises a composed `Table` exposed through `getTable()` — `TableView` with
637
+ * `fieldControls: true` keeps Table's own column chooser — and any other subclass may override
638
+ * this to publish its own.
639
+ * @protected
640
+ * @returns {HTMLElement|null}
641
+ */
642
+ _nativeFieldControls() {
643
+ if (!this._viewFieldSettings?.enabled) return null;
644
+ const self = /** @type {{getTable?: () => unknown}} */ (/** @type {unknown} */ (this));
645
+ const table = typeof self.getTable === 'function' ? /** @type {{el?: Element|null}|null} */ (self.getTable()) : null;
646
+ const root = table?.el;
647
+ if (!root || typeof root.querySelector !== 'function') return null;
648
+ const own = /** @type {HTMLElement|null} */ (root.querySelector('.zx-table__column-controls'));
649
+ return own ?? this._viewNativeMounted;
650
+ }
651
+
652
+ /**
653
+ * Moves a field one place through the chooser's buttons, keeps focus on that field's control
654
+ * across the re-render, and announces where it landed.
655
+ * @private
656
+ * @param {string} id Field id.
657
+ * @param {-1|1} delta Direction.
658
+ * @returns {void}
659
+ */
660
+ _moveViewFieldFromChooser(id, delta) {
661
+ const order = this._viewFieldOrder;
662
+ const index = order.indexOf(id);
663
+ if (index < 0) return;
664
+ const neighbour = order[index + delta];
665
+ const action = /** @type {'up'|'down'} */ (delta < 0 ? 'up' : 'down');
666
+ if (neighbour === undefined) {
667
+ this._renderViewFieldControls({ id, action });
668
+ return;
669
+ }
670
+ this._viewFieldFocus = { id, action };
671
+ try {
672
+ this.moveField(id, neighbour, delta < 0 ? 'before' : 'after');
673
+ } finally {
674
+ this._viewFieldFocus = null;
675
+ }
676
+ const field = this._viewFields.find((candidate) => candidate.id === id);
677
+ const position = this._viewFieldOrder.indexOf(id) + 1;
678
+ if (this._viewFieldStatus && field) {
679
+ this._viewFieldStatus.textContent = this._message('recordView.fieldMoved',
680
+ '%1 moved to position %2 of %3', field.label, position, this._viewFieldOrder.length);
681
+ }
682
+ }
683
+
557
684
  /**
558
685
  * Resolves the field-chooser trigger label: the configured one, else the translated message.
559
686
  * @protected
@@ -629,8 +756,15 @@ export class RecordView extends Component {
629
756
  this.emit('statechange', { state: this.getViewState(), reason });
630
757
  }
631
758
 
632
- /** @private @param {string|null} [focusId=null] @returns {void} */
633
- _renderViewFieldControls(focusId = null) {
759
+ /**
760
+ * Re-renders the chooser list. A focus request names the field and the control that should
761
+ * hold focus afterwards — a bare id means its checkbox — and falls back to the field's other
762
+ * enabled control when the requested one became disabled (a move button at either end).
763
+ * @private
764
+ * @param {string|{id:string,action:'toggle'|'up'|'down'}|null} [focus=null] Control to focus.
765
+ * @returns {void}
766
+ */
767
+ _renderViewFieldControls(focus = null) {
634
768
  if (!this._viewFieldList) return;
635
769
  const fields = this.getFields();
636
770
  const visible = fields.filter((field) => !this._viewHidden.has(field.id));
@@ -641,7 +775,8 @@ export class RecordView extends Component {
641
775
  type: 'checkbox',
642
776
  value: field.id,
643
777
  checked: !this._viewHidden.has(field.id),
644
- disabled: !this._viewHidden.has(field.id) && visible.length === 1
778
+ disabled: !this._viewHidden.has(field.id) && visible.length === 1,
779
+ dataset: { viewFieldAction: 'toggle', fieldId: field.id }
645
780
  });
646
781
  const up = h('button', {
647
782
  class: 'zx-record-view__field-move',
@@ -662,14 +797,39 @@ export class RecordView extends Component {
662
797
  h('span', { class: 'zx-record-view__field-order' }, up, down));
663
798
  });
664
799
  this._viewFieldList.replaceChildren(...controls);
665
- if (focusId != null) {
666
- const match = [...this._viewFieldList.querySelectorAll('.zx-record-view__field-toggle')]
667
- .find((control) => /** @type {HTMLInputElement} */ (control).value === focusId);
668
- queueMicrotask(() => {
669
- if (match?.isConnected) /** @type {HTMLElement} */ (match).focus();
670
- });
800
+ if (focus == null) return;
801
+ const request = typeof focus === 'string' ? { id: focus, action: 'toggle' } : focus;
802
+ const match = chooserFocusTarget(controls, request);
803
+ if (!match) return;
804
+ // Synchronously, so focus never passes through <body> between the removal and the restore.
805
+ match.focus();
806
+ queueMicrotask(() => {
807
+ if (match.isConnected && match.ownerDocument?.activeElement !== match) match.focus();
808
+ });
809
+ }
810
+ }
811
+
812
+ /**
813
+ * Picks the chooser control that should hold focus after a re-render: the requested control of
814
+ * the field, else that field's other enabled move button, else any enabled control of the field.
815
+ * @param {HTMLElement[]} options Rendered `.zx-record-view__field-option` rows.
816
+ * @param {{id:string,action:string}} request Field id and control kind.
817
+ * @returns {HTMLElement|null}
818
+ */
819
+ function chooserFocusTarget(options, request) {
820
+ /** @type {HTMLElement[]} */
821
+ const controls = [];
822
+ for (const option of options) {
823
+ for (const control of option.querySelectorAll?.('[data-view-field-action]') ?? []) {
824
+ if (/** @type {HTMLElement} */ (control).dataset.fieldId === request.id) controls.push(/** @type {HTMLElement} */ (control));
671
825
  }
672
826
  }
827
+ const enabled = (control) => !(/** @type {HTMLButtonElement} */ (control).disabled);
828
+ const exact = controls.find((control) => control.dataset.viewFieldAction === request.action);
829
+ if (exact && enabled(exact)) return exact;
830
+ const move = controls.find((control) => control.dataset.viewFieldAction !== 'toggle' && enabled(control));
831
+ if (request.action !== 'toggle' && move) return move;
832
+ return controls.find(enabled) ?? null;
673
833
  }
674
834
 
675
835
  /**
@@ -0,0 +1,285 @@
1
+ /*
2
+ * The content of a "View settings" panel: the shown fields in order, the hidden ones to bring
3
+ * back, grouping, and the board's axes. It fills whatever the host gives it — a sheet body, a
4
+ * dropdown, a settings page — and draws no frame of its own.
5
+ */
6
+ .zx-view-settings {
7
+ /* A size container, so the board settings stack when the panel is narrow rather than the window.
8
+ Size containment ignores the contents' width, so the width is declared: the host's, with a
9
+ floor for a host such as a dropdown that sizes itself to its contents. */
10
+ container-type: inline-size;
11
+ inline-size: 100%;
12
+ min-inline-size: 15rem;
13
+ display: grid;
14
+ gap: var(--zx-space-5);
15
+ color: var(--zx-color-text);
16
+ font-size: var(--zx-control-font-size);
17
+ }
18
+
19
+ .zx-view-settings__section {
20
+ display: grid;
21
+ gap: var(--zx-space-2);
22
+ min-inline-size: 0;
23
+ }
24
+
25
+ .zx-view-settings__heading {
26
+ margin: 0;
27
+ color: var(--zx-color-text);
28
+ font-size: var(--zx-text-md);
29
+ font-weight: var(--zx-weight-semibold);
30
+ line-height: 1.3;
31
+ }
32
+
33
+ .zx-view-settings__subheading {
34
+ margin: var(--zx-space-1) 0 0;
35
+ color: var(--zx-color-text-muted);
36
+ font-size: var(--zx-text-sm);
37
+ font-weight: var(--zx-weight-medium);
38
+ line-height: 1.3;
39
+ }
40
+
41
+ .zx-view-settings__search-slot:empty {
42
+ display: none;
43
+ }
44
+
45
+ .zx-view-settings__search {
46
+ inline-size: 100%;
47
+ }
48
+
49
+ /* The shown list and its insertion line share a positioned frame; the line is not a list item. */
50
+ .zx-view-settings__frame {
51
+ position: relative;
52
+ }
53
+
54
+ .zx-view-settings__list,
55
+ .zx-view-settings__hidden {
56
+ display: grid;
57
+ gap: calc(var(--zx-space-1) / 2);
58
+ margin: 0;
59
+ padding: 0;
60
+ list-style: none;
61
+ }
62
+
63
+ /* A long hidden list scrolls on its own, so the shown fields above never leave the screen. */
64
+ .zx-view-settings__hidden {
65
+ max-block-size: calc(var(--zx-control-height) * 8);
66
+ overflow-y: auto;
67
+ overscroll-behavior: contain;
68
+ }
69
+
70
+ .zx-view-settings__hidden[hidden] {
71
+ display: none;
72
+ }
73
+
74
+ /*
75
+ * One row: handle, type glyph, label, action. The type glyph follows the label in the source, so
76
+ * a screen reader hears "Owner, Text", and is placed before it here, where the eye expects it.
77
+ */
78
+ .zx-view-settings__row {
79
+ position: relative;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: var(--zx-space-2);
83
+ min-block-size: var(--zx-control-height);
84
+ padding-inline: var(--zx-space-1);
85
+ border-radius: var(--zx-control-radius);
86
+ background: var(--zx-color-bg-surface);
87
+ }
88
+
89
+ .zx-view-settings__row:hover {
90
+ background: var(--zx-color-bg-hover);
91
+ }
92
+
93
+ .zx-view-settings__handle {
94
+ order: -2;
95
+ flex: none;
96
+ }
97
+
98
+ .zx-view-settings__type {
99
+ order: -1;
100
+ display: inline-flex;
101
+ flex: none;
102
+ align-items: center;
103
+ justify-content: center;
104
+ inline-size: 1.25rem;
105
+ color: var(--zx-color-text-subtle);
106
+ }
107
+
108
+ .zx-view-settings__label {
109
+ flex: 1 1 auto;
110
+ min-inline-size: 0;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ white-space: nowrap;
114
+ }
115
+
116
+ /* Handle and hide buttons: flat until hovered or focused, square at the control height. */
117
+ .zx-view-settings__handle,
118
+ .zx-view-settings__hide {
119
+ display: inline-grid;
120
+ place-items: center;
121
+ inline-size: var(--zx-control-height);
122
+ block-size: var(--zx-control-height);
123
+ min-block-size: 0;
124
+ padding: 0;
125
+ border: 1px solid transparent;
126
+ border-radius: var(--zx-control-radius);
127
+ background: none;
128
+ color: var(--zx-color-text-subtle);
129
+ }
130
+
131
+ .zx-view-settings__handle[data-placeholder] {
132
+ visibility: hidden;
133
+ }
134
+
135
+ button.zx-view-settings__handle {
136
+ cursor: grab;
137
+ touch-action: none;
138
+ -webkit-user-select: none;
139
+ user-select: none;
140
+ }
141
+
142
+ .zx-view-settings__handle:hover,
143
+ .zx-view-settings__hide:hover:not(:disabled) {
144
+ background: var(--zx-color-bg-hover);
145
+ color: var(--zx-color-text);
146
+ }
147
+
148
+ .zx-view-settings__handle:focus-visible,
149
+ .zx-view-settings__hide:focus-visible {
150
+ box-shadow: var(--zx-focus-ring);
151
+ }
152
+
153
+ .zx-view-settings__hide:disabled {
154
+ color: var(--zx-color-text-placeholder);
155
+ cursor: default;
156
+ }
157
+
158
+ .zx-view-settings__lock {
159
+ display: inline-grid;
160
+ flex: none;
161
+ place-items: center;
162
+ inline-size: var(--zx-control-height);
163
+ color: var(--zx-color-text-placeholder);
164
+ }
165
+
166
+ .zx-view-settings__add {
167
+ flex: none;
168
+ }
169
+
170
+ /* Picked up from the keyboard: marked by the border as well as the fill, never by colour alone. */
171
+ .zx-view-settings__row[data-state="grabbed"] {
172
+ outline: 2px solid var(--zx-color-ring);
173
+ outline-offset: -2px;
174
+ background: var(--zx-color-bg-selected);
175
+ }
176
+
177
+ .zx-view-settings[data-dragging="true"],
178
+ .zx-view-settings[data-dragging="true"] .zx-view-settings__handle {
179
+ cursor: grabbing;
180
+ }
181
+
182
+ .zx-view-settings__row[data-dragging="true"] {
183
+ z-index: 1;
184
+ background: var(--zx-color-bg-raised);
185
+ box-shadow: var(--zx-overlay-shadow);
186
+ }
187
+
188
+ .zx-view-settings__drop-indicator {
189
+ position: absolute;
190
+ inset-inline: 0;
191
+ inset-block-start: 0;
192
+ z-index: 2;
193
+ block-size: 2px;
194
+ margin-block-start: -1px;
195
+ border-radius: var(--zx-radius-full);
196
+ background: var(--zx-color-accent);
197
+ pointer-events: none;
198
+ }
199
+
200
+ .zx-view-settings__drop-indicator[hidden] {
201
+ display: none;
202
+ }
203
+
204
+ .zx-view-settings__empty {
205
+ margin: 0;
206
+ padding-block: var(--zx-space-1);
207
+ color: var(--zx-color-text-muted);
208
+ font-size: var(--zx-text-sm);
209
+ }
210
+
211
+ .zx-view-settings__empty[hidden] {
212
+ display: none;
213
+ }
214
+
215
+ .zx-view-settings__option {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ gap: var(--zx-space-2);
219
+ min-inline-size: 0;
220
+ }
221
+
222
+ .zx-view-settings__option > .zx-icon,
223
+ .zx-view-settings .zx-select__value-adornment > .zx-icon {
224
+ flex: none;
225
+ color: var(--zx-color-text-subtle);
226
+ }
227
+
228
+ .zx-view-settings__group {
229
+ inline-size: 100%;
230
+ }
231
+
232
+ /* Board settings: label beside its select where there is room, above it where there is not. */
233
+ .zx-view-settings__setting {
234
+ display: grid;
235
+ grid-template-columns: minmax(6rem, auto) minmax(0, 1fr);
236
+ align-items: center;
237
+ gap: var(--zx-space-2);
238
+ }
239
+
240
+ .zx-view-settings__setting-label {
241
+ color: var(--zx-color-text-muted);
242
+ font-size: var(--zx-text-sm);
243
+ }
244
+
245
+ @container (max-width: 18rem) {
246
+ .zx-view-settings__setting {
247
+ grid-template-columns: minmax(0, 1fr);
248
+ gap: var(--zx-space-1);
249
+ }
250
+ }
251
+
252
+ .zx-view-settings__toggle {
253
+ justify-self: start;
254
+ }
255
+
256
+ .zx-view-settings__instructions[hidden] {
257
+ display: none;
258
+ }
259
+
260
+ .zx-view-settings__sr,
261
+ .zx-view-settings__status {
262
+ position: absolute;
263
+ inline-size: 1px;
264
+ block-size: 1px;
265
+ margin: -1px;
266
+ padding: 0;
267
+ overflow: hidden;
268
+ clip-path: inset(50%);
269
+ white-space: nowrap;
270
+ border: 0;
271
+ }
272
+
273
+ @media (prefers-reduced-motion: no-preference) {
274
+ .zx-view-settings__row,
275
+ .zx-view-settings__handle,
276
+ .zx-view-settings__hide {
277
+ transition: background var(--zx-dur-fast) var(--zx-ease),
278
+ color var(--zx-dur-fast) var(--zx-ease),
279
+ box-shadow var(--zx-dur-fast) var(--zx-ease);
280
+ }
281
+
282
+ .zx-view-settings__drop-indicator {
283
+ transition: inset-block-start var(--zx-dur-fast) var(--zx-ease);
284
+ }
285
+ }