@zeyos/zx 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +15 -0
  3. package/dist/types/compat/base.d.ts +156 -0
  4. package/dist/types/compat/globals.d.ts +20 -0
  5. package/dist/types/compat/index.d.ts +128 -0
  6. package/dist/types/compat/map/basic.d.ts +175 -0
  7. package/dist/types/compat/map/data.d.ts +102 -0
  8. package/dist/types/compat/map/date-time.d.ts +80 -0
  9. package/dist/types/compat/map/factory.d.ts +9 -0
  10. package/dist/types/compat/map/form.d.ts +98 -0
  11. package/dist/types/compat/map/helpers.d.ts +12 -0
  12. package/dist/types/compat/map/http.d.ts +36 -0
  13. package/dist/types/compat/map/layout.d.ts +106 -0
  14. package/dist/types/compat/map/options.d.ts +80 -0
  15. package/dist/types/compat/map/select.d.ts +95 -0
  16. package/dist/types/compat/map/stubs.d.ts +32 -0
  17. package/dist/types/compat/map/utilities.d.ts +37 -0
  18. package/dist/types/compat-entry.d.ts +5 -0
  19. package/dist/types/components/badge/badge.d.ts +46 -0
  20. package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
  21. package/dist/types/components/button/button.d.ts +42 -0
  22. package/dist/types/components/check-button/check-button.d.ts +98 -0
  23. package/dist/types/components/checklist/checklist.d.ts +143 -0
  24. package/dist/types/components/checklist/field-adapter.d.ts +5 -0
  25. package/dist/types/components/context-menu/context-menu.d.ts +130 -0
  26. package/dist/types/components/copy/copy-button.d.ts +48 -0
  27. package/dist/types/components/copy/copy-input.d.ts +91 -0
  28. package/dist/types/components/data-filter/data-filter.d.ts +250 -0
  29. package/dist/types/components/data-filter/filter-core.d.ts +52 -0
  30. package/dist/types/components/date-picker/date-picker.d.ts +137 -0
  31. package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
  32. package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
  33. package/dist/types/components/date-picker/month-picker.d.ts +75 -0
  34. package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
  35. package/dist/types/components/date-picker/time-picker.d.ts +127 -0
  36. package/dist/types/components/datebox/date-range-box.d.ts +218 -0
  37. package/dist/types/components/datebox/datebox.d.ts +160 -0
  38. package/dist/types/components/datebox/field-adapter.d.ts +5 -0
  39. package/dist/types/components/dialog/dialog.d.ts +284 -0
  40. package/dist/types/components/dropdown/dropdown.d.ts +118 -0
  41. package/dist/types/components/empty-state/empty-state.d.ts +38 -0
  42. package/dist/types/components/field/field.d.ts +231 -0
  43. package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
  44. package/dist/types/components/field-upload/field-upload.d.ts +213 -0
  45. package/dist/types/components/fieldset/fieldset.d.ts +125 -0
  46. package/dist/types/components/finder/finder.d.ts +252 -0
  47. package/dist/types/components/form/form.d.ts +194 -0
  48. package/dist/types/components/groupbox/groupbox.d.ts +89 -0
  49. package/dist/types/components/layout/layout.d.ts +132 -0
  50. package/dist/types/components/loading/inline-loading.d.ts +86 -0
  51. package/dist/types/components/loading/progress-bar.d.ts +168 -0
  52. package/dist/types/components/loading/spinner.d.ts +34 -0
  53. package/dist/types/components/master-panel/master-panel.d.ts +112 -0
  54. package/dist/types/components/menu-button/menu-button.d.ts +142 -0
  55. package/dist/types/components/message/message.d.ts +212 -0
  56. package/dist/types/components/modal/modal.d.ts +105 -0
  57. package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
  58. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
  59. package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
  60. package/dist/types/components/number-field/field-adapter.d.ts +5 -0
  61. package/dist/types/components/number-field/number-field.d.ts +292 -0
  62. package/dist/types/components/pagination/pagination.d.ts +267 -0
  63. package/dist/types/components/panel/panel.d.ts +194 -0
  64. package/dist/types/components/permission/permission.d.ts +93 -0
  65. package/dist/types/components/rating/field-adapter.d.ts +5 -0
  66. package/dist/types/components/rating/rating.d.ts +213 -0
  67. package/dist/types/components/search/search.d.ts +98 -0
  68. package/dist/types/components/select/field-adapter.d.ts +5 -0
  69. package/dist/types/components/select/filter.d.ts +11 -0
  70. package/dist/types/components/select/select.d.ts +235 -0
  71. package/dist/types/components/skeleton/skeleton.d.ts +97 -0
  72. package/dist/types/components/slider/field-adapter.d.ts +5 -0
  73. package/dist/types/components/slider/slider.d.ts +216 -0
  74. package/dist/types/components/split-view/split-view.d.ts +335 -0
  75. package/dist/types/components/stepper/stepper.d.ts +314 -0
  76. package/dist/types/components/stepper/steps.d.ts +131 -0
  77. package/dist/types/components/tabbox/tabbox.d.ts +230 -0
  78. package/dist/types/components/table/sort.d.ts +49 -0
  79. package/dist/types/components/table/table.d.ts +872 -0
  80. package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
  81. package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
  82. package/dist/types/components/timebox/field-adapter.d.ts +5 -0
  83. package/dist/types/components/timebox/timebox.d.ts +121 -0
  84. package/dist/types/components/toggle/field-adapter.d.ts +5 -0
  85. package/dist/types/components/toggle/toggle.d.ts +94 -0
  86. package/dist/types/components/toolbar/toolbar.d.ts +230 -0
  87. package/dist/types/components/tooltip/tooltip.d.ts +183 -0
  88. package/dist/types/components/tree/hierarchy.d.ts +113 -0
  89. package/dist/types/components/tree/tree.d.ts +383 -0
  90. package/dist/types/components/truncate/truncate.d.ts +61 -0
  91. package/dist/types/components/value-list/field-adapter.d.ts +5 -0
  92. package/dist/types/components/value-list/value-list.d.ts +143 -0
  93. package/dist/types/core/breakpoint.d.ts +68 -0
  94. package/dist/types/core/component.d.ts +115 -0
  95. package/dist/types/core/date.d.ts +50 -0
  96. package/dist/types/core/dom.d.ts +86 -0
  97. package/dist/types/core/export.d.ts +70 -0
  98. package/dist/types/core/fontawesome.d.ts +107 -0
  99. package/dist/types/core/format.d.ts +107 -0
  100. package/dist/types/core/http.d.ts +122 -0
  101. package/dist/types/core/i18n.d.ts +35 -0
  102. package/dist/types/core/icons.d.ts +137 -0
  103. package/dist/types/core/keyboard.d.ts +72 -0
  104. package/dist/types/core/position.d.ts +45 -0
  105. package/dist/types/core/storage.d.ts +46 -0
  106. package/dist/types/core/util.d.ts +117 -0
  107. package/dist/types/elements/define.d.ts +73 -0
  108. package/dist/types/elements/reflect.d.ts +27 -0
  109. package/dist/types/index.d.ts +79 -0
  110. package/dist/types/zeyos/connect.d.ts +65 -0
  111. package/dist/types/zeyos/form.d.ts +140 -0
  112. package/dist/types/zeyos/icons.d.ts +104 -0
  113. package/dist/types/zeyos/index.d.ts +12 -0
  114. package/dist/types/zeyos/modules.d.ts +110 -0
  115. package/dist/types/zeyos/query.d.ts +157 -0
  116. package/dist/types/zeyos/schema.d.ts +79 -0
  117. package/dist/types/zeyos/select.d.ts +167 -0
  118. package/dist/types/zeyos/table.d.ts +205 -0
  119. package/dist/zx-compat.esm.js +362 -33
  120. package/dist/zx-compat.esm.min.js +1 -1
  121. package/dist/zx-compat.global.js +359 -67
  122. package/dist/zx-compat.global.min.js +3 -3
  123. package/dist/zx-zeyos.esm.js +342 -43
  124. package/dist/zx-zeyos.esm.min.js +1 -1
  125. package/dist/zx.css +68 -6
  126. package/dist/zx.esm.js +346 -67
  127. package/dist/zx.esm.min.js +3 -3
  128. package/dist/zx.global.js +346 -67
  129. package/dist/zx.global.min.js +3 -3
  130. package/dist/zx.min.css +1 -1
  131. package/docs/api.json +48 -2
  132. package/docs/llms.md +28 -0
  133. package/package.json +37 -9
  134. package/src/compat/base.js +291 -0
  135. package/src/compat/globals.js +165 -0
  136. package/src/compat/index.js +84 -0
  137. package/src/compat/map/basic.js +464 -0
  138. package/src/compat/map/data.js +232 -0
  139. package/src/compat/map/date-time.js +177 -0
  140. package/src/compat/map/factory.js +45 -0
  141. package/src/compat/map/form.js +330 -0
  142. package/src/compat/map/helpers.js +46 -0
  143. package/src/compat/map/http.js +117 -0
  144. package/src/compat/map/layout.js +256 -0
  145. package/src/compat/map/options.js +267 -0
  146. package/src/compat/map/select.js +198 -0
  147. package/src/compat/map/stubs.js +26 -0
  148. package/src/compat/map/utilities.js +113 -0
  149. package/src/compat-entry.js +11 -0
  150. package/src/components/badge/badge.css +147 -0
  151. package/src/components/badge/badge.js +92 -0
  152. package/src/components/breadcrumb/breadcrumb.css +105 -0
  153. package/src/components/breadcrumb/breadcrumb.js +331 -0
  154. package/src/components/button/button.css +104 -0
  155. package/src/components/button/button.js +76 -0
  156. package/src/components/check-button/check-button.css +66 -0
  157. package/src/components/check-button/check-button.js +167 -0
  158. package/src/components/checklist/checklist.css +65 -0
  159. package/src/components/checklist/checklist.js +282 -0
  160. package/src/components/checklist/field-adapter.js +46 -0
  161. package/src/components/context-menu/context-menu.css +54 -0
  162. package/src/components/context-menu/context-menu.js +334 -0
  163. package/src/components/copy/copy-button.css +27 -0
  164. package/src/components/copy/copy-button.js +78 -0
  165. package/src/components/copy/copy-input.css +59 -0
  166. package/src/components/copy/copy-input.js +123 -0
  167. package/src/components/data-filter/data-filter.css +53 -0
  168. package/src/components/data-filter/data-filter.js +400 -0
  169. package/src/components/data-filter/filter-core.js +128 -0
  170. package/src/components/date-picker/date-picker.css +181 -0
  171. package/src/components/date-picker/date-picker.js +500 -0
  172. package/src/components/date-picker/date-range-picker.css +264 -0
  173. package/src/components/date-picker/date-range-picker.js +990 -0
  174. package/src/components/date-picker/field-adapter.js +36 -0
  175. package/src/components/date-picker/month-picker.js +223 -0
  176. package/src/components/date-picker/range-field-adapter.js +31 -0
  177. package/src/components/date-picker/time-picker.css +191 -0
  178. package/src/components/date-picker/time-picker.js +406 -0
  179. package/src/components/datebox/date-range-box.css +128 -0
  180. package/src/components/datebox/date-range-box.js +492 -0
  181. package/src/components/datebox/datebox.css +108 -0
  182. package/src/components/datebox/datebox.js +358 -0
  183. package/src/components/datebox/field-adapter.js +34 -0
  184. package/src/components/dialog/dialog.css +74 -0
  185. package/src/components/dialog/dialog.js +450 -0
  186. package/src/components/dropdown/dropdown.css +24 -0
  187. package/src/components/dropdown/dropdown.js +288 -0
  188. package/src/components/empty-state/empty-state.css +80 -0
  189. package/src/components/empty-state/empty-state.js +92 -0
  190. package/src/components/field/field.css +127 -0
  191. package/src/components/field/field.js +605 -0
  192. package/src/components/field-upload/field-adapter.js +21 -0
  193. package/src/components/field-upload/field-upload.css +108 -0
  194. package/src/components/field-upload/field-upload.js +475 -0
  195. package/src/components/fieldset/fieldset.css +37 -0
  196. package/src/components/fieldset/fieldset.js +307 -0
  197. package/src/components/finder/finder.css +98 -0
  198. package/src/components/finder/finder.js +509 -0
  199. package/src/components/form/form.css +32 -0
  200. package/src/components/form/form.js +302 -0
  201. package/src/components/groupbox/groupbox.css +51 -0
  202. package/src/components/groupbox/groupbox.js +199 -0
  203. package/src/components/layout/layout.css +117 -0
  204. package/src/components/layout/layout.js +136 -0
  205. package/src/components/loading/inline-loading.css +34 -0
  206. package/src/components/loading/inline-loading.js +140 -0
  207. package/src/components/loading/progress-bar.css +115 -0
  208. package/src/components/loading/progress-bar.js +259 -0
  209. package/src/components/loading/spinner.css +58 -0
  210. package/src/components/loading/spinner.js +57 -0
  211. package/src/components/master-panel/master-panel.css +51 -0
  212. package/src/components/master-panel/master-panel.js +203 -0
  213. package/src/components/menu-button/menu-button.css +49 -0
  214. package/src/components/menu-button/menu-button.js +294 -0
  215. package/src/components/message/message.css +122 -0
  216. package/src/components/message/message.js +499 -0
  217. package/src/components/modal/modal.css +47 -0
  218. package/src/components/modal/modal.js +203 -0
  219. package/src/components/multi-value-editor/field-adapter.js +44 -0
  220. package/src/components/multi-value-editor/multi-value-editor.css +56 -0
  221. package/src/components/multi-value-editor/multi-value-editor.js +170 -0
  222. package/src/components/navigation-bar/navigation-bar.css +130 -0
  223. package/src/components/navigation-bar/navigation-bar.js +450 -0
  224. package/src/components/number-field/field-adapter.js +40 -0
  225. package/src/components/number-field/number-field.css +77 -0
  226. package/src/components/number-field/number-field.js +545 -0
  227. package/src/components/pagination/pagination.css +95 -0
  228. package/src/components/pagination/pagination.js +481 -0
  229. package/src/components/panel/panel.css +123 -0
  230. package/src/components/panel/panel.js +336 -0
  231. package/src/components/permission/permission.css +47 -0
  232. package/src/components/permission/permission.js +198 -0
  233. package/src/components/rating/field-adapter.js +34 -0
  234. package/src/components/rating/rating.css +97 -0
  235. package/src/components/rating/rating.js +395 -0
  236. package/src/components/search/search.css +70 -0
  237. package/src/components/search/search.js +193 -0
  238. package/src/components/select/field-adapter.js +24 -0
  239. package/src/components/select/filter.js +39 -0
  240. package/src/components/select/select.css +180 -0
  241. package/src/components/select/select.js +663 -0
  242. package/src/components/skeleton/skeleton.css +76 -0
  243. package/src/components/skeleton/skeleton.js +139 -0
  244. package/src/components/slider/field-adapter.js +36 -0
  245. package/src/components/slider/slider.css +181 -0
  246. package/src/components/slider/slider.js +399 -0
  247. package/src/components/split-view/split-view.css +139 -0
  248. package/src/components/split-view/split-view.js +806 -0
  249. package/src/components/stepper/stepper.css +234 -0
  250. package/src/components/stepper/stepper.js +459 -0
  251. package/src/components/stepper/steps.js +131 -0
  252. package/src/components/tabbox/tabbox.css +281 -0
  253. package/src/components/tabbox/tabbox.js +484 -0
  254. package/src/components/table/sort.js +85 -0
  255. package/src/components/table/table.css +364 -0
  256. package/src/components/table/table.js +1693 -0
  257. package/src/components/tag-picker/field-adapter.js +35 -0
  258. package/src/components/tag-picker/tag-picker.css +169 -0
  259. package/src/components/tag-picker/tag-picker.js +837 -0
  260. package/src/components/timebox/field-adapter.js +24 -0
  261. package/src/components/timebox/timebox.css +51 -0
  262. package/src/components/timebox/timebox.js +276 -0
  263. package/src/components/toggle/field-adapter.js +24 -0
  264. package/src/components/toggle/toggle.css +66 -0
  265. package/src/components/toggle/toggle.js +150 -0
  266. package/src/components/toolbar/toolbar.css +81 -0
  267. package/src/components/toolbar/toolbar.js +511 -0
  268. package/src/components/tooltip/tooltip.css +36 -0
  269. package/src/components/tooltip/tooltip.js +420 -0
  270. package/src/components/tree/hierarchy.js +151 -0
  271. package/src/components/tree/tree.css +99 -0
  272. package/src/components/tree/tree.js +784 -0
  273. package/src/components/truncate/truncate.css +20 -0
  274. package/src/components/truncate/truncate.js +92 -0
  275. package/src/components/value-list/field-adapter.js +24 -0
  276. package/src/components/value-list/value-list.css +108 -0
  277. package/src/components/value-list/value-list.js +336 -0
  278. package/src/core/breakpoint.js +150 -0
  279. package/src/core/component.js +292 -0
  280. package/src/core/date.js +216 -0
  281. package/src/core/dom.js +260 -0
  282. package/src/core/export.js +204 -0
  283. package/src/core/fontawesome.js +177 -0
  284. package/src/core/format.js +283 -0
  285. package/src/core/http.js +246 -0
  286. package/src/core/i18n.js +83 -0
  287. package/src/core/icons.js +283 -0
  288. package/src/core/keyboard.js +240 -0
  289. package/src/core/position.js +177 -0
  290. package/src/core/storage.js +175 -0
  291. package/src/core/util.js +336 -0
  292. package/src/elements/define.js +715 -0
  293. package/src/elements/reflect.js +79 -0
  294. package/src/index.js +131 -0
  295. package/src/zeyos/connect.js +142 -0
  296. package/src/zeyos/form.js +254 -0
  297. package/src/zeyos/icons.js +92 -0
  298. package/src/zeyos/index.js +21 -0
  299. package/src/zeyos/modules.js +269 -0
  300. package/src/zeyos/query.js +199 -0
  301. package/src/zeyos/schema.js +474 -0
  302. package/src/zeyos/select.js +175 -0
  303. package/src/zeyos/table.js +272 -0
  304. package/styles/base.css +124 -0
  305. package/styles/icons.css +34 -0
  306. package/styles/tokens/dark.css +84 -0
  307. package/styles/tokens/density.css +9 -0
  308. package/styles/tokens/global.css +80 -0
  309. package/styles/tokens/modules.css +109 -0
  310. package/styles/tokens/semantic.css +49 -0
  311. package/styles/zx.css +61 -0
@@ -0,0 +1,406 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+ import { uid } from '../../core/util.js';
5
+
6
+ /** @typedef {{h: number, m: number, s: number}} TimePickerValue */
7
+ /** @typedef {'h'|'m'} ClockUnit */
8
+ /**
9
+ * @typedef {Object} TimePickerOptions
10
+ * @property {TimePickerValue|Date|null} [value=null] Initial local time.
11
+ * @property {boolean} [seconds=false] Show the seconds spinbutton.
12
+ * @property {number} [step=5] Minute increment used by arrow keys.
13
+ * @property {boolean} [clock=true] Offer a clock-face dial behind a toggle button.
14
+ * @property {(event: CustomEvent<{time: TimePickerValue}>) => void} [onchange] Change listener.
15
+ */
16
+ /** @typedef {{silent?: boolean}} TimePickerSetOptions */
17
+ /** @typedef {{value: number, label: string, angle: number, ring: 'outer'|'inner'}} ClockMark */
18
+
19
+ /** Minute marks on the dial, one per 5 minutes — the granularity a clock face can resolve. */
20
+ const MINUTE_STEP = 5;
21
+
22
+ /**
23
+ * Accessible segmented time picker made from spinbutton inputs, with an optional clock face for
24
+ * picking an hour and a minute by eye. The spinbuttons stay the primary control: the dial is a
25
+ * second, visual route to the same value, and every mark is a radio in an APG radio group.
26
+ * @fires TimePicker#change
27
+ * @extends {Component<TimePickerOptions>}
28
+ */
29
+ export class TimePicker extends Component {
30
+ static cssName = 'time-picker';
31
+
32
+ /** @type {TimePickerOptions} */
33
+ static defaults = { value: null, seconds: false, step: 5, clock: true };
34
+
35
+ /** @returns {HTMLElement} */
36
+ render() {
37
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
38
+ this.el = root;
39
+ const initial = normalizeTime(this.options.value);
40
+ this._value = initial;
41
+ this._display = initial ?? { h: 0, m: 0, s: 0 };
42
+ this._inputs = [];
43
+ this._clockOpen = false;
44
+ /** @type {ClockUnit} */
45
+ this._clockUnit = 'h';
46
+ this._clock = null;
47
+
48
+ const content = h('div', {
49
+ class: 'zx-time-picker__segments',
50
+ role: 'group',
51
+ ariaLabel: 'Time'
52
+ });
53
+ this._content = content;
54
+ this._addSpin(content, 'h', 'Hour', 23);
55
+ content.append(h('span', { class: 'zx-time-picker__separator', ariaHidden: 'true' }, ':'));
56
+ this._addSpin(content, 'm', 'Minute', 59);
57
+ if (this.options.seconds) {
58
+ content.append(h('span', { class: 'zx-time-picker__separator', ariaHidden: 'true' }, ':'));
59
+ this._addSpin(content, 's', 'Second', 59);
60
+ }
61
+ root.append(content);
62
+
63
+ if (this.options.clock) {
64
+ const clockId = uid('zx-time-picker-clock');
65
+ content.append(h('button', {
66
+ ref: 'clockToggle',
67
+ class: 'zx-icon-btn zx-time-picker__clock-toggle',
68
+ type: 'button',
69
+ ariaLabel: 'Pick the time on a clock',
70
+ ariaExpanded: 'false',
71
+ ariaControls: clockId
72
+ }, icon('clock', { size: 15 })));
73
+ this._clock = this._buildClock(clockId);
74
+ root.append(this._clock);
75
+ this.listen(this.refs.clockToggle, 'click', () => this.toggleClock());
76
+ }
77
+
78
+ this._writeInputs();
79
+ return root;
80
+ }
81
+
82
+ /**
83
+ * Returns a copy of the selected time, or null when no initial value has been committed.
84
+ * @returns {TimePickerValue|null}
85
+ */
86
+ get() {
87
+ return this._value ? { ...this._value } : null;
88
+ }
89
+
90
+ /**
91
+ * Sets the displayed local time.
92
+ * @param {TimePickerValue|Date|null} value Time object, Date, or null.
93
+ * @param {TimePickerSetOptions} [options={}] Update behavior.
94
+ * @returns {this}
95
+ * @fires TimePicker#change
96
+ */
97
+ set(value, { silent = false } = {}) {
98
+ const normalized = normalizeTime(value);
99
+ this._value = normalized;
100
+ this._display = normalized ?? { h: 0, m: 0, s: 0 };
101
+ this._writeInputs();
102
+ if (!silent && normalized) this.emit('change', { time: { ...normalized } });
103
+ return this;
104
+ }
105
+
106
+ /**
107
+ * Opens the clock face on the given unit.
108
+ * @param {ClockUnit} [unit='h'] Dial to show first.
109
+ * @returns {this}
110
+ */
111
+ openClock(unit = 'h') {
112
+ if (!this._clock) return this;
113
+ this._clockOpen = true;
114
+ this._clockUnit = unit === 'm' ? 'm' : 'h';
115
+ this._syncClock();
116
+ return this;
117
+ }
118
+
119
+ /** Closes the clock face. @returns {this} */
120
+ closeClock() {
121
+ if (!this._clock) return this;
122
+ this._clockOpen = false;
123
+ this._syncClock();
124
+ return this;
125
+ }
126
+
127
+ /** Toggles the clock face. @returns {this} */
128
+ toggleClock() {
129
+ return this._clockOpen ? this.closeClock() : this.openClock('h');
130
+ }
131
+
132
+ /** Removes generated content from an enhanced target. @returns {void} */
133
+ destroy() {
134
+ this._content?.remove();
135
+ this._clock?.remove();
136
+ this._clock = null;
137
+ super.destroy();
138
+ }
139
+
140
+ /** @param {HTMLElement} parent @param {'h'|'m'|'s'} segment @param {string} label @param {number} max @returns {void} */
141
+ _addSpin(parent, segment, label, max) {
142
+ const input = h('input', {
143
+ class: 'zx-time-picker__spin',
144
+ type: 'text',
145
+ role: 'spinbutton',
146
+ inputMode: 'numeric',
147
+ maxLength: 2,
148
+ autocomplete: 'off',
149
+ ariaLabel: label,
150
+ ariaValueMin: '0',
151
+ ariaValueMax: String(max),
152
+ dataset: { segment }
153
+ });
154
+ this._inputs.push(input);
155
+ parent.append(input);
156
+ this.listen(input, 'focus', () => input.select());
157
+ this.listen(input, 'keydown', (event) => this._onKeydown(event, segment, max));
158
+ this.listen(input, 'input', () => this._onInput(input, segment, max));
159
+ this.listen(input, 'blur', () => this._commitFromInputs());
160
+ }
161
+
162
+ /** @param {KeyboardEvent} event @param {'h'|'m'|'s'} segment @param {number} max @returns {void} */
163
+ _onKeydown(event, segment, max) {
164
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
165
+ event.preventDefault();
166
+ const direction = event.key === 'ArrowUp' ? 1 : -1;
167
+ const increment = segment === 'm' ? normalizeStep(this.options.step) : 1;
168
+ const current = numberInRange(event.currentTarget.value, 0, max);
169
+ const modulus = max + 1;
170
+ const value = (current + direction * increment + modulus * increment) % modulus;
171
+ event.currentTarget.value = pad(value);
172
+ this._commitFromInputs();
173
+ }
174
+
175
+ /** @param {HTMLInputElement} input @param {'h'|'m'|'s'} segment @param {number} max @returns {void} */
176
+ _onInput(input, segment, max) {
177
+ input.value = input.value.replace(/\D/g, '').slice(0, 2);
178
+ if (input.value.length < 2) return;
179
+ input.value = pad(numberInRange(input.value, 0, max));
180
+ this._commitFromInputs();
181
+ const index = this._inputs.indexOf(input);
182
+ this._inputs[index + 1]?.focus();
183
+ }
184
+
185
+ /** @returns {void} */
186
+ _commitFromInputs() {
187
+ const next = { h: 0, m: 0, s: 0 };
188
+ for (const input of this._inputs) {
189
+ const segment = input.dataset.segment;
190
+ const max = segment === 'h' ? 23 : 59;
191
+ next[segment] = numberInRange(input.value, 0, max);
192
+ }
193
+ const changed = !this._value || !sameTime(this._value, next);
194
+ this._value = next;
195
+ this._display = next;
196
+ this._writeInputs();
197
+ if (changed) this.emit('change', { time: { ...next } });
198
+ }
199
+
200
+ /** @returns {void} */
201
+ _writeInputs() {
202
+ for (const input of this._inputs) {
203
+ const segment = input.dataset.segment;
204
+ input.value = pad(this._display[segment]);
205
+ input.setAttribute('aria-valuenow', String(this._display[segment]));
206
+ input.setAttribute('aria-valuetext', input.value);
207
+ }
208
+ this._syncClock();
209
+ }
210
+
211
+ /* ------------------------------------------------------------------- clock -- */
212
+
213
+ /**
214
+ * Builds the collapsible clock face: a read-out that doubles as the hour/minute switch, and a
215
+ * dial whose marks are radios.
216
+ * @param {string} id Element id the toggle button controls.
217
+ * @returns {HTMLElement}
218
+ */
219
+ _buildClock(id) {
220
+ const clock = h('div', { class: 'zx-time-picker__clock', id, hidden: true },
221
+ h('div', { class: 'zx-time-picker__clock-head' },
222
+ h('button', {
223
+ ref: 'headHour',
224
+ class: 'zx-time-picker__clock-unit',
225
+ type: 'button',
226
+ ariaLabel: 'Select the hour',
227
+ dataset: { unit: 'h' }
228
+ }),
229
+ h('span', { class: 'zx-time-picker__clock-colon', ariaHidden: 'true' }, ':'),
230
+ h('button', {
231
+ ref: 'headMinute',
232
+ class: 'zx-time-picker__clock-unit',
233
+ type: 'button',
234
+ ariaLabel: 'Select the minutes',
235
+ dataset: { unit: 'm' }
236
+ })),
237
+ h('div', { ref: 'dial', class: 'zx-time-picker__dial', role: 'radiogroup' },
238
+ h('span', { ref: 'hand', class: 'zx-time-picker__hand', ariaHidden: 'true' }),
239
+ h('span', { class: 'zx-time-picker__pivot', ariaHidden: 'true' })));
240
+
241
+ this.listen(this.refs.headHour, 'click', () => this.openClock('h'));
242
+ this.listen(this.refs.headMinute, 'click', () => this.openClock('m'));
243
+ this.listen(this.refs.dial, 'click', (event) => {
244
+ const mark = event.target.closest?.('.zx-time-picker__mark');
245
+ if (mark) this._pick(Number(mark.dataset.value));
246
+ });
247
+ this.listen(this.refs.dial, 'keydown', (event) => this._onDialKeydown(event));
248
+ return clock;
249
+ }
250
+
251
+ /**
252
+ * Applies a mark's value to the active unit, then moves on to the minutes so a whole time can
253
+ * be set with two clicks.
254
+ * @param {number} value Hour or minute the mark stands for.
255
+ * @returns {void}
256
+ */
257
+ _pick(value) {
258
+ const next = { ...this._display, [this._clockUnit]: value };
259
+ const advance = this._clockUnit === 'h';
260
+ this.set(next);
261
+ if (advance) this.openClock('m');
262
+ }
263
+
264
+ /** @param {KeyboardEvent} event @returns {void} */
265
+ _onDialKeydown(event) {
266
+ const marks = [...this.refs.dial.querySelectorAll('.zx-time-picker__mark')];
267
+ const current = event.target.closest?.('.zx-time-picker__mark');
268
+ const index = marks.indexOf(current);
269
+ if (index < 0) return;
270
+ // Marks are laid out clockwise within a ring, so Right/Down step forward and Left/Up back.
271
+ const offset = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key];
272
+ let next = null;
273
+ if (offset !== undefined) next = (index + offset + marks.length) % marks.length;
274
+ else if (event.key === 'Home') next = 0;
275
+ else if (event.key === 'End') next = marks.length - 1;
276
+ if (next === null) return;
277
+ event.preventDefault();
278
+ // Selection follows focus, the APG behaviour for a radio group.
279
+ this._pick(Number(marks[next].dataset.value));
280
+ this.refs.dial.querySelector(`[data-value="${marks[next].dataset.value}"]`)?.focus();
281
+ }
282
+
283
+ /** Reflects the open state, the active unit, the marks, and the hand. @returns {void} */
284
+ _syncClock() {
285
+ if (!this._clock) return;
286
+ const hours = this._clockUnit === 'h';
287
+ this._clock.hidden = !this._clockOpen;
288
+ this.refs.clockToggle.setAttribute('aria-expanded', String(this._clockOpen));
289
+ this.refs.headHour.textContent = pad(this._display.h);
290
+ this.refs.headMinute.textContent = pad(this._display.m);
291
+ this.refs.headHour.setAttribute('aria-pressed', String(hours));
292
+ this.refs.headMinute.setAttribute('aria-pressed', String(!hours));
293
+ this.refs.dial.setAttribute('aria-label', hours ? 'Hour' : 'Minutes');
294
+ if (!this._clockOpen) return;
295
+
296
+ const selected = hours ? this._display.h : this._display.m;
297
+ const marks = hours ? hourMarks() : minuteMarks();
298
+ const rendered = this.refs.dial.querySelectorAll('.zx-time-picker__mark');
299
+ // Rebuild only when the dial switches unit; otherwise just restate which mark is checked.
300
+ if (rendered.length !== marks.length || this.refs.dial.dataset.unit !== this._clockUnit) {
301
+ for (const mark of rendered) mark.remove();
302
+ this.refs.dial.dataset.unit = this._clockUnit;
303
+ this.refs.dial.append(...marks.map((mark) => this._buildMark(mark, selected)));
304
+ } else {
305
+ marks.forEach((mark, index) => this._syncMark(rendered[index], mark, selected));
306
+ }
307
+
308
+ const ring = hours && !isOuterHour(selected) ? 'inner' : 'outer';
309
+ const angle = hours ? (selected % 12) * 30 : selected * 6;
310
+ this.refs.hand.dataset.ring = ring;
311
+ this.refs.hand.style.setProperty('--zx-clock-angle', `${angle}deg`);
312
+ }
313
+
314
+ /** @param {ClockMark} mark @param {number} selected @returns {HTMLElement} */
315
+ _buildMark(mark, selected) {
316
+ const element = h('button', {
317
+ class: 'zx-time-picker__mark',
318
+ type: 'button',
319
+ role: 'radio',
320
+ dataset: { value: String(mark.value), ring: mark.ring }
321
+ }, mark.label);
322
+ element.style.setProperty('--zx-clock-angle', `${mark.angle}deg`);
323
+ this._syncMark(element, mark, selected);
324
+ return element;
325
+ }
326
+
327
+ /** @param {HTMLElement} element @param {ClockMark} mark @param {number} selected @returns {void} */
328
+ _syncMark(element, mark, selected) {
329
+ const checked = mark.value === selected;
330
+ element.setAttribute('aria-checked', String(checked));
331
+ element.tabIndex = checked ? 0 : -1;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Fired after a time is committed.
337
+ * @event TimePicker#change
338
+ * @type {CustomEvent<{time: TimePickerValue}>}
339
+ */
340
+
341
+ /**
342
+ * Hour marks: 1–12 on the outer ring, 13–23 and 00 on the inner one, so all 24 hours are
343
+ * reachable without an AM/PM switch.
344
+ * @returns {ClockMark[]}
345
+ */
346
+ function hourMarks() {
347
+ const marks = [];
348
+ for (let index = 1; index <= 12; index += 1) {
349
+ marks.push({ value: index, label: String(index), angle: (index % 12) * 30, ring: 'outer' });
350
+ }
351
+ for (let index = 0; index < 12; index += 1) {
352
+ const value = index === 0 ? 0 : 12 + index;
353
+ marks.push({ value, label: pad(value), angle: index * 30, ring: 'inner' });
354
+ }
355
+ return marks;
356
+ }
357
+
358
+ /** @returns {ClockMark[]} */
359
+ function minuteMarks() {
360
+ const marks = [];
361
+ for (let value = 0; value < 60; value += MINUTE_STEP) {
362
+ marks.push({ value, label: pad(value), angle: value * 6, ring: 'outer' });
363
+ }
364
+ return marks;
365
+ }
366
+
367
+ /** @param {number} hour @returns {boolean} */
368
+ function isOuterHour(hour) {
369
+ return hour >= 1 && hour <= 12;
370
+ }
371
+
372
+ /** @param {unknown} value @returns {TimePickerValue|null} */
373
+ function normalizeTime(value) {
374
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
375
+ return { h: value.getHours(), m: value.getMinutes(), s: value.getSeconds() };
376
+ }
377
+ if (!value || typeof value !== 'object') return null;
378
+ return {
379
+ h: numberInRange(value.h, 0, 23),
380
+ m: numberInRange(value.m, 0, 59),
381
+ s: numberInRange(value.s, 0, 59)
382
+ };
383
+ }
384
+
385
+ /** @param {unknown} value @param {number} min @param {number} max @returns {number} */
386
+ function numberInRange(value, min, max) {
387
+ const number = Number.parseInt(String(value ?? ''), 10);
388
+ if (!Number.isFinite(number)) return min;
389
+ return Math.max(min, Math.min(max, number));
390
+ }
391
+
392
+ /** @param {unknown} value @returns {number} */
393
+ function normalizeStep(value) {
394
+ const number = Math.round(Number(value));
395
+ return Number.isFinite(number) && number > 0 && number <= 59 ? number : 5;
396
+ }
397
+
398
+ /** @param {number} value @returns {string} */
399
+ function pad(value) {
400
+ return String(value).padStart(2, '0');
401
+ }
402
+
403
+ /** @param {TimePickerValue} a @param {TimePickerValue} b @returns {boolean} */
404
+ function sameTime(a, b) {
405
+ return a.h === b.h && a.m === b.m && a.s === b.s;
406
+ }
@@ -0,0 +1,128 @@
1
+ .zx-date-range-box {
2
+ display: inline-block;
3
+ color: var(--zx-color-text);
4
+ font-size: var(--zx-control-font-size);
5
+ }
6
+
7
+ .zx-date-range-box__content,
8
+ .zx-date-range-box__control {
9
+ display: inline-flex;
10
+ align-items: stretch;
11
+ }
12
+
13
+ .zx-date-range-box__control {
14
+ position: relative;
15
+ border: 1px solid var(--zx-color-border-control);
16
+ border-radius: var(--zx-control-radius);
17
+ background: var(--zx-color-bg-control);
18
+ }
19
+
20
+ .zx-date-range-box__control:focus-within {
21
+ box-shadow: var(--zx-focus-ring);
22
+ }
23
+
24
+ .zx-date-range-box[data-state="invalid"] .zx-date-range-box__control {
25
+ border-color: var(--zx-color-danger);
26
+ background: var(--zx-color-danger-bg);
27
+ }
28
+
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. */
31
+ .zx-date-range-box__input {
32
+ min-inline-size: 16em;
33
+ border: 0;
34
+ border-radius: var(--zx-control-radius) 0 0 var(--zx-control-radius);
35
+ background: inherit;
36
+ padding-inline-end: var(--zx-control-height);
37
+ }
38
+
39
+ /* Focused controls lift above their siblings so the ring is never clipped. */
40
+ .zx-date-range-box__input:focus-visible,
41
+ .zx-date-range-box__toggle:focus-visible {
42
+ position: relative;
43
+ z-index: 1;
44
+ box-shadow: var(--zx-focus-ring);
45
+ }
46
+
47
+ .zx-date-range-box__clear:focus-visible {
48
+ box-shadow: var(--zx-focus-ring);
49
+ }
50
+
51
+ .zx-date-range-box__clear,
52
+ .zx-date-range-box__toggle {
53
+ display: inline-grid;
54
+ place-items: center;
55
+ min-inline-size: var(--zx-control-height);
56
+ padding: 0;
57
+ border: 0;
58
+ border-radius: 0;
59
+ background: inherit;
60
+ color: var(--zx-color-text-muted);
61
+ }
62
+
63
+ .zx-date-range-box__toggle {
64
+ border-inline-start: 1px solid var(--zx-color-border-control);
65
+ border-radius: 0 var(--zx-control-radius) var(--zx-control-radius) 0;
66
+ }
67
+
68
+ .zx-date-range-box__clear {
69
+ position: absolute;
70
+ /* The clear button floats over the input's trailing padding, so it has to outrank the input in
71
+ the stacking order — a focused input raises itself to z-index 1 and would otherwise cover the
72
+ button and swallow its clicks. */
73
+ z-index: 2;
74
+ inset-block-start: 50%;
75
+ /* The picker toggle sits to the right of the input; float the clear button just inside it. */
76
+ inset-inline-end: var(--zx-control-height);
77
+ transform: translateY(-50%);
78
+ background: none;
79
+ }
80
+
81
+ .zx-date-range-box__clear:hover {
82
+ color: var(--zx-color-text);
83
+ }
84
+
85
+ .zx-date-range-box__clear[hidden] {
86
+ display: none;
87
+ }
88
+
89
+ .zx-date-range-box__popover {
90
+ overflow: visible;
91
+ max-inline-size: calc(100vw - var(--zx-space-4));
92
+ border: 0;
93
+ /* The picker's own surface paints the raised background; the popover box only positions it. */
94
+ background: none;
95
+ padding: 0;
96
+ }
97
+
98
+ .zx-date-range-box__popover:not(:popover-open)[data-state="closed"] {
99
+ display: none;
100
+ }
101
+
102
+ .zx-date-range-box__popover[data-state="open"] {
103
+ display: block;
104
+ }
105
+
106
+ /* The picker is a size query container, so it takes no width from its own contents — inside a
107
+ shrink-to-fit popover it needs an explicit one, sized just past the two-month breakpoint. */
108
+ .zx-date-range-box__popover .zx-date-range-picker {
109
+ inline-size: min(calc(100vw - var(--zx-space-4)), 33rem);
110
+ }
111
+
112
+ .zx-date-range-box__popover .zx-date-range-picker[data-presets="true"] {
113
+ inline-size: min(calc(100vw - var(--zx-space-4)), 43rem);
114
+ }
115
+
116
+ .zx-date-range-box__popover .zx-date-range-picker[data-months="1"] {
117
+ inline-size: min(calc(100vw - var(--zx-space-4)), 18rem);
118
+ }
119
+
120
+ .zx-date-range-box__popover .zx-date-range-picker[data-months="1"][data-presets="true"] {
121
+ inline-size: min(calc(100vw - var(--zx-space-4)), 27rem);
122
+ }
123
+
124
+ .zx-date-range-box__popover .zx-date-range-picker__surface {
125
+ max-inline-size: none;
126
+ background: var(--zx-color-bg-raised);
127
+ box-shadow: var(--zx-overlay-shadow);
128
+ }