@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
@@ -19,7 +19,8 @@ import { DateRangePicker, clampRange, normalizeRange } from '../date-picker/date
19
19
  * @property {string} [format='%d.%m.%Y'] Kernel date format used for both endpoints.
20
20
  * @property {string} [separator=' – '] Text placed between the two formatted dates.
21
21
  * @property {string|null} [placeholder=null] Input placeholder; null derives one from the format.
22
- * @property {boolean} [clearable=true] Show a clear button while a range is present.
22
+ * @property {boolean} [clearable=true] Show a clear button while a range is present and the box is
23
+ * enabled; without it the input keeps no room for one.
23
24
  * @property {Date|null} [min=null] Earliest selectable local calendar day.
24
25
  * @property {Date|null} [max=null] Latest selectable local calendar day.
25
26
  * @property {number} [months=2] Month panels in the popover; narrow ones show only the first.
@@ -132,6 +133,8 @@ export class DateRangeBox extends Component {
132
133
  this.el = root;
133
134
  this._originalState = root.getAttribute('data-state');
134
135
  this._originalAriaDisabled = root.getAttribute('aria-disabled');
136
+ this._originalClearable = root.getAttribute('data-clearable');
137
+ root.toggleAttribute('data-clearable', Boolean(this.options.clearable));
135
138
  this._format = String(this.options.format || '%d.%m.%Y');
136
139
  this._separator = String(this.options.separator ?? ' – ');
137
140
  this._disabled = Boolean(this.options.disabled);
@@ -336,6 +339,7 @@ export class DateRangeBox extends Component {
336
339
  this._content?.remove();
337
340
  restoreAttribute(this.el, 'data-state', this._originalState);
338
341
  restoreAttribute(this.el, 'aria-disabled', this._originalAriaDisabled);
342
+ restoreAttribute(this.el, 'data-clearable', this._originalClearable);
339
343
  super.destroy();
340
344
  }
341
345
 
@@ -435,7 +439,13 @@ export class DateRangeBox extends Component {
435
439
  /** @returns {void} */
436
440
  _syncDisplay() {
437
441
  this.refs.input.value = formatRangeText(this._range, this._format, this._separator);
438
- this.refs.clear.hidden = !this.options.clearable || !(this._range.start || this._range.end);
442
+ this._syncClear();
443
+ }
444
+
445
+ /** A disabled box offers no clear button: there is nothing it could do. @returns {void} */
446
+ _syncClear() {
447
+ this.refs.clear.hidden = !this.options.clearable || !(this._range.start || this._range.end)
448
+ || this._disabled;
439
449
  }
440
450
 
441
451
  /** @returns {void} */
@@ -444,6 +454,7 @@ export class DateRangeBox extends Component {
444
454
  this.refs.toggle.disabled = this._disabled;
445
455
  this.refs.clear.disabled = this._disabled;
446
456
  this.el.setAttribute('aria-disabled', String(this._disabled));
457
+ this._syncClear();
447
458
  if (this._disabled) this._picker?.disable();
448
459
  else this._picker?.enable();
449
460
  }
@@ -10,6 +10,12 @@
10
10
  align-items: stretch;
11
11
  }
12
12
 
13
+ /* The box draws the frame, so the input and buttons inside it are one border shorter on each side:
14
+ the control as a whole is one control height, like a text input beside it. */
15
+ .zx-datebox__control > :is(input, button) {
16
+ min-block-size: calc(var(--zx-control-height) - 2px);
17
+ }
18
+
13
19
  .zx-datebox__control {
14
20
  position: relative;
15
21
  border: 1px solid var(--zx-color-border-control);
@@ -26,16 +32,40 @@
26
32
  background: var(--zx-color-danger-bg);
27
33
  }
28
34
 
29
- /* The input always reserves room for the clear button, which floats over its trailing edge —
30
- showing or hiding the button must not change the control's total width. */
35
+ /* A clearable input always reserves room for the clear button, which floats over its trailing
36
+ edge — showing or hiding the button must not change the control's total width. Without
37
+ `clearable` there is no button, and no room is kept for one. */
31
38
  .zx-datebox__input {
32
39
  min-inline-size: 10em;
33
40
  border: 0;
34
41
  border-radius: var(--zx-control-radius) 0 0 var(--zx-control-radius);
35
42
  background: inherit;
43
+ }
44
+
45
+ .zx-datebox[data-clearable] .zx-datebox__input {
36
46
  padding-inline-end: var(--zx-control-height);
37
47
  }
38
48
 
49
+ /* In a Field the control fills its column, as a text input does, and gives way in a narrow one
50
+ instead of holding a minimum width that overflows a two-column row. */
51
+ .zx-field__control > .zx-datebox {
52
+ display: block;
53
+ }
54
+
55
+ .zx-field__control > .zx-datebox :is(.zx-datebox__content, .zx-datebox__control) {
56
+ display: flex;
57
+ }
58
+
59
+ .zx-field__control > .zx-datebox .zx-datebox__control {
60
+ flex: 1 1 auto;
61
+ min-inline-size: 0;
62
+ }
63
+
64
+ .zx-field__control > .zx-datebox .zx-datebox__input {
65
+ flex: 1 1 auto;
66
+ min-inline-size: 0;
67
+ }
68
+
39
69
  /* Focused controls lift above their siblings so the ring is never clipped. */
40
70
  .zx-datebox__input:focus-visible,
41
71
  .zx-datebox__toggle:focus-visible {
@@ -121,3 +151,50 @@
121
151
  box-shadow: var(--zx-overlay-shadow);
122
152
  }
123
153
  }
154
+
155
+ /*
156
+ * Quiet fields. Inside `[data-zx-fields="quiet"]` the date reads as text: no border, no fill, and
157
+ * the calendar button and its divider hidden. Hover brings the hover fill and the button back;
158
+ * focus brings back the border, the control fill and the focus ring. An invalid value keeps its
159
+ * danger border, quiet or not. A disabled control never lights up.
160
+ */
161
+ [data-zx-fields="quiet"] .zx-datebox:not([data-state="invalid"]) .zx-datebox__control {
162
+ border-color: transparent;
163
+ background: transparent;
164
+ }
165
+
166
+ [data-zx-fields="quiet"] .zx-datebox__toggle {
167
+ border-inline-start-color: transparent;
168
+ opacity: 0;
169
+ }
170
+
171
+ [data-zx-fields="quiet"] .zx-datebox__clear {
172
+ opacity: 0;
173
+ }
174
+
175
+ [data-zx-fields="quiet"] .zx-datebox:not([aria-disabled="true"], [data-state="invalid"]):hover .zx-datebox__control {
176
+ background: var(--zx-color-bg-hover);
177
+ }
178
+
179
+ [data-zx-fields="quiet"] .zx-datebox:not([aria-disabled="true"]):is(:hover, :focus-within) :is(.zx-datebox__toggle, .zx-datebox__clear) {
180
+ opacity: 1;
181
+ }
182
+
183
+ [data-zx-fields="quiet"] .zx-datebox:not([aria-disabled="true"]):focus-within .zx-datebox__toggle {
184
+ border-inline-start-color: var(--zx-color-border-control);
185
+ }
186
+
187
+ [data-zx-fields="quiet"] .zx-datebox:not([aria-disabled="true"], [data-state="invalid"]):focus-within .zx-datebox__control {
188
+ border-color: var(--zx-color-border-control);
189
+ background: var(--zx-color-bg-control);
190
+ }
191
+
192
+ @media (prefers-reduced-motion: no-preference) {
193
+ [data-zx-fields="quiet"] .zx-datebox__control {
194
+ transition: background-color var(--zx-dur-fast) var(--zx-ease), border-color var(--zx-dur-fast) var(--zx-ease);
195
+ }
196
+
197
+ [data-zx-fields="quiet"] :is(.zx-datebox__toggle, .zx-datebox__clear) {
198
+ transition: opacity var(--zx-dur-fast) var(--zx-ease), border-color var(--zx-dur-fast) var(--zx-ease);
199
+ }
200
+ }
@@ -14,7 +14,8 @@ import { DatePicker } from '../date-picker/date-picker.js';
14
14
  * @property {Date|null} [min=null] Earliest selectable date.
15
15
  * @property {Date|null} [max=null] Latest selectable date.
16
16
  * @property {string|null} [placeholder=null] Input placeholder; null derives one from the format.
17
- * @property {boolean} [clearable=true] Show a clear button when a value is present.
17
+ * @property {boolean} [clearable=true] Show a clear button while a value is present and the box is
18
+ * enabled; without it the input keeps no room for one.
18
19
  * @property {boolean} [disabled=false] Disable all controls.
19
20
  * @property {(event: CustomEvent<{date: Date|null}>) => void} [onchange] Change listener.
20
21
  * @property {(event: CustomEvent<{text: string}>) => void} [oninvalid] Invalid-input listener.
@@ -53,6 +54,8 @@ export class Datebox extends Component {
53
54
  this.el = root;
54
55
  this._originalState = root.getAttribute('data-state');
55
56
  this._originalAriaDisabled = root.getAttribute('aria-disabled');
57
+ this._originalClearable = root.getAttribute('data-clearable');
58
+ root.toggleAttribute('data-clearable', Boolean(this.options.clearable));
56
59
  this._format = effectiveFormat(this.options.format, this.options.time);
57
60
  this._disabled = Boolean(this.options.disabled);
58
61
  this._open = false;
@@ -230,6 +233,8 @@ export class Datebox extends Component {
230
233
  else this.el.setAttribute('data-state', this._originalState);
231
234
  if (this._originalAriaDisabled === null) this.el.removeAttribute('aria-disabled');
232
235
  else this.el.setAttribute('aria-disabled', this._originalAriaDisabled);
236
+ if (this._originalClearable === null) this.el.removeAttribute('data-clearable');
237
+ else this.el.setAttribute('data-clearable', this._originalClearable);
233
238
  super.destroy();
234
239
  }
235
240
 
@@ -278,7 +283,12 @@ export class Datebox extends Component {
278
283
  /** @returns {void} */
279
284
  _syncDisplay() {
280
285
  this.refs.input.value = this._value ? formatDate(this._value, this._format) : '';
281
- this.refs.clear.hidden = !this.options.clearable || !this._value;
286
+ this._syncClear();
287
+ }
288
+
289
+ /** A disabled box offers no clear button: there is nothing it could do. @returns {void} */
290
+ _syncClear() {
291
+ this.refs.clear.hidden = !this.options.clearable || !this._value || this._disabled;
282
292
  }
283
293
 
284
294
  /** @returns {void} */
@@ -287,6 +297,7 @@ export class Datebox extends Component {
287
297
  this.refs.toggle.disabled = this._disabled;
288
298
  this.refs.clear.disabled = this._disabled;
289
299
  this.el.setAttribute('aria-disabled', String(this._disabled));
300
+ this._syncClear();
290
301
  }
291
302
 
292
303
  /** @returns {void} */
@@ -24,6 +24,8 @@
24
24
  .zx-dialog__title {
25
25
  margin: 0;
26
26
  font-size: var(--zx-text-xl);
27
+ font-weight: 650;
28
+ letter-spacing: var(--zx-tracking-title);
27
29
  line-height: 1.3;
28
30
  }
29
31
 
@@ -61,6 +63,30 @@
61
63
  inline-size: 100%;
62
64
  }
63
65
 
66
+ /* Type-to-confirm: the message, then the label naming the text to type, then the input. */
67
+ .zx-dialog__confirm {
68
+ display: grid;
69
+ gap: var(--zx-space-2);
70
+ }
71
+
72
+ .zx-dialog__confirm > .zx-dialog__prompt-message {
73
+ margin-block-end: var(--zx-space-2);
74
+ }
75
+
76
+ .zx-dialog__confirm-label {
77
+ color: var(--zx-color-text-muted);
78
+ font-size: var(--zx-text-sm);
79
+ }
80
+
81
+ .zx-dialog__confirm-label > b {
82
+ color: var(--zx-color-text);
83
+ font-weight: var(--zx-weight-semibold);
84
+ }
85
+
86
+ .zx-dialog__confirm-input {
87
+ inline-size: 100%;
88
+ }
89
+
64
90
  @media (prefers-reduced-motion: no-preference) {
65
91
  .zx-dialog__view[aria-hidden="false"] {
66
92
  animation: zx-dialog-view-enter var(--zx-dur-fast) var(--zx-ease);
@@ -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
@@ -22,7 +20,7 @@ const FOCUSABLE_SELECTOR = [
22
20
  * @property {DialogButton[]} [buttons=[]] Footer buttons.
23
21
  * @property {boolean} [closable=true] Whether Escape and the header button may close the dialog.
24
22
  * @property {Node|string|number|{toElement: () => Node|null}|null} [content=null] Body content.
25
- * @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the overlay; defaults to the opener.
23
+ * @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the overlay; defaults to the opener. Resolved on every open, so a scope connected after construction still owns it.
26
24
  * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
27
25
  * @property {(event: CustomEvent<{result: unknown}>) => void} [onclose] Close event listener.
28
26
  * @property {(event: CustomEvent<Record<string, never>>) => void} [oncancel] Cancel event listener.
@@ -53,6 +51,11 @@ const FOCUSABLE_SELECTOR = [
53
51
  * @property {string} [okLabel='OK'] Confirmation label.
54
52
  * @property {string} [cancelLabel='Cancel'] Cancellation label.
55
53
  * @property {boolean} [danger=false] Whether the confirmation action is dangerous.
54
+ * @property {string|null} [confirmText=null] Text the reader must type before the confirm button
55
+ * enables — the name of what is about to be deleted, say. The dialog adds an input labelled
56
+ * "Type <confirmText> to confirm" (message `dialog.typeToConfirm`, `%1` standing for the text),
57
+ * focuses it, and enables the confirm button, and Enter, only while its value matches exactly,
58
+ * ignoring surrounding spaces.
56
59
  */
57
60
  /**
58
61
  * @typedef {Object} PromptOptions
@@ -60,6 +63,8 @@ const FOCUSABLE_SELECTOR = [
60
63
  * @property {Node|string|number|null} [message=''] Prompt message.
61
64
  * @property {string} [value=''] Initial input value.
62
65
  * @property {string} [placeholder=''] Input placeholder.
66
+ * @property {string} [okLabel='OK'] Confirmation label.
67
+ * @property {string} [cancelLabel='Cancel'] Cancellation label.
63
68
  */
64
69
 
65
70
  /**
@@ -86,8 +91,6 @@ export class Dialog extends Modal {
86
91
  /** @type {DialogButton[]} */
87
92
  #baseButtons = [];
88
93
  #baseTitle = '';
89
- /** @type {Element|null} */
90
- #opener = null;
91
94
 
92
95
  /**
93
96
  * Creates a structured dialog in the configured theme scope, or at document level when unscoped.
@@ -99,7 +102,7 @@ export class Dialog extends Modal {
99
102
  this._applySize(this.options.size);
100
103
  this.#baseTitle = String(this.options.title ?? '');
101
104
  this.#applyTitle(this.#baseTitle);
102
- this.#baseButtons = copyButtons(this.options.buttons);
105
+ this.#baseButtons = normalizeDialogButtons(this.options.buttons);
103
106
  this.#renderButtons(this.#baseButtons);
104
107
  this.refs.close.hidden = !this.options.closable;
105
108
 
@@ -111,7 +114,6 @@ export class Dialog extends Modal {
111
114
  if (!definition) return;
112
115
  this.#runButton(definition);
113
116
  });
114
- this.on('close', () => this.#restoreFocus());
115
117
  }
116
118
 
117
119
  /** @type {DialogButton[]} */
@@ -123,6 +125,7 @@ export class Dialog extends Modal {
123
125
  */
124
126
  render() {
125
127
  const titleId = uid('zx-dialog-title');
128
+ const close = this._message('dialog.close', 'Close');
126
129
  const dialog = /** @type {HTMLDialogElement} */ (h('dialog', {
127
130
  class: 'zx-modal zx-dialog',
128
131
  ariaLabelledby: titleId
@@ -134,15 +137,15 @@ export class Dialog extends Modal {
134
137
  ref: 'close',
135
138
  type: 'button',
136
139
  'data-kind': 'ghost',
137
- ariaLabel: 'Close',
138
- title: 'Close'
140
+ ariaLabel: close,
141
+ title: close
139
142
  }, '×')
140
143
  ),
141
144
  h('div', { class: 'zx-modal__content zx-dialog__body', ref: 'content' }),
142
145
  h('footer', { class: 'zx-dialog__footer', ref: 'footer' })
143
146
  ));
144
147
  dialog.dataset.state = 'closed';
145
- this.mountTarget().append(dialog);
148
+ this._mountInitial(dialog);
146
149
  return dialog;
147
150
  }
148
151
 
@@ -151,16 +154,13 @@ export class Dialog extends Modal {
151
154
  * @returns {this}
152
155
  */
153
156
  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;
157
+ /*
158
+ * Capturing the opener, placing initial focus and restoring it afterwards are `Modal`'s, so
159
+ * that every overlay built on it is accessible rather than only the structured one. The
160
+ * override stays because the promise is the dialog's: the header close button is the first
161
+ * focusable descendant, and a footer button declared `autofocus` outranks it.
162
+ */
163
+ return super.open();
164
164
  }
165
165
 
166
166
  /**
@@ -192,7 +192,7 @@ export class Dialog extends Modal {
192
192
  * @returns {this}
193
193
  */
194
194
  setButtons(list) {
195
- this.#baseButtons = copyButtons(list);
195
+ this.#baseButtons = normalizeDialogButtons(list);
196
196
  this.#renderButtons(this.#baseButtons);
197
197
  return this;
198
198
  }
@@ -216,7 +216,7 @@ export class Dialog extends Modal {
216
216
  const view = {
217
217
  key: normalizedKey,
218
218
  content,
219
- buttons: options && Object.hasOwn(options, 'buttons') ? copyButtons(options.buttons ?? []) : null,
219
+ buttons: options && Object.hasOwn(options, 'buttons') ? normalizeDialogButtons(options.buttons ?? []) : null,
220
220
  title: options && Object.hasOwn(options, 'title') ? String(options.title ?? '') : null
221
221
  };
222
222
  this.#views.set(normalizedKey, view);
@@ -260,7 +260,7 @@ export class Dialog extends Modal {
260
260
  * @param {AlertOptions} [options={}] Alert options.
261
261
  * @returns {Promise<void>}
262
262
  */
263
- static alert({ title = '', message = '', okLabel = 'OK' } = {}) {
263
+ static alert({ title = '', message = '', okLabel } = {}) {
264
264
  return new Promise((resolve) => {
265
265
  let settled = false;
266
266
  const finish = () => {
@@ -269,11 +269,13 @@ export class Dialog extends Modal {
269
269
  dialog.close();
270
270
  resolve();
271
271
  };
272
- const dialog = new Dialog(null, {
273
- title,
274
- content: message,
275
- buttons: [{ label: okLabel, kind: 'primary', action: finish, autofocus: true }]
276
- });
272
+ const dialog = new Dialog(null, { title, content: message });
273
+ dialog.setButtons([{
274
+ label: okLabel ?? dialog._message('dialog.ok', 'OK'),
275
+ kind: 'primary',
276
+ action: finish,
277
+ autofocus: true
278
+ }]);
277
279
  dialog.once('close', () => {
278
280
  if (!settled) {
279
281
  settled = true;
@@ -286,12 +288,13 @@ export class Dialog extends Modal {
286
288
  }
287
289
 
288
290
  /**
289
- * Opens a confirmation dialog.
291
+ * Opens a confirmation dialog. With `confirmText` it becomes a type-to-confirm dialog: the
292
+ * confirm button stays disabled until the reader has typed that text.
290
293
  * @param {ConfirmOptions} [options={}] Confirmation options.
291
- * @returns {Promise<boolean>}
294
+ * @returns {Promise<boolean>} True when confirmed; false on Cancel, Escape, or the close button.
292
295
  */
293
296
  static confirm({
294
- title = '', message = '', okLabel = 'OK', cancelLabel = 'Cancel', danger = false
297
+ title = '', message = '', okLabel, cancelLabel, danger = false, confirmText = null
295
298
  } = {}) {
296
299
  return new Promise((resolve) => {
297
300
  let settled = false;
@@ -301,14 +304,58 @@ export class Dialog extends Modal {
301
304
  dialog.close(value);
302
305
  resolve(value);
303
306
  };
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
- });
307
+ const expected = confirmText == null ? '' : String(confirmText).trim();
308
+ const dialog = new Dialog(null, { title, content: expected ? null : message });
309
+ // A destructive confirmation starts on the least destructive choice, as the APG alert dialog
310
+ // has it; type-to-confirm starts in its input.
311
+ const cancel = {
312
+ label: cancelLabel ?? dialog._message('dialog.cancel', 'Cancel'),
313
+ action: () => finish(false),
314
+ autofocus: danger && !expected
315
+ };
316
+ const accept = {
317
+ label: okLabel ?? dialog._message('dialog.ok', 'OK'),
318
+ kind: /** @type {DialogButtonKind} */ (danger ? 'danger' : 'primary'),
319
+ action: () => finish(true),
320
+ autofocus: !danger && !expected
321
+ };
322
+ if (expected) {
323
+ const input = /** @type {HTMLInputElement} */ (h('input', {
324
+ class: 'zx-dialog__confirm-input',
325
+ id: uid('zx-dialog-confirm'),
326
+ type: 'text',
327
+ autocomplete: 'off',
328
+ autocapitalize: 'off',
329
+ spellcheck: false,
330
+ autofocus: true
331
+ }));
332
+ const [before, after] = typeToConfirmParts(
333
+ dialog._message('dialog.typeToConfirm', 'Type %1 to confirm'), expected);
334
+ const label = h('label', { class: 'zx-dialog__confirm-label', for: input.id },
335
+ before, h('b', {}, expected), after);
336
+ dialog.setContent(h('div', { class: 'zx-dialog__confirm' },
337
+ isEmptyContent(message) ? null : h('div', { class: 'zx-dialog__prompt-message' }, message),
338
+ label,
339
+ input));
340
+ let matched = false;
341
+ const settle = () => dialog.setButtons([cancel, { ...accept, autofocus: false, disabled: !matched }]);
342
+ settle();
343
+ dialog.listen(input, 'input', () => {
344
+ const next = typedConfirmation(input.value, expected);
345
+ if (next === matched) return;
346
+ matched = next;
347
+ settle();
348
+ });
349
+ // Enter confirms only a match; otherwise it does nothing, as the disabled button would.
350
+ dialog.listen(input, 'keydown', (event) => {
351
+ const key = /** @type {KeyboardEvent} */ (event);
352
+ if (key.key !== 'Enter' || key.isComposing) return;
353
+ key.preventDefault();
354
+ if (typedConfirmation(input.value, expected)) finish(true);
355
+ });
356
+ } else {
357
+ dialog.setButtons([cancel, accept]);
358
+ }
312
359
  dialog.once('close', () => {
313
360
  if (!settled) {
314
361
  settled = true;
@@ -325,15 +372,16 @@ export class Dialog extends Modal {
325
372
  * @param {PromptOptions} [options={}] Prompt options.
326
373
  * @returns {Promise<string|null>}
327
374
  */
328
- static prompt({ title = '', message = '', value = '', placeholder = '' } = {}) {
375
+ static prompt({
376
+ title = '', message = '', value = '', placeholder = '', okLabel, cancelLabel
377
+ } = {}) {
329
378
  return new Promise((resolve) => {
330
379
  let settled = false;
331
380
  const input = h('input', {
332
381
  class: 'zx-dialog__prompt-input',
333
382
  value: String(value),
334
383
  placeholder: String(placeholder),
335
- autofocus: true,
336
- ariaLabel: String(title || 'Prompt value')
384
+ autofocus: true
337
385
  });
338
386
  const content = h('div', { class: 'zx-dialog__prompt' },
339
387
  h('div', { class: 'zx-dialog__prompt-message' }, message),
@@ -345,14 +393,12 @@ export class Dialog extends Modal {
345
393
  dialog.close(result);
346
394
  resolve(result);
347
395
  };
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
- });
396
+ const dialog = new Dialog(null, { title, content });
397
+ input.setAttribute('aria-label', String(title || dialog._message('dialog.promptValue', 'Prompt value')));
398
+ dialog.setButtons([
399
+ { label: cancelLabel ?? dialog._message('dialog.cancel', 'Cancel'), action: () => finish(null) },
400
+ { label: okLabel ?? dialog._message('dialog.ok', 'OK'), kind: 'primary', action: () => finish(input.value) }
401
+ ]);
356
402
  dialog.listen(input, 'keydown', (event) => {
357
403
  if (event.key !== 'Enter') return;
358
404
  event.preventDefault();
@@ -383,6 +429,17 @@ export class Dialog extends Modal {
383
429
  this.el.style.inlineSize = `${Math.max(0, width)}px`;
384
430
  }
385
431
 
432
+ /**
433
+ * Resolves a component message, falling back to the built-in English text.
434
+ * @param {string} key Message key.
435
+ * @param {string} fallback English default.
436
+ * @returns {string}
437
+ */
438
+ _message(key, fallback) {
439
+ const message = this.msg(key);
440
+ return message === key ? fallback : message;
441
+ }
442
+
386
443
  /** @param {string} title @returns {void} */
387
444
  #applyTitle(title) {
388
445
  this.refs.title.textContent = title;
@@ -390,23 +447,27 @@ export class Dialog extends Modal {
390
447
 
391
448
  /** @param {DialogButton[]} list @returns {void} */
392
449
  #renderButtons(list) {
393
- this.#renderedButtons = copyButtons(list);
450
+ this.#renderedButtons = normalizeDialogButtons(list);
394
451
  this.refs.footer.replaceChildren();
395
452
  this.refs.footer.hidden = this.#renderedButtons.length === 0;
396
453
  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)));
454
+ const control = button({
455
+ label: definition.label,
456
+ kind: definition.kind,
457
+ shortcut: definition.shortcut,
458
+ disabled: definition.disabled
459
+ });
460
+ control.dataset.dialogButton = String(index);
461
+ control.autofocus = definition.autofocus;
462
+ this.refs.footer.append(control);
405
463
  });
406
464
  }
407
465
 
408
466
  /** @param {DialogButton} definition @returns {void} */
409
467
  #runButton(definition) {
468
+ // A native disabled button swallows its own clicks, but the footer listener is delegated and
469
+ // a host may dispatch one at the row; the descriptor is the authority either way.
470
+ if (definition.disabled) return;
410
471
  const action = definition.action ?? 'close';
411
472
  if (action === 'close') {
412
473
  this.close();
@@ -417,17 +478,60 @@ export class Dialog extends Modal {
417
478
  action(this);
418
479
  }
419
480
  }
481
+ }
420
482
 
421
- /** @returns {void} */
422
- #restoreFocus() {
423
- if (this.#opener?.isConnected && typeof this.#opener.focus === 'function') this.#opener.focus();
424
- this.#opener = null;
425
- }
483
+ /**
484
+ * Reports whether what the reader typed confirms a type-to-confirm dialog: the same text, exactly
485
+ * — case and inner spacing included — once surrounding spaces are ignored on both sides.
486
+ * @param {unknown} typed Input value.
487
+ * @param {unknown} expected Text the dialog asks for.
488
+ * @returns {boolean}
489
+ */
490
+ export function typedConfirmation(typed, expected) {
491
+ const target = expected == null ? '' : String(expected).trim();
492
+ return target !== '' && String(typed ?? '').trim() === target;
493
+ }
494
+
495
+ /**
496
+ * Splits the type-to-confirm message around `%1`, where the text to type goes. A translation that
497
+ * leaves `%1` out gets the text after it, following a space; a second `%1` is filled in as text.
498
+ * @param {string} template Message such as "Type %1 to confirm".
499
+ * @param {string} expected Text to type.
500
+ * @returns {[string, string]} The text before and after the text to type.
501
+ */
502
+ export function typeToConfirmParts(template, expected) {
503
+ const parts = String(template).split('%1');
504
+ if (parts.length === 1) return [`${parts[0]} `, ''];
505
+ return [parts[0], parts.slice(1).join(expected)];
506
+ }
507
+
508
+ /** @param {unknown} content @returns {boolean} */
509
+ function isEmptyContent(content) {
510
+ return content == null || content === '';
426
511
  }
427
512
 
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 ?? '') })) : [];
513
+ /**
514
+ * Copies footer-button descriptors without mutating the caller's array or objects, and settles
515
+ * the two booleans the footer renders from.
516
+ *
517
+ * `autofocus` and `disabled` are resolved together because they contradict each other: a control
518
+ * that refuses activation must not be the one initial focus lands on, and a host that writes both
519
+ * has said what it wants twice. Deciding it here rather than at render time keeps `Sheet` — which
520
+ * takes the same descriptors through the same footer — on one answer.
521
+ * @param {DialogButton[]|null|undefined} list Button descriptors.
522
+ * @returns {DialogButton[]} Normalized copies.
523
+ */
524
+ export function normalizeDialogButtons(list) {
525
+ if (!Array.isArray(list)) return [];
526
+ return list.map((button) => {
527
+ const disabled = Boolean(button?.disabled);
528
+ return {
529
+ ...button,
530
+ label: String(button?.label ?? ''),
531
+ disabled,
532
+ autofocus: Boolean(button?.autofocus) && !disabled
533
+ };
534
+ });
431
535
  }
432
536
 
433
537
  /**
@@ -3,7 +3,7 @@
3
3
  max-block-size: calc(100vh - (2 * var(--zx-space-3)));
4
4
  overflow: auto;
5
5
  border: 1px solid var(--zx-color-overlay-border);
6
- border-radius: var(--zx-radius-md);
6
+ border-radius: var(--zx-radius-lg);
7
7
  background-color: var(--zx-color-overlay-surface);
8
8
  background-image: var(--zx-overlay-surface-image);
9
9
  background-clip: padding-box;