@zeyos/zx 4.3.0 → 4.4.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 (136) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/README.md +7 -3
  3. package/dist/types/components/activity-item/activity-item.d.ts +351 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +195 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +28 -0
  6. package/dist/types/components/button/button.d.ts +8 -0
  7. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  8. package/dist/types/components/calendar/calendar.d.ts +19 -0
  9. package/dist/types/components/card/card.d.ts +7 -0
  10. package/dist/types/components/card-view/card-view.d.ts +12 -4
  11. package/dist/types/components/card-view/record-card.d.ts +53 -5
  12. package/dist/types/components/code-editor/code-editor.d.ts +160 -0
  13. package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
  14. package/dist/types/components/context-menu/context-menu.d.ts +18 -2
  15. package/dist/types/components/date-picker/date-picker.d.ts +8 -0
  16. package/dist/types/components/date-picker/time-picker.d.ts +7 -0
  17. package/dist/types/components/dialog/dialog.d.ts +29 -0
  18. package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
  19. package/dist/types/components/file-item/file-item.d.ts +234 -0
  20. package/dist/types/components/file-list/file-list.d.ts +171 -0
  21. package/dist/types/components/filter/filter.d.ts +91 -36
  22. package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
  23. package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
  24. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  25. package/dist/types/components/kanban-view/kanban-view.d.ts +837 -26
  26. package/dist/types/components/launcher/launcher.d.ts +19 -12
  27. package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
  28. package/dist/types/components/menu-button/menu-button.d.ts +4 -37
  29. package/dist/types/components/message/message.d.ts +7 -0
  30. package/dist/types/components/modal/modal.d.ts +48 -0
  31. package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
  32. package/dist/types/components/panel/panel.d.ts +7 -0
  33. package/dist/types/components/search/search.d.ts +7 -0
  34. package/dist/types/components/select/select.d.ts +4 -0
  35. package/dist/types/components/sort-control/sort-control.d.ts +105 -0
  36. package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
  37. package/dist/types/components/tabbox/tabbox.d.ts +7 -0
  38. package/dist/types/components/table/table.d.ts +144 -0
  39. package/dist/types/components/table-view/table-view.d.ts +11 -5
  40. package/dist/types/components/timebox/timebox.d.ts +7 -0
  41. package/dist/types/components/value-list/value-list.d.ts +8 -0
  42. package/dist/types/components/view/record-view.d.ts +96 -5
  43. package/dist/types/core/component.d.ts +6 -1
  44. package/dist/types/core/icons.d.ts +17 -0
  45. package/dist/types/index.d.ts +13 -1
  46. package/dist/types/internal/app-rail.d.ts +10 -0
  47. package/dist/types/internal/menu-items.d.ts +134 -0
  48. package/dist/zx-zeyos.esm.js +651 -88
  49. package/dist/zx-zeyos.esm.min.js +8 -1
  50. package/dist/zx.css +6266 -3900
  51. package/dist/zx.esm.js +8487 -1805
  52. package/dist/zx.esm.min.js +10 -3
  53. package/dist/zx.global.js +8487 -1805
  54. package/dist/zx.global.min.js +10 -3
  55. package/dist/zx.min.css +1 -1
  56. package/docs/api.json +1811 -247
  57. package/docs/llms.md +345 -40
  58. package/docs/llms.txt +11 -7
  59. package/package.json +1 -2
  60. package/src/components/activity-item/activity-item.css +137 -0
  61. package/src/components/activity-item/activity-item.js +599 -0
  62. package/src/components/activity-list/activity-list.css +64 -0
  63. package/src/components/activity-list/activity-list.js +375 -0
  64. package/src/components/app-rail/app-rail.css +227 -19
  65. package/src/components/app-sidebar/app-sidebar.css +414 -24
  66. package/src/components/app-sidebar/app-sidebar.js +65 -5
  67. package/src/components/button/button.css +8 -0
  68. package/src/components/button/button.js +6 -1
  69. package/src/components/calendar/calendar-model.js +45 -0
  70. package/src/components/calendar/calendar.css +30 -10
  71. package/src/components/calendar/calendar.js +41 -13
  72. package/src/components/card/card.js +12 -1
  73. package/src/components/card-view/card-view.css +212 -9
  74. package/src/components/card-view/card-view.js +32 -16
  75. package/src/components/card-view/record-card.js +98 -21
  76. package/src/components/code-editor/code-editor.css +67 -0
  77. package/src/components/code-editor/code-editor.js +288 -0
  78. package/src/components/code-editor/field-adapter.js +34 -0
  79. package/src/components/context-menu/context-menu.css +80 -3
  80. package/src/components/context-menu/context-menu.js +22 -30
  81. package/src/components/date-picker/date-picker.js +22 -8
  82. package/src/components/date-picker/month-picker.js +3 -3
  83. package/src/components/date-picker/time-picker.js +20 -8
  84. package/src/components/dialog/dialog.js +88 -66
  85. package/src/components/dropdown/dropdown.js +2 -2
  86. package/src/components/entity-ref/entity-ref.css +182 -0
  87. package/src/components/entity-ref/entity-ref.js +496 -0
  88. package/src/components/file-item/file-item.css +88 -0
  89. package/src/components/file-item/file-item.js +371 -0
  90. package/src/components/file-list/file-list.css +51 -0
  91. package/src/components/file-list/file-list.js +320 -0
  92. package/src/components/filter/filter.css +27 -1
  93. package/src/components/filter/filter.js +199 -45
  94. package/src/components/filter-panel/filter-panel.css +104 -0
  95. package/src/components/filter-panel/filter-panel.js +964 -0
  96. package/src/components/kanban-view/kanban-card.js +239 -0
  97. package/src/components/kanban-view/kanban-policy.js +412 -0
  98. package/src/components/kanban-view/kanban-view.css +674 -36
  99. package/src/components/kanban-view/kanban-view.js +1762 -240
  100. package/src/components/launcher/launcher.js +59 -27
  101. package/src/components/list-toolbar/list-toolbar.css +189 -0
  102. package/src/components/list-toolbar/list-toolbar.js +851 -0
  103. package/src/components/menu-button/menu-button.css +80 -3
  104. package/src/components/menu-button/menu-button.js +21 -37
  105. package/src/components/message/message.css +14 -12
  106. package/src/components/message/message.js +13 -2
  107. package/src/components/modal/modal.css +15 -0
  108. package/src/components/modal/modal.js +141 -0
  109. package/src/components/navigation-bar/navigation-bar.css +77 -2
  110. package/src/components/navigation-bar/navigation-bar.js +171 -4
  111. package/src/components/panel/panel.js +13 -2
  112. package/src/components/search/search.js +17 -5
  113. package/src/components/select/field-adapter.js +4 -1
  114. package/src/components/select/select.js +3 -0
  115. package/src/components/sort-control/sort-control.css +17 -0
  116. package/src/components/sort-control/sort-control.js +140 -0
  117. package/src/components/stat-tile/stat-tile.css +213 -0
  118. package/src/components/stat-tile/stat-tile.js +590 -0
  119. package/src/components/tabbox/tabbox.js +12 -1
  120. package/src/components/table/table.css +51 -2
  121. package/src/components/table/table.js +298 -19
  122. package/src/components/table-view/table-view.css +8 -2
  123. package/src/components/table-view/table-view.js +21 -6
  124. package/src/components/tag-picker/tag-picker.css +3 -1
  125. package/src/components/timebox/timebox.js +18 -5
  126. package/src/components/value-list/value-list.js +22 -6
  127. package/src/components/view/record-view.css +5 -0
  128. package/src/components/view/record-view.js +100 -7
  129. package/src/core/component.js +8 -3
  130. package/src/core/icons.js +74 -5
  131. package/src/index.js +33 -3
  132. package/src/internal/app-rail.js +19 -1
  133. package/src/internal/menu-items.js +142 -0
  134. package/styles/print.css +192 -0
  135. package/styles/zx.css +11 -0
  136. package/DESIGN-SYSTEM.md +0 -182
@@ -1,11 +1,15 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h, resolveElement } from '../../core/dom.js';
3
- import { icon } from '../../core/icons.js';
4
3
  import { rovingTabindex, typeahead } from '../../core/keyboard.js';
4
+ import {
5
+ isMenuItem,
6
+ MENU_ITEM_SELECTOR,
7
+ renderMenuEntries
8
+ } from '../../internal/menu-items.js';
5
9
  import { Dropdown } from '../dropdown/dropdown.js';
6
10
 
7
- /** @typedef {import('../menu-button/menu-button.js').MenuItem} MenuItem */
8
- /** @typedef {MenuItem|'-'} ContextMenuEntry */
11
+ /** @typedef {import('../../internal/menu-items.js').MenuItem} MenuItem */
12
+ /** @typedef {import('../../internal/menu-items.js').MenuEntry} ContextMenuEntry */
9
13
 
10
14
  /**
11
15
  * @typedef {Object} ContextMenuOptions
@@ -22,8 +26,6 @@ import { Dropdown } from '../dropdown/dropdown.js';
22
26
  * @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close listener.
23
27
  */
24
28
 
25
- const MENU_ITEM_SELECTOR = '[role="menuitem"]';
26
-
27
29
  /**
28
30
  * Right-click menu for a region, implementing the APG menu pattern.
29
31
  *
@@ -107,7 +109,7 @@ export class ContextMenu extends Component {
107
109
  this.listen(this.#target, 'keydown', (event) => this.#onTargetKeydown(event));
108
110
  this.listen(this.#panel, 'click', (event) => {
109
111
  const item = event.target.closest?.(MENU_ITEM_SELECTOR);
110
- if (item && this.#panel.contains(item)) this.#select(item);
112
+ if (item && this.#panel.contains(item)) this.#select(item, event);
111
113
  });
112
114
  this.listen(this.#panel, 'keydown', (event) => this.#onMenuKeydown(event));
113
115
  }
@@ -127,25 +129,7 @@ export class ContextMenu extends Component {
127
129
  */
128
130
  setItems(items) {
129
131
  this.#items = Array.isArray(items) ? items.slice() : [];
130
- this.#panel.replaceChildren();
131
- this.#items.forEach((item, index) => {
132
- if (item === '-') {
133
- this.#panel.append(h('div', { class: 'zx-context-menu__separator', role: 'separator' }));
134
- return;
135
- }
136
- const children = [];
137
- if (item.icon) children.push(h('span', { class: 'zx-context-menu__icon' }, icon(item.icon)));
138
- children.push(h('span', { class: 'zx-context-menu__label' }, String(item.label ?? '')));
139
- this.#panel.append(h('button', {
140
- class: 'zx-context-menu__item',
141
- type: 'button',
142
- role: 'menuitem',
143
- tabindex: '-1',
144
- 'data-menu-item': String(index),
145
- 'data-danger': item.danger ? 'true' : null,
146
- ariaDisabled: item.disabled ? 'true' : null
147
- }, children));
148
- });
132
+ this.#panel.replaceChildren(...renderMenuEntries(this.#items, 'context-menu'));
149
133
  return this;
150
134
  }
151
135
 
@@ -292,20 +276,28 @@ export class ContextMenu extends Component {
292
276
  if (key.key === 'Enter' || key.key === ' ') {
293
277
  const item = /** @type {Element} */ (key.target).closest?.(MENU_ITEM_SELECTOR);
294
278
  if (!item || !this.#panel.contains(item)) return;
279
+ if (key.key === 'Enter' && item.tagName === 'A') return;
295
280
  event.preventDefault();
296
- this.#select(item);
281
+ /** @type {HTMLElement} */ (item).click();
297
282
  return;
298
283
  }
299
284
  this.#typeahead(key);
300
285
  }
301
286
 
302
- /** @param {Element} element @returns {void} */
303
- #select(element) {
287
+ /** @param {Element} element @param {Event|null} [event=null] @returns {void} */
288
+ #select(element, event = null) {
304
289
  const index = Number(element.getAttribute('data-menu-item'));
305
290
  const item = this.#items[index];
306
- if (!item || item === '-' || item.disabled) return;
291
+ if (!isMenuItem(item) || item.disabled) {
292
+ event?.preventDefault();
293
+ return;
294
+ }
307
295
  const context = this.#context;
308
- this.emit('select', { value: item.value, item, context });
296
+ const selected = this.emit('select', { value: item.value, item, context });
297
+ if (selected.defaultPrevented) {
298
+ event?.preventDefault();
299
+ return;
300
+ }
309
301
  try {
310
302
  item.onselect?.(item.value, item, this);
311
303
  } finally {
@@ -2,6 +2,7 @@ import { Component } from '../../core/component.js';
2
2
  import { addDays, addMonths, formatDate, getWeekStart, isSameDay, parseDate } from '../../core/date.js';
3
3
  import { h } from '../../core/dom.js';
4
4
  import { icon } from '../../core/icons.js';
5
+ import { printf } from '../../core/i18n.js';
5
6
  import { TimePicker } from './time-picker.js';
6
7
 
7
8
  /**
@@ -55,7 +56,7 @@ export class DatePicker extends Component {
55
56
  ref: 'previous',
56
57
  class: 'zx-date-picker__nav',
57
58
  type: 'button',
58
- ariaLabel: 'Previous month'
59
+ ariaLabel: this._message('datePicker.previousMonth', 'Previous month')
59
60
  }, icon('chevron-left')),
60
61
  h('button', {
61
62
  ref: 'heading',
@@ -68,7 +69,7 @@ export class DatePicker extends Component {
68
69
  ref: 'next',
69
70
  class: 'zx-date-picker__nav',
70
71
  type: 'button',
71
- ariaLabel: 'Next month'
72
+ ariaLabel: this._message('datePicker.nextMonth', 'Next month')
72
73
  }, icon('chevron-right'))
73
74
  ),
74
75
  h('div', {
@@ -81,14 +82,14 @@ export class DatePicker extends Component {
81
82
  ref: 'quickPrevious',
82
83
  class: 'zx-date-picker__nav',
83
84
  type: 'button',
84
- ariaLabel: 'Previous year'
85
+ ariaLabel: this._message('datePicker.previousYear', 'Previous year')
85
86
  }, icon('chevron-left')),
86
87
  h('span', { ref: 'quickYear', class: 'zx-date-picker__quick-year', ariaLive: 'polite' }),
87
88
  h('button', {
88
89
  ref: 'quickNext',
89
90
  class: 'zx-date-picker__nav',
90
91
  type: 'button',
91
- ariaLabel: 'Next year'
92
+ ariaLabel: this._message('datePicker.nextYear', 'Next year')
92
93
  }, icon('chevron-right'))
93
94
  ),
94
95
  h('div', { ref: 'quickGrid', class: 'zx-date-picker__quick-grid', role: 'grid' })),
@@ -96,7 +97,7 @@ export class DatePicker extends Component {
96
97
  ref: 'grid',
97
98
  class: 'zx-date-picker__grid',
98
99
  role: 'grid',
99
- ariaLabel: 'Calendar'
100
+ ariaLabel: this._message('datePicker.calendar', 'Calendar')
100
101
  },
101
102
  h('thead', { ref: 'weekdays' }),
102
103
  h('tbody', { ref: 'days' }))
@@ -203,7 +204,7 @@ export class DatePicker extends Component {
203
204
  row.append(h('th', {
204
205
  class: 'zx-date-picker__week-heading',
205
206
  scope: 'col',
206
- ariaLabel: 'Week number'
207
+ ariaLabel: this._message('datePicker.weekNumber', 'Week number')
207
208
  }, '#'));
208
209
  }
209
210
  const sunday = new Date(2023, 0, 1);
@@ -224,7 +225,8 @@ export class DatePicker extends Component {
224
225
  const offset = (first.getDay() - this._weekStart + 7) % 7;
225
226
  const start = addDays(first, -offset);
226
227
  this.refs.heading.textContent = formatDate(first, '%B %Y');
227
- this.refs.heading.setAttribute('aria-label', `Choose month and year, ${formatDate(first, '%B %Y')}`);
228
+ this.refs.heading.setAttribute('aria-label',
229
+ this._message('datePicker.chooseMonthYear', 'Choose month and year, %1', formatDate(first, '%B %Y')));
228
230
  const rows = [];
229
231
 
230
232
  for (let week = 0; week < 6; week += 1) {
@@ -234,7 +236,7 @@ export class DatePicker extends Component {
234
236
  row.append(h('th', {
235
237
  class: 'zx-date-picker__week',
236
238
  scope: 'row',
237
- ariaLabel: `Week ${isoWeekNumber(rowStart)}`
239
+ ariaLabel: this._message('datePicker.week', 'Week %1', isoWeekNumber(rowStart))
238
240
  }, String(isoWeekNumber(rowStart))));
239
241
  }
240
242
  for (let column = 0; column < 7; column += 1) {
@@ -450,6 +452,18 @@ export class DatePicker extends Component {
450
452
  _buttonForDate(date) {
451
453
  return this.refs.days.querySelector(`[data-date="${formatDate(date, '%Y-%m-%d')}"]`);
452
454
  }
455
+
456
+ /**
457
+ * Resolves a message through the host translator, falling back to the built-in English text.
458
+ * @param {string} key Message key.
459
+ * @param {string} fallback Built-in text, with `%1`-style placeholders.
460
+ * @param {...unknown} args Interpolation values.
461
+ * @returns {string}
462
+ */
463
+ _message(key, fallback, ...args) {
464
+ const message = this.msg(key, ...args);
465
+ return message === key ? printf(fallback, args) : message;
466
+ }
453
467
  }
454
468
 
455
469
  /**
@@ -47,21 +47,21 @@ export class MonthPicker extends DatePicker {
47
47
  ref: 'previousYear',
48
48
  class: 'zx-month-picker__nav',
49
49
  type: 'button',
50
- ariaLabel: 'Previous year'
50
+ ariaLabel: this._message('monthPicker.previousYear', 'Previous year')
51
51
  }, icon('chevron-left')),
52
52
  h('span', { ref: 'year', class: 'zx-month-picker__year', ariaLive: 'polite' }),
53
53
  h('button', {
54
54
  ref: 'nextYear',
55
55
  class: 'zx-month-picker__nav',
56
56
  type: 'button',
57
- ariaLabel: 'Next year'
57
+ ariaLabel: this._message('monthPicker.nextYear', 'Next year')
58
58
  }, icon('chevron-right'))
59
59
  ),
60
60
  h('div', {
61
61
  ref: 'grid',
62
62
  class: 'zx-month-picker__grid',
63
63
  role: 'grid',
64
- ariaLabel: 'Choose month'
64
+ ariaLabel: this._message('monthPicker.chooseMonth', 'Choose month')
65
65
  })
66
66
  );
67
67
  this._content = content;
@@ -48,15 +48,15 @@ export class TimePicker extends Component {
48
48
  const content = h('div', {
49
49
  class: 'zx-time-picker__segments',
50
50
  role: 'group',
51
- ariaLabel: 'Time'
51
+ ariaLabel: this._message('timePicker.label', 'Time')
52
52
  });
53
53
  this._content = content;
54
- this._addSpin(content, 'h', 'Hour', 23);
54
+ this._addSpin(content, 'h', this._message('timePicker.hour', 'Hour'), 23);
55
55
  content.append(h('span', { class: 'zx-time-picker__separator', ariaHidden: 'true' }, ':'));
56
- this._addSpin(content, 'm', 'Minute', 59);
56
+ this._addSpin(content, 'm', this._message('timePicker.minute', 'Minute'), 59);
57
57
  if (this.options.seconds) {
58
58
  content.append(h('span', { class: 'zx-time-picker__separator', ariaHidden: 'true' }, ':'));
59
- this._addSpin(content, 's', 'Second', 59);
59
+ this._addSpin(content, 's', this._message('timePicker.second', 'Second'), 59);
60
60
  }
61
61
  root.append(content);
62
62
 
@@ -66,7 +66,7 @@ export class TimePicker extends Component {
66
66
  ref: 'clockToggle',
67
67
  class: 'zx-icon-btn zx-time-picker__clock-toggle',
68
68
  type: 'button',
69
- ariaLabel: 'Pick the time on a clock',
69
+ ariaLabel: this._message('timePicker.clock', 'Pick the time on a clock'),
70
70
  ariaExpanded: 'false',
71
71
  ariaControls: clockId
72
72
  }, icon('clock', { size: 15 })));
@@ -223,7 +223,7 @@ export class TimePicker extends Component {
223
223
  ref: 'headHour',
224
224
  class: 'zx-time-picker__clock-unit',
225
225
  type: 'button',
226
- ariaLabel: 'Select the hour',
226
+ ariaLabel: this._message('timePicker.selectHour', 'Select the hour'),
227
227
  dataset: { unit: 'h' }
228
228
  }),
229
229
  h('span', { class: 'zx-time-picker__clock-colon', ariaHidden: 'true' }, ':'),
@@ -231,7 +231,7 @@ export class TimePicker extends Component {
231
231
  ref: 'headMinute',
232
232
  class: 'zx-time-picker__clock-unit',
233
233
  type: 'button',
234
- ariaLabel: 'Select the minutes',
234
+ ariaLabel: this._message('timePicker.selectMinutes', 'Select the minutes'),
235
235
  dataset: { unit: 'm' }
236
236
  })),
237
237
  h('div', { ref: 'dial', class: 'zx-time-picker__dial', role: 'radiogroup' },
@@ -290,7 +290,8 @@ export class TimePicker extends Component {
290
290
  this.refs.headMinute.textContent = pad(this._display.m);
291
291
  this.refs.headHour.setAttribute('aria-pressed', String(hours));
292
292
  this.refs.headMinute.setAttribute('aria-pressed', String(!hours));
293
- this.refs.dial.setAttribute('aria-label', hours ? 'Hour' : 'Minutes');
293
+ this.refs.dial.setAttribute('aria-label', hours
294
+ ? this._message('timePicker.hour', 'Hour') : this._message('timePicker.minutes', 'Minutes'));
294
295
  if (!this._clockOpen) return;
295
296
 
296
297
  const selected = hours ? this._display.h : this._display.m;
@@ -330,6 +331,17 @@ export class TimePicker extends Component {
330
331
  element.setAttribute('aria-checked', String(checked));
331
332
  element.tabIndex = checked ? 0 : -1;
332
333
  }
334
+
335
+ /**
336
+ * Resolves a message through the host translator, falling back to the built-in English text.
337
+ * @param {string} key Message key.
338
+ * @param {string} fallback English default.
339
+ * @returns {string}
340
+ */
341
+ _message(key, fallback) {
342
+ const message = this.msg(key);
343
+ return message === key ? fallback : message;
344
+ }
333
345
  }
334
346
 
335
347
  /**
@@ -1,11 +1,7 @@
1
1
  import { h } from '../../core/dom.js';
2
2
  import { uid } from '../../core/util.js';
3
3
  import { Modal } from '../modal/modal.js';
4
-
5
- const FOCUSABLE_SELECTOR = [
6
- 'a[href]', 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])',
7
- 'textarea:not([disabled])', '[contenteditable="true"]', '[tabindex]:not([tabindex="-1"])'
8
- ].join(',');
4
+ import { button } from '../button/button.js';
9
5
 
10
6
  /** @typedef {'default'|'primary'|'danger'|'ghost'} DialogButtonKind */
11
7
  /**
@@ -14,6 +10,8 @@ const FOCUSABLE_SELECTOR = [
14
10
  * @property {DialogButtonKind} [kind='default'] Visual button kind.
15
11
  * @property {'close'|'cancel'|((dialog: Dialog) => void)} [action='close'] Button action.
16
12
  * @property {boolean} [autofocus=false] Whether this button receives initial focus.
13
+ * @property {boolean} [disabled=false] Whether the footer button is disabled.
14
+ * @property {import('../button/button.js').ButtonShortcut} [shortcut] Visible shortcut hint; the host binds the action.
17
15
  */
18
16
  /**
19
17
  * @typedef {Object} DialogOptions
@@ -86,8 +84,6 @@ export class Dialog extends Modal {
86
84
  /** @type {DialogButton[]} */
87
85
  #baseButtons = [];
88
86
  #baseTitle = '';
89
- /** @type {Element|null} */
90
- #opener = null;
91
87
 
92
88
  /**
93
89
  * Creates a structured dialog in the configured theme scope, or at document level when unscoped.
@@ -99,7 +95,7 @@ export class Dialog extends Modal {
99
95
  this._applySize(this.options.size);
100
96
  this.#baseTitle = String(this.options.title ?? '');
101
97
  this.#applyTitle(this.#baseTitle);
102
- this.#baseButtons = copyButtons(this.options.buttons);
98
+ this.#baseButtons = normalizeDialogButtons(this.options.buttons);
103
99
  this.#renderButtons(this.#baseButtons);
104
100
  this.refs.close.hidden = !this.options.closable;
105
101
 
@@ -111,7 +107,6 @@ export class Dialog extends Modal {
111
107
  if (!definition) return;
112
108
  this.#runButton(definition);
113
109
  });
114
- this.on('close', () => this.#restoreFocus());
115
110
  }
116
111
 
117
112
  /** @type {DialogButton[]} */
@@ -123,6 +118,7 @@ export class Dialog extends Modal {
123
118
  */
124
119
  render() {
125
120
  const titleId = uid('zx-dialog-title');
121
+ const close = this._message('dialog.close', 'Close');
126
122
  const dialog = /** @type {HTMLDialogElement} */ (h('dialog', {
127
123
  class: 'zx-modal zx-dialog',
128
124
  ariaLabelledby: titleId
@@ -134,8 +130,8 @@ export class Dialog extends Modal {
134
130
  ref: 'close',
135
131
  type: 'button',
136
132
  'data-kind': 'ghost',
137
- ariaLabel: 'Close',
138
- title: 'Close'
133
+ ariaLabel: close,
134
+ title: close
139
135
  }, '×')
140
136
  ),
141
137
  h('div', { class: 'zx-modal__content zx-dialog__body', ref: 'content' }),
@@ -151,16 +147,13 @@ export class Dialog extends Modal {
151
147
  * @returns {this}
152
148
  */
153
149
  open() {
154
- if (this.isOpen()) return this;
155
- this.#opener = document.activeElement instanceof Element ? document.activeElement : null;
156
- super.open();
157
- queueMicrotask(() => {
158
- if (!this.isOpen()) return;
159
- const autofocus = this.el.querySelector('[autofocus]:not([disabled])');
160
- const first = autofocus ?? this.el.querySelector(FOCUSABLE_SELECTOR);
161
- if (first instanceof HTMLElement) first.focus();
162
- });
163
- return this;
150
+ /*
151
+ * Capturing the opener, placing initial focus and restoring it afterwards are `Modal`'s, so
152
+ * that every overlay built on it is accessible rather than only the structured one. The
153
+ * override stays because the promise is the dialog's: the header close button is the first
154
+ * focusable descendant, and a footer button declared `autofocus` outranks it.
155
+ */
156
+ return super.open();
164
157
  }
165
158
 
166
159
  /**
@@ -192,7 +185,7 @@ export class Dialog extends Modal {
192
185
  * @returns {this}
193
186
  */
194
187
  setButtons(list) {
195
- this.#baseButtons = copyButtons(list);
188
+ this.#baseButtons = normalizeDialogButtons(list);
196
189
  this.#renderButtons(this.#baseButtons);
197
190
  return this;
198
191
  }
@@ -216,7 +209,7 @@ export class Dialog extends Modal {
216
209
  const view = {
217
210
  key: normalizedKey,
218
211
  content,
219
- buttons: options && Object.hasOwn(options, 'buttons') ? copyButtons(options.buttons ?? []) : null,
212
+ buttons: options && Object.hasOwn(options, 'buttons') ? normalizeDialogButtons(options.buttons ?? []) : null,
220
213
  title: options && Object.hasOwn(options, 'title') ? String(options.title ?? '') : null
221
214
  };
222
215
  this.#views.set(normalizedKey, view);
@@ -260,7 +253,7 @@ export class Dialog extends Modal {
260
253
  * @param {AlertOptions} [options={}] Alert options.
261
254
  * @returns {Promise<void>}
262
255
  */
263
- static alert({ title = '', message = '', okLabel = 'OK' } = {}) {
256
+ static alert({ title = '', message = '', okLabel } = {}) {
264
257
  return new Promise((resolve) => {
265
258
  let settled = false;
266
259
  const finish = () => {
@@ -269,11 +262,13 @@ export class Dialog extends Modal {
269
262
  dialog.close();
270
263
  resolve();
271
264
  };
272
- const dialog = new Dialog(null, {
273
- title,
274
- content: message,
275
- buttons: [{ label: okLabel, kind: 'primary', action: finish, autofocus: true }]
276
- });
265
+ const dialog = new Dialog(null, { title, content: message });
266
+ dialog.setButtons([{
267
+ label: okLabel ?? dialog._message('dialog.ok', 'OK'),
268
+ kind: 'primary',
269
+ action: finish,
270
+ autofocus: true
271
+ }]);
277
272
  dialog.once('close', () => {
278
273
  if (!settled) {
279
274
  settled = true;
@@ -291,7 +286,7 @@ export class Dialog extends Modal {
291
286
  * @returns {Promise<boolean>}
292
287
  */
293
288
  static confirm({
294
- title = '', message = '', okLabel = 'OK', cancelLabel = 'Cancel', danger = false
289
+ title = '', message = '', okLabel, cancelLabel, danger = false
295
290
  } = {}) {
296
291
  return new Promise((resolve) => {
297
292
  let settled = false;
@@ -301,14 +296,16 @@ export class Dialog extends Modal {
301
296
  dialog.close(value);
302
297
  resolve(value);
303
298
  };
304
- const dialog = new Dialog(null, {
305
- title,
306
- content: message,
307
- buttons: [
308
- { label: cancelLabel, action: () => finish(false) },
309
- { label: okLabel, kind: danger ? 'danger' : 'primary', action: () => finish(true), autofocus: true }
310
- ]
311
- });
299
+ const dialog = new Dialog(null, { title, content: message });
300
+ dialog.setButtons([
301
+ { label: cancelLabel ?? dialog._message('dialog.cancel', 'Cancel'), action: () => finish(false) },
302
+ {
303
+ label: okLabel ?? dialog._message('dialog.ok', 'OK'),
304
+ kind: danger ? 'danger' : 'primary',
305
+ action: () => finish(true),
306
+ autofocus: true
307
+ }
308
+ ]);
312
309
  dialog.once('close', () => {
313
310
  if (!settled) {
314
311
  settled = true;
@@ -332,8 +329,7 @@ export class Dialog extends Modal {
332
329
  class: 'zx-dialog__prompt-input',
333
330
  value: String(value),
334
331
  placeholder: String(placeholder),
335
- autofocus: true,
336
- ariaLabel: String(title || 'Prompt value')
332
+ autofocus: true
337
333
  });
338
334
  const content = h('div', { class: 'zx-dialog__prompt' },
339
335
  h('div', { class: 'zx-dialog__prompt-message' }, message),
@@ -345,14 +341,12 @@ export class Dialog extends Modal {
345
341
  dialog.close(result);
346
342
  resolve(result);
347
343
  };
348
- const dialog = new Dialog(null, {
349
- title,
350
- content,
351
- buttons: [
352
- { label: 'Cancel', action: () => finish(null) },
353
- { label: 'OK', kind: 'primary', action: () => finish(input.value) }
354
- ]
355
- });
344
+ const dialog = new Dialog(null, { title, content });
345
+ input.setAttribute('aria-label', String(title || dialog._message('dialog.promptValue', 'Prompt value')));
346
+ dialog.setButtons([
347
+ { label: dialog._message('dialog.cancel', 'Cancel'), action: () => finish(null) },
348
+ { label: dialog._message('dialog.ok', 'OK'), kind: 'primary', action: () => finish(input.value) }
349
+ ]);
356
350
  dialog.listen(input, 'keydown', (event) => {
357
351
  if (event.key !== 'Enter') return;
358
352
  event.preventDefault();
@@ -383,6 +377,17 @@ export class Dialog extends Modal {
383
377
  this.el.style.inlineSize = `${Math.max(0, width)}px`;
384
378
  }
385
379
 
380
+ /**
381
+ * Resolves a component message, falling back to the built-in English text.
382
+ * @param {string} key Message key.
383
+ * @param {string} fallback English default.
384
+ * @returns {string}
385
+ */
386
+ _message(key, fallback) {
387
+ const message = this.msg(key);
388
+ return message === key ? fallback : message;
389
+ }
390
+
386
391
  /** @param {string} title @returns {void} */
387
392
  #applyTitle(title) {
388
393
  this.refs.title.textContent = title;
@@ -390,23 +395,27 @@ export class Dialog extends Modal {
390
395
 
391
396
  /** @param {DialogButton[]} list @returns {void} */
392
397
  #renderButtons(list) {
393
- this.#renderedButtons = copyButtons(list);
398
+ this.#renderedButtons = normalizeDialogButtons(list);
394
399
  this.refs.footer.replaceChildren();
395
400
  this.refs.footer.hidden = this.#renderedButtons.length === 0;
396
401
  this.#renderedButtons.forEach((definition, index) => {
397
- this.refs.footer.append(h('button', {
398
- class: 'zx-btn',
399
- type: 'button',
400
- 'data-kind': definition.kind ?? 'default',
401
- 'data-size': 'md',
402
- 'data-dialog-button': String(index),
403
- autofocus: Boolean(definition.autofocus)
404
- }, h('span', { class: 'zx-btn__label' }, definition.label)));
402
+ const control = button({
403
+ label: definition.label,
404
+ kind: definition.kind,
405
+ shortcut: definition.shortcut,
406
+ disabled: definition.disabled
407
+ });
408
+ control.dataset.dialogButton = String(index);
409
+ control.autofocus = definition.autofocus;
410
+ this.refs.footer.append(control);
405
411
  });
406
412
  }
407
413
 
408
414
  /** @param {DialogButton} definition @returns {void} */
409
415
  #runButton(definition) {
416
+ // A native disabled button swallows its own clicks, but the footer listener is delegated and
417
+ // a host may dispatch one at the row; the descriptor is the authority either way.
418
+ if (definition.disabled) return;
410
419
  const action = definition.action ?? 'close';
411
420
  if (action === 'close') {
412
421
  this.close();
@@ -417,17 +426,30 @@ export class Dialog extends Modal {
417
426
  action(this);
418
427
  }
419
428
  }
420
-
421
- /** @returns {void} */
422
- #restoreFocus() {
423
- if (this.#opener?.isConnected && typeof this.#opener.focus === 'function') this.#opener.focus();
424
- this.#opener = null;
425
- }
426
429
  }
427
430
 
428
- /** @param {DialogButton[]|null|undefined} list @returns {DialogButton[]} */
429
- function copyButtons(list) {
430
- return Array.isArray(list) ? list.map((button) => ({ ...button, label: String(button.label ?? '') })) : [];
431
+ /**
432
+ * Copies footer-button descriptors without mutating the caller's array or objects, and settles
433
+ * the two booleans the footer renders from.
434
+ *
435
+ * `autofocus` and `disabled` are resolved together because they contradict each other: a control
436
+ * that refuses activation must not be the one initial focus lands on, and a host that writes both
437
+ * has said what it wants twice. Deciding it here rather than at render time keeps `Sheet` — which
438
+ * takes the same descriptors through the same footer — on one answer.
439
+ * @param {DialogButton[]|null|undefined} list Button descriptors.
440
+ * @returns {DialogButton[]} Normalized copies.
441
+ */
442
+ export function normalizeDialogButtons(list) {
443
+ if (!Array.isArray(list)) return [];
444
+ return list.map((button) => {
445
+ const disabled = Boolean(button?.disabled);
446
+ return {
447
+ ...button,
448
+ label: String(button?.label ?? ''),
449
+ disabled,
450
+ autofocus: Boolean(button?.autofocus) && !disabled
451
+ };
452
+ });
431
453
  }
432
454
 
433
455
  /**
@@ -182,7 +182,7 @@ export class Dropdown extends Component {
182
182
 
183
183
  /** @param {KeyboardEvent} event @returns {void} */
184
184
  [HANDLE_ESCAPE_DISMISS](event) {
185
- if (event.key !== 'Escape' || !this.isOpen()) return;
185
+ if (event.defaultPrevented || event.key !== 'Escape' || !this.isOpen()) return;
186
186
  event.preventDefault();
187
187
  const restoreFocus = this.el.contains(document.activeElement);
188
188
  this.close();
@@ -205,7 +205,7 @@ class DropdownDismissManager extends Component {
205
205
  if (component instanceof Dropdown) component[HANDLE_POINTER_DISMISS](event);
206
206
  });
207
207
  this.listen(document, 'keydown', (event) => {
208
- if (event.key !== 'Escape') return;
208
+ if (event.defaultPrevented || event.key !== 'Escape') return;
209
209
  const component = topmostOverlayComponent();
210
210
  if (component instanceof Dropdown) component[HANDLE_ESCAPE_DISMISS](event);
211
211
  });