@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
@@ -26,6 +26,8 @@
26
26
  margin: 0;
27
27
  flex: 1;
28
28
  font-size: var(--zx-text-xl);
29
+ font-weight: 650;
30
+ letter-spacing: var(--zx-tracking-title);
29
31
  }
30
32
 
31
33
  .zx-master-panel__buttons {
@@ -11,15 +11,23 @@
11
11
  }
12
12
 
13
13
  .zx-menu-button__item {
14
- display: flex;
14
+ display: grid;
15
+ grid-template-columns: auto minmax(0, 1fr) auto;
15
16
  align-items: center;
16
17
  gap: var(--zx-space-2);
17
18
  inline-size: 100%;
18
- min-block-size: var(--zx-control-height);
19
- border-color: transparent;
19
+ /* A menu row sits a little under a control so a list of them reads lighter than a column of
20
+ fields; the radius is the smallest step, inset inside the panel's own. */
21
+ min-block-size: calc(var(--zx-control-height) - 2px);
22
+ border: 0;
23
+ border-radius: var(--zx-radius-sm);
20
24
  background: transparent;
21
- padding-inline: var(--zx-control-pad-x);
25
+ padding: var(--zx-space-1) var(--zx-space-2);
26
+ color: inherit;
27
+ font: inherit;
28
+ line-height: 1.35;
22
29
  text-align: start;
30
+ text-decoration: none;
23
31
  }
24
32
 
25
33
  .zx-menu-button__item:hover,
@@ -37,11 +45,85 @@
37
45
  opacity: .65;
38
46
  }
39
47
 
40
- .zx-menu-button__icon {
48
+ .zx-menu-button__leading,
49
+ .zx-menu-button__meta {
41
50
  display: inline-flex;
51
+ align-items: center;
52
+ gap: var(--zx-space-2);
42
53
  flex: none;
43
54
  }
44
55
 
56
+ .zx-menu-button__meta {
57
+ justify-content: flex-end;
58
+ }
59
+
60
+ .zx-menu-button__leading {
61
+ justify-content: center;
62
+ inline-size: var(--zx-space-4);
63
+ }
64
+
65
+ .zx-menu-button__body {
66
+ display: flex;
67
+ min-inline-size: 0;
68
+ flex-direction: column;
69
+ }
70
+
71
+ .zx-menu-button__label,
72
+ .zx-menu-button__description {
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ white-space: nowrap;
76
+ }
77
+
78
+ .zx-menu-button__description,
79
+ .zx-menu-button__shortcut,
80
+ .zx-menu-button__adornment {
81
+ color: var(--zx-color-text-muted);
82
+ font-size: var(--zx-text-xs);
83
+ }
84
+
85
+ .zx-menu-button__badge {
86
+ min-inline-size: var(--zx-space-4);
87
+ border-radius: var(--zx-radius-full);
88
+ background: var(--zx-color-bg-muted);
89
+ padding-inline: var(--zx-space-1);
90
+ color: var(--zx-color-text-muted);
91
+ font-size: var(--zx-text-xs);
92
+ text-align: center;
93
+ }
94
+
95
+ .zx-menu-button__shortcut {
96
+ border: 0;
97
+ background: transparent;
98
+ padding: 0;
99
+ font-family: var(--zx-font-mono);
100
+ }
101
+
102
+ .zx-menu-button__check {
103
+ inline-size: var(--zx-space-3);
104
+ block-size: var(--zx-space-3);
105
+ border: 1px solid var(--zx-color-border-strong);
106
+ border-radius: var(--zx-radius-sm);
107
+ }
108
+
109
+ .zx-menu-button__check[data-check-kind="radio"] {
110
+ border-radius: var(--zx-radius-full);
111
+ }
112
+
113
+ .zx-menu-button__item[aria-checked="true"] .zx-menu-button__check {
114
+ border-color: var(--zx-color-accent);
115
+ background: var(--zx-color-accent);
116
+ box-shadow: inset 0 0 0 2px var(--zx-color-bg-raised);
117
+ }
118
+
119
+ /* A section label is a caption, never taller than the rows it introduces. */
120
+ .zx-menu-button__heading {
121
+ padding: var(--zx-space-1) var(--zx-space-2);
122
+ color: var(--zx-color-text-muted);
123
+ font-size: var(--zx-text-xs);
124
+ font-weight: 600;
125
+ }
126
+
45
127
  .zx-menu-button__separator {
46
128
  block-size: 1px;
47
129
  margin: var(--zx-space-1) var(--zx-space-2);
@@ -1,21 +1,17 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h, resolveElement } from '../../core/dom.js';
3
+ import { uid } from '../../core/util.js';
3
4
  import { icon } from '../../core/icons.js';
4
5
  import { rovingTabindex, typeahead } from '../../core/keyboard.js';
6
+ import {
7
+ isMenuItem,
8
+ MENU_ITEM_SELECTOR,
9
+ renderMenuEntries
10
+ } from '../../internal/menu-items.js';
5
11
  import { Dropdown } from '../dropdown/dropdown.js';
6
12
 
7
- const MENU_ITEM_SELECTOR = '[role="menuitem"]';
8
-
9
- /**
10
- * @typedef {Object} MenuItem
11
- * @property {string} label Item label.
12
- * @property {string} [icon] Kernel icon name.
13
- * @property {unknown} [value] Value emitted on selection.
14
- * @property {boolean} [disabled=false] Whether selection is disabled.
15
- * @property {boolean} [danger=false] Whether the action is dangerous.
16
- * @property {(value: unknown, item: MenuItem, menuButton: MenuButton) => void} [onselect] Selection callback.
17
- */
18
- /** @typedef {MenuItem|'-'} MenuButtonItem */
13
+ /** @typedef {import('../../internal/menu-items.js').MenuItem} MenuItem */
14
+ /** @typedef {import('../../internal/menu-items.js').MenuEntry} MenuButtonItem */
19
15
  /**
20
16
  * @typedef {Object} MenuButtonOptions
21
17
  * @property {string} [label=''] Trigger label.
@@ -28,8 +24,19 @@ const MENU_ITEM_SELECTOR = '[role="menuitem"]';
28
24
  * @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close event listener.
29
25
  */
30
26
 
27
+ /**
28
+ * @typedef {Object} MenuButtonOpenOptions
29
+ * @property {boolean|HTMLElement} [returnFocus=true] Where focus goes when this opening ends with a
30
+ * selection or Escape: `true` is the trigger, an element is that element (the trigger again if
31
+ * it has left the document), and `false` leaves focus alone.
32
+ */
33
+
31
34
  /**
32
35
  * APG menu-button trigger backed by an anchored Dropdown.
36
+ *
37
+ * Choosing an item closes the menu and puts focus back on the trigger before the `select` event
38
+ * and the item's `onselect` run, so whatever they open — a dialog, a sheet — records the trigger
39
+ * as the place to return to, and focus they move themselves stays where they put it.
33
40
  * @fires MenuButton#select
34
41
  * @fires MenuButton#open
35
42
  * @fires MenuButton#close
@@ -59,6 +66,8 @@ export class MenuButton extends Component {
59
66
  #items = [];
60
67
  /** @type {{attributes: Array<[string, string]>, children: Node[]}|null} */
61
68
  #original = null;
69
+ /** @type {boolean|HTMLElement} Focus destination for the current opening. */
70
+ #returnFocus = true;
62
71
  #created = false;
63
72
  #destroyed = false;
64
73
 
@@ -94,6 +103,10 @@ export class MenuButton extends Component {
94
103
  this.#panel = this.#dropdown.getPanel();
95
104
  this.#panel.classList.add('zx-menu-button__menu');
96
105
  this.#panel.setAttribute('role', 'menu');
106
+ // The menu is named by its trigger, as the APG menu-button pattern has it. An id given here to
107
+ // an enhanced trigger goes again on destroy(), with the rest of the attributes it gained.
108
+ if (!this.el.id) this.el.id = uid('zx-menu-button');
109
+ this.#panel.setAttribute('aria-labelledby', this.el.id);
97
110
  this.el.setAttribute('aria-haspopup', 'menu');
98
111
  if (existing && hasExplicitLabel) this.setLabel(this.options.label);
99
112
 
@@ -105,7 +118,10 @@ export class MenuButton extends Component {
105
118
  );
106
119
 
107
120
  this.#dropdown.on('open', () => this.emit('open'));
108
- this.#dropdown.on('close', () => this.emit('close'));
121
+ this.#dropdown.on('close', () => {
122
+ this.#returnFocus = true;
123
+ this.emit('close');
124
+ });
109
125
  this.listen(this.el, 'click', () => {
110
126
  if (this.isOpen()) this.close();
111
127
  else {
@@ -116,7 +132,7 @@ export class MenuButton extends Component {
116
132
  this.listen(this.el, 'keydown', (event) => this.#onTriggerKeydown(event));
117
133
  this.listen(this.#panel, 'click', (event) => {
118
134
  const item = event.target.closest?.(MENU_ITEM_SELECTOR);
119
- if (item && this.#panel.contains(item)) this.#selectElement(item);
135
+ if (item && this.#panel.contains(item)) this.#selectElement(item, event);
120
136
  });
121
137
  this.listen(this.#panel, 'keydown', (event) => this.#onMenuKeydown(event));
122
138
  }
@@ -142,33 +158,18 @@ export class MenuButton extends Component {
142
158
  */
143
159
  setItems(items) {
144
160
  this.#items = Array.isArray(items) ? items.slice() : [];
145
- this.#panel.replaceChildren();
146
- this.#items.forEach((item, index) => {
147
- if (item === '-') {
148
- this.#panel.append(h('div', { class: 'zx-menu-button__separator', role: 'separator' }));
149
- return;
150
- }
151
- const children = [];
152
- if (item.icon) children.push(h('span', { class: 'zx-menu-button__icon' }, icon(item.icon)));
153
- children.push(h('span', { class: 'zx-menu-button__label' }, item.label));
154
- this.#panel.append(h('button', {
155
- class: 'zx-menu-button__item',
156
- type: 'button',
157
- role: 'menuitem',
158
- tabindex: '-1',
159
- 'data-menu-item': String(index),
160
- 'data-danger': item.danger ? 'true' : null,
161
- ariaDisabled: item.disabled ? 'true' : null
162
- }, children));
163
- });
161
+ this.#panel.replaceChildren(...renderMenuEntries(this.#items, 'menu-button'));
164
162
  return this;
165
163
  }
166
164
 
167
165
  /**
168
166
  * Opens the menu.
167
+ * @param {MenuButtonOpenOptions} [options={}] Where focus returns when this opening ends; ignored
168
+ * while the menu is already open.
169
169
  * @returns {this}
170
170
  */
171
- open() {
171
+ open(options = {}) {
172
+ if (!this.isOpen()) this.#returnFocus = options?.returnFocus ?? true;
172
173
  this.#dropdown.open();
173
174
  return this;
174
175
  }
@@ -200,16 +201,24 @@ export class MenuButton extends Component {
200
201
  return this.#panel;
201
202
  }
202
203
 
203
- /** Opens the menu and focuses its first enabled item. @returns {this} */
204
- focusFirst() {
205
- this.open();
204
+ /**
205
+ * Opens the menu and focuses its first enabled item.
206
+ * @param {MenuButtonOpenOptions} [options={}] Passed to `open()`.
207
+ * @returns {this}
208
+ */
209
+ focusFirst(options = {}) {
210
+ this.open(options);
206
211
  this.#roving.focusFirst();
207
212
  return this;
208
213
  }
209
214
 
210
- /** Opens the menu and focuses its last enabled item. @returns {this} */
211
- focusLast() {
212
- this.open();
215
+ /**
216
+ * Opens the menu and focuses its last enabled item.
217
+ * @param {MenuButtonOpenOptions} [options={}] Passed to `open()`.
218
+ * @returns {this}
219
+ */
220
+ focusLast(options = {}) {
221
+ this.open(options);
213
222
  this.#roving.focusLast();
214
223
  return this;
215
224
  }
@@ -254,8 +263,9 @@ export class MenuButton extends Component {
254
263
  #onMenuKeydown(event) {
255
264
  if (event.key === 'Escape') {
256
265
  event.preventDefault();
266
+ const target = this.#returnTarget();
257
267
  this.close();
258
- this.el.focus();
268
+ target?.focus();
259
269
  return;
260
270
  }
261
271
  if (event.key === 'Tab') {
@@ -265,20 +275,39 @@ export class MenuButton extends Component {
265
275
  if (event.key === 'Enter' || event.key === ' ') {
266
276
  const item = event.target.closest?.(MENU_ITEM_SELECTOR);
267
277
  if (!item || !this.#panel.contains(item)) return;
278
+ if (event.key === 'Enter' && item.tagName === 'A') return;
268
279
  event.preventDefault();
269
- this.#selectElement(item);
280
+ /** @type {HTMLElement} */ (item).click();
270
281
  return;
271
282
  }
272
283
  this.#typeahead(event);
273
284
  }
274
285
 
275
- /** @param {Element} element @returns {void} */
276
- #selectElement(element) {
286
+ /** @param {Element} element @param {Event|null} [event=null] @returns {void} */
287
+ #selectElement(element, event = null) {
277
288
  const index = Number(element.getAttribute('data-menu-item'));
278
289
  const item = this.#items[index];
279
- if (!item || item === '-' || item.disabled) return;
290
+ if (!isMenuItem(item) || item.disabled) {
291
+ event?.preventDefault();
292
+ return;
293
+ }
294
+ /*
295
+ * Focus goes back first, as the APG pattern has it, and only if it is still the menu's to give:
296
+ * inside the panel, or already lost to the body. A dialog opened by a listener below then
297
+ * records the trigger as its opener rather than an item that is about to be hidden, and focus
298
+ * a listener moves on purpose is left where it was put.
299
+ */
300
+ const target = this.#returnTarget();
301
+ const active = document.activeElement;
302
+ const reclaim = Boolean(target) && (!active || active === document.body || this.#panel.contains(active));
303
+ if (reclaim) target?.focus();
280
304
  const selected = this.emit('select', { value: item.value, item });
281
- if (selected.defaultPrevented) return;
305
+ if (selected.defaultPrevented) {
306
+ event?.preventDefault();
307
+ // The menu stays open, so focus returns to the item, unless a listener has moved it again.
308
+ if (reclaim && document.activeElement === target && element instanceof HTMLElement) element.focus();
309
+ return;
310
+ }
282
311
  try {
283
312
  item.onselect?.(item.value, item, this);
284
313
  } finally {
@@ -286,6 +315,17 @@ export class MenuButton extends Component {
286
315
  }
287
316
  }
288
317
 
318
+ /**
319
+ * The element focus returns to when the current opening ends, or null when it should stay put.
320
+ * @returns {HTMLElement|null}
321
+ */
322
+ #returnTarget() {
323
+ const target = this.#returnFocus;
324
+ if (target === false) return null;
325
+ if (target instanceof HTMLElement && target.isConnected) return target;
326
+ return /** @type {HTMLElement} */ (this.el);
327
+ }
328
+
289
329
  /** @returns {HTMLElement[]} */
290
330
  #menuItems() {
291
331
  return Array.from(this.#panel.querySelectorAll(MENU_ITEM_SELECTOR));
@@ -25,12 +25,30 @@
25
25
  align-content: start;
26
26
  }
27
27
 
28
+ /* On a phone the toasts rise from the foot of the screen, full width, where they cover content
29
+ rather than the header's controls, and sit where a thumb can reach their action. */
30
+ @media (max-width: 640px) {
31
+ .zx-message-region {
32
+ inset-block-start: auto;
33
+ inset-block-end: var(--zx-space-4);
34
+ inset-inline: var(--zx-space-4);
35
+ inline-size: auto;
36
+ }
37
+
38
+ .zx-message-region:popover-open {
39
+ align-content: end;
40
+ }
41
+ }
42
+
28
43
  .zx-message__toast {
29
44
  --zx-message-kind: var(--zx-color-info);
30
45
  --zx-message-fallback-bg: var(--zx-color-info-bg);
31
46
  position: relative;
32
47
  display: grid;
33
- grid-template-columns: auto minmax(0, 1fr) auto;
48
+ /* Icon and text, then whatever follows — the action, the close button — in columns of their own. */
49
+ grid-template-columns: auto minmax(0, 1fr);
50
+ grid-auto-columns: auto;
51
+ grid-auto-flow: column;
34
52
  align-items: start;
35
53
  gap: var(--zx-space-3);
36
54
  border: 1px solid var(--zx-color-overlay-border);
@@ -64,8 +82,8 @@
64
82
  background-clip: padding-box;
65
83
  -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
66
84
  backdrop-filter: var(--zx-glass-filter-overlay);
67
- box-shadow: inset 3px 0 0 color-mix(in srgb, var(--zx-message-kind) 72%,
68
- var(--zx-color-glass-highlight-strong)), var(--zx-glass-overlay-shadow);
85
+ /* No bar on the leading edge: the kind shows in the icon and the tinted border. */
86
+ box-shadow: var(--zx-glass-overlay-shadow);
69
87
  }
70
88
  }
71
89
 
@@ -90,10 +108,25 @@
90
108
  overflow-wrap: anywhere;
91
109
  }
92
110
 
111
+ /* A leading visual — an avatar, say — keeps its own size, centred on the first line of text. */
112
+ .zx-message__toast[data-leading] > .zx-message__icon {
113
+ margin-block: calc(-0.5 * var(--zx-space-1)) 0;
114
+ }
115
+
116
+ /* The action sits level with the close button: both are pulled up the same step so their centres
117
+ meet the first line of text rather than hanging below it. */
118
+ .zx-message__action {
119
+ margin-block: calc(-1 * var(--zx-space-1)) 0;
120
+ white-space: nowrap;
121
+ }
122
+
93
123
  /* Visual chrome comes from the shared .zx-icon-btn utility; only alignment is set here so
94
- the close control sits flush with the top-right corner without stretching the toast. */
124
+ the close control sits flush with the trailing top corner without stretching the toast.
125
+ Trailing, not right: in an RTL document the close control belongs on the left, and a
126
+ four-value `margin` shorthand is the one place a logical property cannot be spelled. */
95
127
  .zx-message__close {
96
- margin: calc(-1 * var(--zx-space-1)) calc(-1 * var(--zx-space-1)) 0 0;
128
+ margin-block: calc(-1 * var(--zx-space-1)) 0;
129
+ margin-inline: 0 calc(-1 * var(--zx-space-1));
97
130
  }
98
131
 
99
132
  .zx-message__progress {