@zeyos/zx 2.1.0 → 2.2.1

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 +51 -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 +375 -39
  120. package/dist/zx-compat.esm.min.js +1 -1
  121. package/dist/zx-compat.global.js +372 -73
  122. package/dist/zx-compat.global.min.js +3 -3
  123. package/dist/zx-zeyos.esm.js +355 -49
  124. package/dist/zx-zeyos.esm.min.js +1 -1
  125. package/dist/zx.css +68 -6
  126. package/dist/zx.esm.js +359 -73
  127. package/dist/zx.esm.min.js +3 -3
  128. package/dist/zx.global.js +359 -73
  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 +31 -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 +1711 -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,545 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+
5
+ /**
6
+ * @typedef {Object} NumberFieldOptions
7
+ * @property {number|null} [value=null] Initial value, or null for empty.
8
+ * @property {number|null} [min=null] Lowest accepted value.
9
+ * @property {number|null} [max=null] Highest accepted value.
10
+ * @property {number} [step=1] Increment applied by the buttons and arrow keys.
11
+ * @property {number} [largeStep=10] Multiple of `step` applied by PageUp/PageDown.
12
+ * @property {number|null} [precision=null] Decimal places; derived from `step` when null.
13
+ * @property {boolean} [wrap=false] Whether stepping past a bound wraps to the other one.
14
+ * @property {string} [placeholder=''] Empty-state text.
15
+ * @property {string|null} [label=null] Accessible name for the spinbutton.
16
+ * @property {string|null} [unit=null] Suffix rendered inside the control (e.g. `%`, `kg`).
17
+ * @property {boolean} [disabled=false] Whether interaction is disabled.
18
+ * @property {boolean} [readonly=false] Whether the value is read-only.
19
+ * @property {boolean} [required=false] Whether an empty value is invalid.
20
+ * @property {string|null} [name=null] Native form field name.
21
+ * @property {boolean} [group=false] Whether to render thousands separators while idle.
22
+ * @property {string|null} [locale=null] Locale for formatting; defaults to the document locale.
23
+ * @property {Record<string, string>|Record<string, Record<string, string>>} [msg] Localized messages.
24
+ * @property {(event: CustomEvent<{value: number|null}>) => void} [onchange] Committed-value listener.
25
+ * @property {(event: CustomEvent<{value: number|null}>) => void} [oninput] Per-keystroke listener.
26
+ */
27
+
28
+ /**
29
+ * Numeric input with decrement and increment buttons, following the APG spinbutton pattern.
30
+ *
31
+ * The step buttons are pointer affordances only (`tabindex="-1"`, `aria-hidden`): the input
32
+ * itself is the spinbutton, so keyboard and screen-reader users step with the arrow keys rather
33
+ * than tabbing through two extra controls in every form row.
34
+ *
35
+ * @fires NumberField#change
36
+ * @fires NumberField#input
37
+ * @extends {Component<NumberFieldOptions>}
38
+ */
39
+ export class NumberField extends Component {
40
+ static cssName = 'number-field';
41
+
42
+ /** @type {NumberFieldOptions} */
43
+ static defaults = {
44
+ value: null,
45
+ min: null,
46
+ max: null,
47
+ step: 1,
48
+ largeStep: 10,
49
+ precision: null,
50
+ wrap: false,
51
+ placeholder: '',
52
+ label: null,
53
+ unit: null,
54
+ disabled: false,
55
+ readonly: false,
56
+ required: false,
57
+ name: null,
58
+ group: false,
59
+ locale: null
60
+ };
61
+
62
+ /** @returns {HTMLElement} */
63
+ render() {
64
+ // render() runs inside the base constructor, before class-field initializers would run.
65
+ this._value = null;
66
+ this._disabled = false;
67
+ this._readonly = false;
68
+ this._editing = false;
69
+ this._createdRoot = this.el === null;
70
+ this._snapshot = this._createdRoot ? null : snapshotTarget(this.el);
71
+
72
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
73
+ this.el = root;
74
+
75
+ const step = normalizeStep(this.options.step);
76
+ this._step = step;
77
+ this._precision = this.options.precision === null
78
+ ? decimalsOf(step)
79
+ : Math.max(0, Math.trunc(Number(this.options.precision) || 0));
80
+
81
+ const down = this._stepButton('down', 'minus');
82
+ const up = this._stepButton('up', 'plus');
83
+ const input = h('input', {
84
+ ref: 'input',
85
+ class: 'zx-number-field__input',
86
+ type: 'text',
87
+ role: 'spinbutton',
88
+ inputMode: this._precision > 0 ? 'decimal' : 'numeric',
89
+ autocomplete: 'off',
90
+ spellcheck: false,
91
+ placeholder: String(this.options.placeholder ?? ''),
92
+ ariaLabel: this.options.label ?? undefined,
93
+ name: this.options.name ?? undefined,
94
+ required: Boolean(this.options.required)
95
+ });
96
+ const children = [down, input];
97
+ if (this.options.unit !== null) {
98
+ children.push(h('span', {
99
+ ref: 'unit',
100
+ class: 'zx-number-field__unit',
101
+ ariaHidden: 'true'
102
+ }, String(this.options.unit)));
103
+ }
104
+ children.push(up);
105
+ root.replaceChildren(...children);
106
+
107
+ this._disabled = Boolean(this.options.disabled);
108
+ this._readonly = Boolean(this.options.readonly);
109
+
110
+ this.listen(input, 'input', () => this._onInput());
111
+ this.listen(input, 'keydown', (event) => this._onKeydown(/** @type {KeyboardEvent} */ (event)));
112
+ // Grouped values are only shown while idle; editing always sees a plain, re-parseable number.
113
+ this.listen(input, 'focus', () => {
114
+ this._editing = true;
115
+ this._syncInput();
116
+ });
117
+ this.listen(input, 'blur', () => {
118
+ this._commit();
119
+ this._editing = false;
120
+ this._syncInput();
121
+ });
122
+ this.listen(down, 'click', () => this._stepBy(-1));
123
+ this.listen(up, 'click', () => this._stepBy(1));
124
+ // Pointer wheel only while focused, so scrolling a form never changes a value by accident.
125
+ this.listen(input, 'wheel', (event) => {
126
+ if (document.activeElement !== input || this._disabled || this._readonly) return;
127
+ event.preventDefault();
128
+ this._stepBy(event.deltaY < 0 ? 1 : -1);
129
+ }, { passive: false });
130
+
131
+ this.set(this.options.value, { silent: true });
132
+ this._syncDisabled();
133
+ return root;
134
+ }
135
+
136
+ /** Current value, or null when empty. @returns {number|null} */
137
+ get value() {
138
+ return this._value;
139
+ }
140
+
141
+ /** @param {number|null} next Next value. */
142
+ set value(next) {
143
+ this.set(next);
144
+ }
145
+
146
+ /**
147
+ * Returns the current value.
148
+ * @returns {number|null}
149
+ */
150
+ get() {
151
+ return this._value;
152
+ }
153
+
154
+ /**
155
+ * Sets the value, snapping it to `step` and clamping it into `[min, max]`.
156
+ * @param {number|string|null} next Next value; empty strings and null clear the field.
157
+ * @param {{silent?: boolean}} [options={}] Set `silent` to suppress `change`.
158
+ * @returns {this}
159
+ * @fires NumberField#change
160
+ */
161
+ set(next, { silent = false } = {}) {
162
+ const parsed = this._coerce(next);
163
+ const changed = parsed !== this._value;
164
+ this._value = parsed;
165
+ this._syncInput();
166
+ if (changed && !silent) this.emit('change', { value: this._value });
167
+ return this;
168
+ }
169
+
170
+ /**
171
+ * Increases the value by `count` steps.
172
+ * @param {number} [count=1] Number of steps.
173
+ * @returns {this}
174
+ * @fires NumberField#change
175
+ */
176
+ stepUp(count = 1) {
177
+ return this._stepBy(count);
178
+ }
179
+
180
+ /**
181
+ * Decreases the value by `count` steps.
182
+ * @param {number} [count=1] Number of steps.
183
+ * @returns {this}
184
+ * @fires NumberField#change
185
+ */
186
+ stepDown(count = 1) {
187
+ return this._stepBy(-count);
188
+ }
189
+
190
+ /**
191
+ * Restores the configured initial value.
192
+ * @param {{silent?: boolean}} [options={}] Reset behavior.
193
+ * @returns {this}
194
+ */
195
+ reset({ silent = false } = {}) {
196
+ return this.set(this.options.value, { silent });
197
+ }
198
+
199
+ /** Focuses the input. @returns {this} */
200
+ focus() {
201
+ this.refs.input.focus();
202
+ return this;
203
+ }
204
+
205
+ /** Returns the underlying input element. @returns {HTMLInputElement} */
206
+ getInput() {
207
+ return /** @type {HTMLInputElement} */ (this.refs.input);
208
+ }
209
+
210
+ /** Enables user interaction. @returns {this} */
211
+ enable() {
212
+ this._disabled = false;
213
+ this._syncDisabled();
214
+ return this;
215
+ }
216
+
217
+ /** Disables user interaction. @returns {this} */
218
+ disable() {
219
+ this._disabled = true;
220
+ this._syncDisabled();
221
+ return this;
222
+ }
223
+
224
+ /**
225
+ * Sets the read-only state.
226
+ * @param {boolean} readonly Whether the value is read-only.
227
+ * @returns {this}
228
+ */
229
+ setReadonly(readonly) {
230
+ this._readonly = Boolean(readonly);
231
+ this._syncDisabled();
232
+ return this;
233
+ }
234
+
235
+ /**
236
+ * Replaces the accepted range.
237
+ * @param {number|null} min Lowest accepted value.
238
+ * @param {number|null} max Highest accepted value.
239
+ * @returns {this}
240
+ */
241
+ setRange(min, max) {
242
+ this._min = min === null || min === undefined ? null : Number(min);
243
+ this._max = max === null || max === undefined ? null : Number(max);
244
+ return this.set(this._value, { silent: true });
245
+ }
246
+
247
+ /** @param {'down'|'up'} direction @param {string} glyph @returns {HTMLButtonElement} */
248
+ _stepButton(direction, glyph) {
249
+ return /** @type {HTMLButtonElement} */ (h('button', {
250
+ ref: direction,
251
+ class: 'zx-number-field__step',
252
+ type: 'button',
253
+ tabIndex: -1,
254
+ ariaHidden: 'true',
255
+ dataset: { step: direction }
256
+ }, icon(glyph, { size: 13 })));
257
+ }
258
+
259
+ /** @returns {number|null} */
260
+ get _rangeMin() {
261
+ if (this._min !== undefined) return this._min;
262
+ return this.options.min === null || this.options.min === undefined ? null : Number(this.options.min);
263
+ }
264
+
265
+ /** @returns {number|null} */
266
+ get _rangeMax() {
267
+ if (this._max !== undefined) return this._max;
268
+ return this.options.max === null || this.options.max === undefined ? null : Number(this.options.max);
269
+ }
270
+
271
+ /**
272
+ * Parses, snaps, and clamps an arbitrary input value.
273
+ * @param {unknown} raw Value from an option, a method call, or the input element.
274
+ * @returns {number|null}
275
+ */
276
+ _coerce(raw) {
277
+ const parsed = parseNumber(raw, this.options.group ? this._separators() : null);
278
+ if (parsed === null) return null;
279
+ return snapNumber(parsed, {
280
+ min: this._rangeMin,
281
+ max: this._rangeMax,
282
+ step: this._step,
283
+ precision: this._precision
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Applies `count` steps relative to the current value.
289
+ * @param {number} count Signed step count.
290
+ * @returns {this}
291
+ */
292
+ _stepBy(count) {
293
+ if (this._disabled || this._readonly) return this;
294
+ const min = this._rangeMin;
295
+ const max = this._rangeMax;
296
+ // Step from whatever is in the field right now: typing 25 then pressing ArrowUp must give 26,
297
+ // not commit-value + 1.
298
+ const typed = parseNumber(this.refs.input.value);
299
+ const current = typed ?? this._value;
300
+ const base = current === null ? (min ?? 0) : current;
301
+ // An empty field steps to the lower bound first, so the first click lands on a real value.
302
+ const start = current === null && count > 0 && min !== null ? min - this._step * count : base;
303
+ let next = round(start + this._step * count, this._precision);
304
+
305
+ if (this.options.wrap && min !== null && max !== null) {
306
+ if (next > max) next = min;
307
+ else if (next < min) next = max;
308
+ }
309
+ this.set(next);
310
+ return this;
311
+ }
312
+
313
+ /**
314
+ * The grouping and decimal separators of the configured locale.
315
+ * @returns {{group: string, decimal: string}}
316
+ */
317
+ _separators() {
318
+ if (this._localeSeparators) return this._localeSeparators;
319
+ const parts = new Intl.NumberFormat(this.options.locale ?? undefined, {
320
+ minimumFractionDigits: 1
321
+ }).formatToParts(1234.5);
322
+ this._localeSeparators = {
323
+ group: parts.find((part) => part.type === 'group')?.value ?? ',',
324
+ decimal: parts.find((part) => part.type === 'decimal')?.value ?? '.'
325
+ };
326
+ return this._localeSeparators;
327
+ }
328
+
329
+ /** @returns {void} */
330
+ _onInput() {
331
+ const parsed = parseNumber(this.refs.input.value, this.options.group ? this._separators() : null);
332
+ this._syncAria(parsed);
333
+ this.emit('input', { value: parsed });
334
+ }
335
+
336
+ /** @param {KeyboardEvent} event @returns {void} */
337
+ _onKeydown(event) {
338
+ if (this._disabled || this._readonly) return;
339
+ const large = Math.max(1, Math.trunc(Number(this.options.largeStep) || 1));
340
+ const actions = {
341
+ ArrowUp: () => this._stepBy(1),
342
+ ArrowDown: () => this._stepBy(-1),
343
+ PageUp: () => this._stepBy(large),
344
+ PageDown: () => this._stepBy(-large),
345
+ Home: () => (this._rangeMin === null ? null : this.set(this._rangeMin)),
346
+ End: () => (this._rangeMax === null ? null : this.set(this._rangeMax)),
347
+ Enter: () => this._commit()
348
+ };
349
+ const action = actions[event.key];
350
+ if (!action) return;
351
+ // Home/End keep their text-caret meaning when the field has no bound to jump to.
352
+ if ((event.key === 'Home' && this._rangeMin === null)
353
+ || (event.key === 'End' && this._rangeMax === null)) return;
354
+ event.preventDefault();
355
+ action();
356
+ }
357
+
358
+ /** Parses whatever the user typed and normalizes the displayed text. @returns {void} */
359
+ _commit() {
360
+ this.set(this.refs.input.value);
361
+ }
362
+
363
+ /** @returns {void} */
364
+ _syncInput() {
365
+ const input = /** @type {HTMLInputElement} */ (this.refs.input);
366
+ const text = this._value === null ? '' : this._format(this._value, this._editing);
367
+ if (input.value !== text) input.value = text;
368
+ this._syncAria(this._value);
369
+ }
370
+
371
+ /**
372
+ * @param {number|null} value Value to announce.
373
+ * @returns {void}
374
+ */
375
+ _syncAria(value) {
376
+ const input = this.refs.input;
377
+ const min = this._rangeMin;
378
+ const max = this._rangeMax;
379
+ if (min === null) input.removeAttribute('aria-valuemin');
380
+ else input.setAttribute('aria-valuemin', String(min));
381
+ if (max === null) input.removeAttribute('aria-valuemax');
382
+ else input.setAttribute('aria-valuemax', String(max));
383
+ if (value === null) {
384
+ // ARIA: omit aria-valuenow when the value is unknown, but still announce the empty state.
385
+ input.removeAttribute('aria-valuenow');
386
+ input.setAttribute('aria-valuetext', this._message('numberField.empty', 'Empty'));
387
+ } else {
388
+ input.setAttribute('aria-valuenow', String(value));
389
+ const unit = this.options.unit === null ? '' : ` ${this.options.unit}`;
390
+ input.setAttribute('aria-valuetext', `${this._format(value, false)}${unit}`);
391
+ }
392
+ this.el.toggleAttribute('data-empty', value === null);
393
+ }
394
+
395
+ /**
396
+ * @param {number} value Value to render.
397
+ * @param {boolean} editing Whether the field currently has focus.
398
+ * @returns {string}
399
+ */
400
+ _format(value, editing) {
401
+ // Ungrouped fields use a plain machine format, which `parseNumber` reads heuristically.
402
+ if (!this.options.group) return value.toFixed(this._precision);
403
+ // Grouped fields stay in the locale's notation even while editing — only the thousands
404
+ // separators drop away. Display and input therefore always use the same decimal character,
405
+ // so a value the field rendered is always a value it can read back.
406
+ return new Intl.NumberFormat(this.options.locale ?? undefined, {
407
+ minimumFractionDigits: this._precision,
408
+ maximumFractionDigits: this._precision,
409
+ useGrouping: !editing
410
+ }).format(value);
411
+ }
412
+
413
+ /**
414
+ * @param {string} key Message key.
415
+ * @param {string} fallback Text used when the host has no translation.
416
+ * @returns {string}
417
+ */
418
+ _message(key, fallback) {
419
+ const message = this.msg(key);
420
+ return message === key ? fallback : message;
421
+ }
422
+
423
+ /** @returns {void} */
424
+ _syncDisabled() {
425
+ const input = /** @type {HTMLInputElement} */ (this.refs.input);
426
+ input.disabled = this._disabled;
427
+ input.readOnly = this._readonly;
428
+ this.refs.up.disabled = this._disabled || this._readonly;
429
+ this.refs.down.disabled = this._disabled || this._readonly;
430
+ this.el.toggleAttribute('data-disabled', this._disabled);
431
+ this.el.toggleAttribute('data-readonly', this._readonly);
432
+ if (this._disabled) this.el.setAttribute('aria-disabled', 'true');
433
+ else this.el.removeAttribute('aria-disabled');
434
+ }
435
+
436
+ /** Restores an enhanced target to the markup it had before the takeover. @returns {void} */
437
+ destroy() {
438
+ const root = this.el;
439
+ super.destroy();
440
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Committed value change.
446
+ * @event NumberField#change
447
+ * @type {CustomEvent<{value: number|null}>}
448
+ */
449
+
450
+ /**
451
+ * Per-keystroke value change; the value may still be unsnapped or out of range.
452
+ * @event NumberField#input
453
+ * @type {CustomEvent<{value: number|null}>}
454
+ */
455
+
456
+ /**
457
+ * Parses a user-entered number.
458
+ *
459
+ * With `separators` (from the field's locale) the grouping character is removed and the decimal
460
+ * character is honoured, so a displayed `1,234` in en-US reads back as 1234 rather than 1.234.
461
+ * Without them, the last of `.` or `,` is treated as the decimal point and the other as grouping,
462
+ * which is the best available guess for free-form typing.
463
+ *
464
+ * @param {unknown} raw Value to parse.
465
+ * @param {{group: string, decimal: string}|null} [separators=null] Known locale separators.
466
+ * @returns {number|null} The number, or null when the input is empty or unparseable.
467
+ */
468
+ export function parseNumber(raw, separators = null) {
469
+ if (typeof raw === 'number') return Number.isFinite(raw) ? raw : null;
470
+ if (raw === null || raw === undefined) return null;
471
+ const text = String(raw).trim();
472
+ if (text === '') return null;
473
+
474
+ let normalized;
475
+ if (separators) {
476
+ normalized = text
477
+ .split(separators.group).join('')
478
+ .replace(separators.decimal, '.')
479
+ .replace(/[^\d.\-+eE]/g, '');
480
+ } else {
481
+ // Keep the last separator as the decimal point; everything before it is grouping.
482
+ const cleaned = text.replace(/[^\d,.\-+eE]/g, '');
483
+ const lastComma = cleaned.lastIndexOf(',');
484
+ const lastDot = cleaned.lastIndexOf('.');
485
+ normalized = cleaned;
486
+ if (lastComma >= 0 && lastComma > lastDot) {
487
+ normalized = cleaned.replace(/\./g, '').replace(',', '.');
488
+ } else if (lastComma >= 0) {
489
+ normalized = cleaned.replace(/,/g, '');
490
+ }
491
+ }
492
+ // Stripping non-numeric characters can empty the string ("abc", "€", "-"); `Number('')` is 0,
493
+ // which would silently turn junk into a valid zero.
494
+ if (!/\d/.test(normalized)) return null;
495
+ const value = Number(normalized);
496
+ return Number.isFinite(value) ? value : null;
497
+ }
498
+
499
+ /**
500
+ * Snaps a number onto the step grid and clamps it into the accepted range.
501
+ * @param {number} value Raw value.
502
+ * @param {{min: number|null, max: number|null, step: number, precision: number}} spec Constraints.
503
+ * @returns {number}
504
+ */
505
+ export function snapNumber(value, { min, max, step, precision }) {
506
+ let result = value;
507
+ if (step > 0) {
508
+ // Snap relative to `min` so a range like 5…95 with step 10 yields 5, 15, 25 rather than 10, 20.
509
+ const origin = min ?? 0;
510
+ result = origin + Math.round((value - origin) / step) * step;
511
+ }
512
+ result = round(result, precision);
513
+ // `min` and `max` are hard bounds and are applied last: rounding after the clamp could push the
514
+ // value back outside the range (max 9.5 with precision 0 would round up to 10).
515
+ if (min !== null && result < min) return min;
516
+ if (max !== null && result > max) return max;
517
+ return result;
518
+ }
519
+
520
+ /** @param {unknown} step @returns {number} */
521
+ function normalizeStep(step) {
522
+ const value = Number(step);
523
+ return Number.isFinite(value) && value > 0 ? value : 1;
524
+ }
525
+
526
+ /** @param {number} step @returns {number} */
527
+ function decimalsOf(step) {
528
+ const text = String(step);
529
+ const exponent = text.match(/e-(\d+)/i);
530
+ if (exponent) return Number(exponent[1]);
531
+ const fraction = text.split('.')[1];
532
+ return fraction ? fraction.length : 0;
533
+ }
534
+
535
+ /**
536
+ * Rounds away the binary floating-point drift repeated stepping introduces
537
+ * (0.1 + 0.2 → 0.30000000000000004).
538
+ * @param {number} value Value to round.
539
+ * @param {number} precision Decimal places.
540
+ * @returns {number}
541
+ */
542
+ function round(value, precision) {
543
+ if (!Number.isFinite(value)) return value;
544
+ return Number(value.toFixed(Math.min(15, Math.max(0, precision))));
545
+ }
@@ -0,0 +1,95 @@
1
+ /*
2
+ * Pagination — the footer bar of a server-backed list.
3
+ *
4
+ * One row that wraps: the summary is pushed to the start, the page-size select and the pager sit
5
+ * at the end. Page buttons are ordinary `.zx-btn` controls so they inherit the library's control
6
+ * height, radius, and focus ring; only the current page repaints in the accent colour.
7
+ */
8
+
9
+ .zx-pagination {
10
+ display: flex;
11
+ flex-wrap: wrap;
12
+ align-items: center;
13
+ gap: var(--zx-space-3);
14
+ min-inline-size: 0;
15
+ font-size: var(--zx-text-sm);
16
+ }
17
+
18
+ .zx-pagination__summary {
19
+ color: var(--zx-color-text-muted);
20
+ font-variant-numeric: tabular-nums;
21
+ white-space: nowrap;
22
+ }
23
+
24
+ /* Absorbs the slack, so summary and controls end up at opposite ends of a wide bar and simply
25
+ stack when there is no room. */
26
+ .zx-pagination__spacer {
27
+ flex: 1 1 var(--zx-space-4);
28
+ }
29
+
30
+ .zx-pagination__size {
31
+ display: inline-flex;
32
+ align-items: center;
33
+ gap: var(--zx-space-2);
34
+ color: var(--zx-color-text-muted);
35
+ white-space: nowrap;
36
+ }
37
+
38
+ .zx-pagination__select {
39
+ min-block-size: calc(var(--zx-control-height) - var(--zx-space-1));
40
+ padding-inline: var(--zx-space-2);
41
+ font-size: var(--zx-text-sm);
42
+ }
43
+
44
+ .zx-pagination__select:focus-visible {
45
+ outline: 0;
46
+ box-shadow: var(--zx-focus-ring);
47
+ }
48
+
49
+ .zx-pagination__controls {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ gap: var(--zx-space-1);
53
+ }
54
+
55
+ .zx-pagination__pages {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: var(--zx-space-1);
59
+ }
60
+
61
+ .zx-pagination__nav {
62
+ inline-size: calc(var(--zx-control-height) - var(--zx-space-1));
63
+ padding-inline: 0;
64
+ color: var(--zx-color-text-muted);
65
+ }
66
+
67
+ .zx-pagination__page {
68
+ min-inline-size: calc(var(--zx-control-height) - var(--zx-space-1));
69
+ padding-inline: var(--zx-space-2);
70
+ font-variant-numeric: tabular-nums;
71
+ }
72
+
73
+ .zx-pagination__page[aria-current="page"] {
74
+ border-color: var(--zx-color-accent);
75
+ background: var(--zx-color-accent);
76
+ color: var(--zx-color-on-accent);
77
+ }
78
+
79
+ .zx-pagination__page[aria-current="page"]:hover:not(:disabled) {
80
+ border-color: var(--zx-color-accent-hover);
81
+ background: var(--zx-color-accent-hover);
82
+ }
83
+
84
+ /* Decorative gap marker: never focusable, never announced. */
85
+ .zx-pagination__ellipsis {
86
+ display: inline-flex;
87
+ justify-content: center;
88
+ min-inline-size: var(--zx-space-5);
89
+ color: var(--zx-color-text-placeholder);
90
+ user-select: none;
91
+ }
92
+
93
+ .zx-pagination[data-mode="loadmore"] .zx-pagination__more {
94
+ flex: none;
95
+ }