@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,24 @@
1
+ import { Field } from '../field/field.js';
2
+ import { Timebox } from './timebox.js';
3
+
4
+ /**
5
+ * Registers the duration-oriented `time` field type.
6
+ * @returns {void}
7
+ */
8
+ export function registerTimeboxFieldAdapter() {
9
+ Field.register('time', (field, options) => {
10
+ const timebox = field.own(new Timebox(null, {
11
+ ...(options.props ?? {}),
12
+ value: options.value
13
+ }));
14
+ field.listen(timebox.el, 'change', (event) => event.stopImmediatePropagation());
15
+ timebox.on('change', (event) => field.emit('change', { value: event.detail.value }));
16
+ return {
17
+ el: timebox.el,
18
+ get: () => timebox.get(),
19
+ set: (value) => timebox.set(Number(value) || 0, { silent: true }),
20
+ focus: () => timebox.el.querySelector('input:not([disabled]), button:not([disabled])')?.focus(),
21
+ setDisabled: (disabled) => disabled ? timebox.disable() : timebox.enable()
22
+ };
23
+ });
24
+ }
@@ -0,0 +1,51 @@
1
+ .zx-timebox {
2
+ display: inline-block;
3
+ color: var(--zx-color-text);
4
+ font-size: var(--zx-control-font-size);
5
+ }
6
+
7
+ .zx-timebox__content {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ gap: var(--zx-space-1);
11
+ }
12
+
13
+ .zx-timebox__segment {
14
+ inline-size: calc(var(--zx-control-height) + var(--zx-space-2));
15
+ padding-inline: var(--zx-space-1);
16
+ text-align: center;
17
+ font-variant-numeric: tabular-nums;
18
+ }
19
+
20
+ .zx-timebox__segment[data-segment="hours"] {
21
+ inline-size: calc(var(--zx-control-height) + var(--zx-space-4));
22
+ }
23
+
24
+ .zx-timebox__segment:focus-visible,
25
+ .zx-timebox__sign:focus-visible {
26
+ box-shadow: var(--zx-focus-ring);
27
+ }
28
+
29
+ .zx-timebox__separator {
30
+ color: var(--zx-color-text-muted);
31
+ font-weight: 700;
32
+ }
33
+
34
+ .zx-timebox__sign {
35
+ min-inline-size: var(--zx-control-height);
36
+ padding: 0;
37
+ border-color: var(--zx-color-border-control);
38
+ font-weight: 750;
39
+ }
40
+
41
+ .zx-timebox__sign[data-state="positive"] {
42
+ border-color: var(--zx-color-success);
43
+ background: var(--zx-color-success-bg);
44
+ color: var(--zx-color-success);
45
+ }
46
+
47
+ .zx-timebox__sign[data-state="negative"] {
48
+ border-color: var(--zx-color-danger);
49
+ background: var(--zx-color-danger-bg);
50
+ color: var(--zx-color-danger);
51
+ }
@@ -0,0 +1,276 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+
4
+ /** @typedef {'minutes'|'seconds'|'hours'} TimeUnit */
5
+ /** @typedef {{hours: number, minutes: number, seconds: number, negative: boolean}} TimeParts */
6
+ /**
7
+ * @typedef {Object} TimeboxOptions
8
+ * @property {number} [value=0] Initial duration in `unit`.
9
+ * @property {TimeUnit} [unit='minutes'] Default input and output unit.
10
+ * @property {boolean} [seconds=false] Show a seconds segment.
11
+ * @property {boolean} [signed=false] Show a positive/negative toggle.
12
+ * @property {boolean} [disabled=false] Disable all controls.
13
+ * @property {(event: CustomEvent<{value: number}>) => void} [onchange] Change listener.
14
+ */
15
+ /** @typedef {{silent?: boolean}} TimeboxSetOptions */
16
+
17
+ /**
18
+ * Splits a duration into unlimited hours and normalized minute/second segments.
19
+ * @param {number} value Duration value.
20
+ * @param {TimeUnit} [unit='minutes'] Unit of `value`.
21
+ * @returns {TimeParts}
22
+ */
23
+ export function splitTime(value, unit = 'minutes') {
24
+ const seconds = Math.round(Math.abs(toSeconds(finiteNumber(value), unit)));
25
+ return {
26
+ hours: Math.floor(seconds / 3600),
27
+ minutes: Math.floor(seconds / 60) % 60,
28
+ seconds: seconds % 60,
29
+ negative: finiteNumber(value) < 0
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Joins duration segments and converts them to the requested unit.
35
+ * Minute and second overflow is carried into the unlimited hours value.
36
+ * @param {Partial<TimeParts>} parts Duration segments.
37
+ * @param {TimeUnit} [unit='minutes'] Unit of the returned number.
38
+ * @returns {number}
39
+ */
40
+ export function joinTime(parts, unit = 'minutes') {
41
+ assertUnit(unit);
42
+ const hours = Math.abs(Math.trunc(finiteNumber(parts?.hours)));
43
+ const minutes = Math.abs(Math.trunc(finiteNumber(parts?.minutes)));
44
+ const seconds = Math.abs(Math.trunc(finiteNumber(parts?.seconds)));
45
+ const sign = parts?.negative ? -1 : 1;
46
+ return fromSeconds(sign * (hours * 3600 + minutes * 60 + seconds), unit);
47
+ }
48
+
49
+ /**
50
+ * Segmented duration input supporting unlimited and signed hours.
51
+ * @fires Timebox#change
52
+ * @extends {Component<TimeboxOptions>}
53
+ */
54
+ export class Timebox extends Component {
55
+ static cssName = 'timebox';
56
+
57
+ /** @type {TimeboxOptions} */
58
+ static defaults = {
59
+ value: 0,
60
+ unit: 'minutes',
61
+ seconds: false,
62
+ signed: false,
63
+ disabled: false
64
+ };
65
+
66
+ /** @returns {HTMLElement} */
67
+ render() {
68
+ assertUnit(this.options.unit);
69
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
70
+ this.el = root;
71
+ this._originalAriaDisabled = root.getAttribute('aria-disabled');
72
+ this._disabled = Boolean(this.options.disabled);
73
+ this._parts = { hours: 0, minutes: 0, seconds: 0, negative: false };
74
+ this._inputs = [];
75
+ const content = h('div', {
76
+ class: 'zx-timebox__content',
77
+ role: 'group',
78
+ ariaLabel: this.options.seconds ? 'Duration in hours, minutes, and seconds' : 'Duration in hours and minutes'
79
+ });
80
+ this._content = content;
81
+
82
+ if (this.options.signed) {
83
+ content.append(h('button', {
84
+ ref: 'sign',
85
+ class: 'zx-timebox__sign',
86
+ type: 'button',
87
+ ariaLabel: 'Toggle duration sign',
88
+ ariaPressed: 'false'
89
+ }, '+'));
90
+ this.listen(this.refs.sign, 'click', () => {
91
+ if (this._disabled) return;
92
+ this._parts.negative = !this._parts.negative;
93
+ this._writeParts();
94
+ this._emitChange();
95
+ });
96
+ }
97
+
98
+ this._addSegment(content, 'hours', 'Hours', false);
99
+ content.append(h('span', { class: 'zx-timebox__separator', ariaHidden: 'true' }, ':'));
100
+ this._addSegment(content, 'minutes', 'Minutes', true);
101
+ if (this.options.seconds) {
102
+ content.append(h('span', { class: 'zx-timebox__separator', ariaHidden: 'true' }, ':'));
103
+ this._addSegment(content, 'seconds', 'Seconds', true);
104
+ }
105
+ root.append(content);
106
+ this.set(this.options.value, this.options.unit, { silent: true });
107
+ this._syncDisabled();
108
+ return root;
109
+ }
110
+
111
+ /**
112
+ * Returns the current duration.
113
+ * @param {TimeUnit} [unit=this.options.unit] Output unit.
114
+ * @returns {number}
115
+ */
116
+ get(unit = this.options.unit) {
117
+ return joinTime(this._readParts(), unit);
118
+ }
119
+
120
+ /**
121
+ * Sets the current duration.
122
+ * @param {number} value Duration value.
123
+ * @param {TimeUnit|TimeboxSetOptions} [unit=this.options.unit] Input unit, or options when omitted.
124
+ * @param {TimeboxSetOptions} [options={}] Update behavior.
125
+ * @returns {this}
126
+ * @fires Timebox#change
127
+ */
128
+ set(value, unit = this.options.unit, options = {}) {
129
+ if (unit && typeof unit === 'object') {
130
+ options = unit;
131
+ unit = this.options.unit;
132
+ }
133
+ assertUnit(unit);
134
+ let numeric = finiteNumber(value);
135
+ if (!this.options.signed && numeric < 0) numeric = 0;
136
+ if (!this.options.seconds) {
137
+ numeric = fromSeconds(Math.round(toSeconds(numeric, unit) / 60) * 60, unit);
138
+ }
139
+ this._parts = splitTime(numeric, unit);
140
+ if (!this.options.signed) this._parts.negative = false;
141
+ this._writeParts();
142
+ if (!options.silent) this._emitChange();
143
+ return this;
144
+ }
145
+
146
+ /** Enables the timebox. @returns {this} */
147
+ enable() {
148
+ this._disabled = false;
149
+ this._syncDisabled();
150
+ return this;
151
+ }
152
+
153
+ /** Disables the timebox. @returns {this} */
154
+ disable() {
155
+ this._disabled = true;
156
+ this._syncDisabled();
157
+ return this;
158
+ }
159
+
160
+ /** Removes generated content from an enhanced target. @returns {void} */
161
+ destroy() {
162
+ this._content?.remove();
163
+ if (this._originalAriaDisabled === null) this.el.removeAttribute('aria-disabled');
164
+ else this.el.setAttribute('aria-disabled', this._originalAriaDisabled);
165
+ super.destroy();
166
+ }
167
+
168
+ /** @param {HTMLElement} parent @param {'hours'|'minutes'|'seconds'} name @param {string} label @param {boolean} bounded @returns {void} */
169
+ _addSegment(parent, name, label, bounded) {
170
+ const input = h('input', {
171
+ class: 'zx-timebox__segment',
172
+ type: 'text',
173
+ inputMode: 'numeric',
174
+ autocomplete: 'off',
175
+ ariaLabel: label,
176
+ maxLength: bounded ? 2 : null,
177
+ dataset: { segment: name, bounded: String(bounded) }
178
+ });
179
+ this._inputs.push(input);
180
+ parent.append(input);
181
+ this.listen(input, 'focus', () => input.select());
182
+ this.listen(input, 'input', () => {
183
+ if (name === 'hours' && this.options.signed && input.value.trim().startsWith('-')) {
184
+ this._parts.negative = true;
185
+ }
186
+ const digits = input.value.replace(/\D/g, '');
187
+ input.value = bounded ? digits.slice(0, 2) : digits;
188
+ });
189
+ this.listen(input, 'change', () => this._commitSegments());
190
+ this.listen(input, 'keydown', (event) => {
191
+ if (event.key !== 'Enter') return;
192
+ event.preventDefault();
193
+ this._commitSegments();
194
+ input.select();
195
+ });
196
+ }
197
+
198
+ /** @returns {TimeParts} */
199
+ _readParts() {
200
+ const parts = { ...this._parts };
201
+ for (const input of this._inputs) {
202
+ const name = input.dataset.segment;
203
+ const maximum = input.dataset.bounded === 'true' ? 59 : Number.MAX_SAFE_INTEGER;
204
+ parts[name] = Math.max(0, Math.min(maximum, Math.trunc(finiteNumber(input.value))));
205
+ }
206
+ return parts;
207
+ }
208
+
209
+ /** @returns {void} */
210
+ _commitSegments() {
211
+ this._parts = this._readParts();
212
+ this._writeParts();
213
+ this._emitChange();
214
+ }
215
+
216
+ /** @returns {void} */
217
+ _writeParts() {
218
+ for (const input of this._inputs) {
219
+ const name = input.dataset.segment;
220
+ input.value = name === 'hours' ? String(this._parts[name]) : pad(this._parts[name]);
221
+ }
222
+ if (this.refs.sign) {
223
+ this.refs.sign.textContent = this._parts.negative ? '-' : '+';
224
+ this.refs.sign.setAttribute('aria-pressed', String(this._parts.negative));
225
+ this.refs.sign.dataset.state = this._parts.negative ? 'negative' : 'positive';
226
+ }
227
+ }
228
+
229
+ /** @returns {void} */
230
+ _syncDisabled() {
231
+ for (const input of this._inputs) input.disabled = this._disabled;
232
+ if (this.refs.sign) this.refs.sign.disabled = this._disabled;
233
+ this.el.setAttribute('aria-disabled', String(this._disabled));
234
+ }
235
+
236
+ /** @returns {void} */
237
+ _emitChange() {
238
+ this.emit('change', { value: this.get(this.options.unit) });
239
+ }
240
+ }
241
+
242
+ /** @event Timebox#change @type {CustomEvent<{value: number}>} */
243
+
244
+ /** @param {unknown} value @returns {number} */
245
+ function finiteNumber(value) {
246
+ const number = Number(value);
247
+ return Number.isFinite(number) ? number : 0;
248
+ }
249
+
250
+ /** @param {number} value @param {TimeUnit} unit @returns {number} */
251
+ function toSeconds(value, unit) {
252
+ assertUnit(unit);
253
+ if (unit === 'hours') return value * 3600;
254
+ if (unit === 'minutes') return value * 60;
255
+ return value;
256
+ }
257
+
258
+ /** @param {number} value @param {TimeUnit} unit @returns {number} */
259
+ function fromSeconds(value, unit) {
260
+ assertUnit(unit);
261
+ if (unit === 'hours') return value / 3600;
262
+ if (unit === 'minutes') return value / 60;
263
+ return value;
264
+ }
265
+
266
+ /** @param {unknown} unit @returns {asserts unit is TimeUnit} */
267
+ function assertUnit(unit) {
268
+ if (unit !== 'hours' && unit !== 'minutes' && unit !== 'seconds') {
269
+ throw new RangeError(`Unknown time unit: ${unit}`);
270
+ }
271
+ }
272
+
273
+ /** @param {number} value @returns {string} */
274
+ function pad(value) {
275
+ return String(value).padStart(2, '0');
276
+ }
@@ -0,0 +1,24 @@
1
+ import { Field } from '../field/field.js';
2
+ import { Toggle } from './toggle.js';
3
+
4
+ /**
5
+ * Registers the boolean `toggle` field type.
6
+ * @returns {void}
7
+ */
8
+ export function registerToggleFieldAdapter() {
9
+ Field.register('toggle', (field, options) => {
10
+ const toggle = field.own(new Toggle(null, {
11
+ ...(options.props ?? {}),
12
+ checked: Boolean(options.value)
13
+ }));
14
+ field.listen(toggle.el, 'change', (event) => event.stopImmediatePropagation());
15
+ toggle.on('change', (event) => field.emit('change', { value: event.detail.value }));
16
+ return {
17
+ el: toggle.el,
18
+ get: () => toggle.getValue(),
19
+ set: (value) => toggle.set(Boolean(value), { silent: true }),
20
+ focus: () => toggle.el.focus(),
21
+ setDisabled: (disabled) => disabled ? toggle.disable() : toggle.enable()
22
+ };
23
+ });
24
+ }
@@ -0,0 +1,66 @@
1
+ .zx-toggle {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--zx-space-2);
5
+ min-block-size: var(--zx-control-height);
6
+ border: 0;
7
+ background: none;
8
+ color: var(--zx-color-text);
9
+ padding: 0;
10
+ font: inherit;
11
+ font-size: var(--zx-control-font-size);
12
+ cursor: pointer;
13
+ }
14
+
15
+ .zx-toggle:hover:not(:disabled) {
16
+ background: none;
17
+ }
18
+
19
+ .zx-toggle:focus-visible {
20
+ border-radius: var(--zx-control-radius);
21
+ box-shadow: var(--zx-focus-ring);
22
+ }
23
+
24
+ .zx-toggle:disabled {
25
+ opacity: 0.55;
26
+ cursor: not-allowed;
27
+ }
28
+
29
+ .zx-toggle__track {
30
+ display: block;
31
+ flex: none;
32
+ inline-size: 34px;
33
+ block-size: 20px;
34
+ border: 1px solid var(--zx-color-border-strong);
35
+ border-radius: var(--zx-radius-full);
36
+ background: var(--zx-color-bg-control);
37
+ padding: 2px;
38
+ }
39
+
40
+ .zx-toggle__thumb {
41
+ display: block;
42
+ inline-size: 14px;
43
+ block-size: 14px;
44
+ border-radius: var(--zx-radius-full);
45
+ background: var(--zx-color-text-muted);
46
+ }
47
+
48
+ .zx-toggle[aria-checked="true"] .zx-toggle__track {
49
+ border-color: var(--zx-color-accent);
50
+ background: var(--zx-color-accent);
51
+ }
52
+
53
+ .zx-toggle[aria-checked="true"] .zx-toggle__thumb {
54
+ background: var(--zx-color-on-accent);
55
+ transform: translateX(14px);
56
+ }
57
+
58
+ @media (prefers-reduced-motion: no-preference) {
59
+ .zx-toggle__track,
60
+ .zx-toggle__thumb {
61
+ transition:
62
+ background-color var(--zx-dur-fast) var(--zx-ease),
63
+ border-color var(--zx-dur-fast) var(--zx-ease),
64
+ transform var(--zx-dur-base) var(--zx-ease);
65
+ }
66
+ }
@@ -0,0 +1,150 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+
4
+ /**
5
+ * @typedef {Object} ToggleOptions
6
+ * @property {boolean} [checked=false] Initial switch state.
7
+ * @property {*} [value=true] Value returned while on.
8
+ * @property {string|null} [label=null] Optional visible label.
9
+ * @property {boolean} [disabled=false] Initial disabled state.
10
+ * @property {(event: CustomEvent<{checked: boolean, value: *}>) => void} [onchange] Change callback.
11
+ */
12
+
13
+ /**
14
+ * Accessible on/off switch.
15
+ * @fires Toggle#change
16
+ * @extends {Component<ToggleOptions>}
17
+ */
18
+ export class Toggle extends Component {
19
+ static cssName = 'toggle';
20
+
21
+ /** @type {Readonly<ToggleOptions>} */
22
+ static defaults = {
23
+ checked: false,
24
+ value: true,
25
+ label: null,
26
+ disabled: false
27
+ };
28
+
29
+ /** @returns {HTMLButtonElement} */
30
+ render() {
31
+ const created = !this.el;
32
+ const root = /** @type {HTMLButtonElement} */ (this.el ?? h('button'));
33
+ this.el = root;
34
+ if (root.localName !== 'button') throw new TypeError('Toggle target must be a button element');
35
+
36
+ this._createdRoot = created;
37
+ this._original = created ? null : snapshot(root);
38
+ this._checked = Boolean(this.options.checked);
39
+ this._disabled = Boolean(this.options.disabled);
40
+ root.type = 'button';
41
+ root.setAttribute('role', 'switch');
42
+ const children = [
43
+ h('span', { class: 'zx-toggle__track', ariaHidden: 'true' },
44
+ h('span', { class: 'zx-toggle__thumb' }))
45
+ ];
46
+ if (this.options.label !== null) {
47
+ children.push(h('span', { class: 'zx-toggle__label' }, String(this.options.label)));
48
+ }
49
+ root.replaceChildren(...children);
50
+ this._sync();
51
+ this.listen(root, 'click', () => this.toggle());
52
+ return root;
53
+ }
54
+
55
+ /**
56
+ * Returns the current switch state.
57
+ * @returns {boolean}
58
+ */
59
+ get() {
60
+ return this._checked;
61
+ }
62
+
63
+ /**
64
+ * Sets the switch state.
65
+ * @param {boolean} checked Next state.
66
+ * @param {{silent?: boolean}} [options={}] Set `silent` to suppress `change`.
67
+ * @returns {this}
68
+ * @fires Toggle#change
69
+ */
70
+ set(checked, { silent = false } = {}) {
71
+ const next = Boolean(checked);
72
+ if (next === this._checked) return this;
73
+ this._checked = next;
74
+ this._sync();
75
+ if (!silent) this.emit('change', { checked: this._checked, value: this.getValue() });
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Toggles the switch state.
81
+ * @returns {this}
82
+ * @fires Toggle#change
83
+ */
84
+ toggle() {
85
+ return this.set(!this._checked);
86
+ }
87
+
88
+ /**
89
+ * Returns the configured value while on, otherwise `false`.
90
+ * @returns {*}
91
+ */
92
+ getValue() {
93
+ return this._checked ? this.options.value : false;
94
+ }
95
+
96
+ /**
97
+ * Enables user interaction.
98
+ * @returns {this}
99
+ */
100
+ enable() {
101
+ this._disabled = false;
102
+ this.el.removeAttribute('disabled');
103
+ return this;
104
+ }
105
+
106
+ /**
107
+ * Disables user interaction.
108
+ * @returns {this}
109
+ */
110
+ disable() {
111
+ this._disabled = true;
112
+ this.el.setAttribute('disabled', '');
113
+ return this;
114
+ }
115
+
116
+ /** @returns {void} */
117
+ destroy() {
118
+ if (this._cleaned) return;
119
+ this._cleaned = true;
120
+ super.destroy();
121
+ if (!this._createdRoot && this._original) restore(this.el, this._original);
122
+ }
123
+
124
+ /** @returns {void} */
125
+ _sync() {
126
+ this.el.setAttribute('aria-checked', String(this._checked));
127
+ this.el.toggleAttribute('disabled', this._disabled);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Change emitted after a real state transition.
133
+ * @event Toggle#change
134
+ * @type {CustomEvent<{checked: boolean, value: *}>}
135
+ */
136
+
137
+ /** @param {Element} element @returns {{attributes: [string, string][], children: Node[]}} */
138
+ function snapshot(element) {
139
+ return {
140
+ attributes: Array.from(element.attributes, (attribute) => [attribute.name, attribute.value]),
141
+ children: Array.from(element.childNodes)
142
+ };
143
+ }
144
+
145
+ /** @param {Element} element @param {{attributes: [string, string][], children: Node[]}} state */
146
+ function restore(element, state) {
147
+ for (const attribute of Array.from(element.attributes)) element.removeAttribute(attribute.name);
148
+ for (const [name, value] of state.attributes) element.setAttribute(name, value);
149
+ element.replaceChildren(...state.children);
150
+ }
@@ -0,0 +1,81 @@
1
+ /*
2
+ * A single, non-wrapping row of controls. It never wraps on purpose: the component measures the
3
+ * row and moves what no longer fits into the trailing overflow menu, which is the wrapping-free
4
+ * replacement for the hand-rolled `.layout-toolbar` row in the website.
5
+ */
6
+ .zx-toolbar {
7
+ display: flex;
8
+ flex-wrap: nowrap;
9
+ min-inline-size: 0;
10
+ align-items: center;
11
+ gap: var(--zx-space-2);
12
+ }
13
+
14
+ /* Items keep their intrinsic width so the measure pass can decide what fits — squashed controls
15
+ would hide the overflow instead of reporting it. */
16
+ .zx-toolbar > * {
17
+ flex: 0 0 auto;
18
+ }
19
+
20
+ /* Pushes everything after it to the trailing edge (the `.layout-toolbar__spacer` role). */
21
+ .zx-toolbar > .zx-toolbar__spacer {
22
+ flex: 1 1 auto;
23
+ min-inline-size: 0;
24
+ }
25
+
26
+ /* Collapsed items and the empty overflow trigger. The attribute selector outranks the `display`
27
+ declarations component CSS puts on buttons, which would otherwise defeat `hidden`. */
28
+ .zx-toolbar > [hidden] {
29
+ display: none;
30
+ }
31
+
32
+ /* Only the collapsing toolbar clips: between a resize and the next measure pass a control can
33
+ momentarily stick out, and the clip margin keeps focus rings visible. */
34
+ .zx-toolbar[data-overflow="true"] {
35
+ overflow: clip;
36
+ overflow-clip-margin: var(--zx-space-1);
37
+ }
38
+
39
+ .zx-toolbar[data-align="start"] {
40
+ justify-content: flex-start;
41
+ }
42
+
43
+ .zx-toolbar[data-align="end"] {
44
+ justify-content: flex-end;
45
+ }
46
+
47
+ .zx-toolbar[data-align="between"] {
48
+ justify-content: space-between;
49
+ }
50
+
51
+ .zx-toolbar[data-dense="true"] {
52
+ gap: var(--zx-space-1);
53
+ }
54
+
55
+ .zx-toolbar__separator,
56
+ .zx-toolbar > [data-separator] {
57
+ flex: none;
58
+ align-self: stretch;
59
+ inline-size: 1px;
60
+ min-block-size: calc(var(--zx-control-height) - var(--zx-space-3));
61
+ margin-block: var(--zx-space-1);
62
+ border: 0;
63
+ background: var(--zx-color-border);
64
+ }
65
+
66
+ .zx-toolbar[data-dense="true"] .zx-toolbar__separator,
67
+ .zx-toolbar[data-dense="true"] > [data-separator] {
68
+ margin-block: calc(var(--zx-space-1) / 2);
69
+ }
70
+
71
+ /* The overflow trigger is icon-only; its empty label would otherwise add a gap. */
72
+ .zx-toolbar__overflow > .zx-btn__label:empty {
73
+ display: none;
74
+ }
75
+
76
+ /* Every item is reachable by keyboard, so every item rings — including the plain Elements a
77
+ caller supplied, which carry no Zx button styling of their own. */
78
+ .zx-toolbar > :focus-visible,
79
+ .zx-toolbar > [role="group"] > :focus-visible {
80
+ box-shadow: var(--zx-focus-ring);
81
+ }