@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,150 @@
1
+ // @ts-check
2
+ /**
3
+ * Named width thresholds, and a way to react to one being crossed.
4
+ *
5
+ * The scale lives here rather than in the token layer because a CSS custom property cannot be
6
+ * used inside a media query, so a `--zx-bp-*` token would be a value no stylesheet could read.
7
+ * Zx's own layout utilities avoid the problem entirely — `.zx-grid` reflows on intrinsic track
8
+ * widths, not on breakpoints — which leaves this module for the cases only script can decide:
9
+ * rendering a Table as cards below a width, collapsing a MasterPanel, choosing a chart density.
10
+ * @module zx/core/breakpoint
11
+ */
12
+
13
+ import { isElement } from './util.js';
14
+
15
+ /** @typedef {'xs'|'sm'|'md'|'lg'|'xl'} BreakpointName */
16
+
17
+ /**
18
+ * Lower bound of each named breakpoint, in pixels. A width below `sm` is `xs`.
19
+ * @type {Readonly<Record<Exclude<BreakpointName, 'xs'>, number>>}
20
+ */
21
+ export const breakpoints = Object.freeze({ sm: 480, md: 768, lg: 1024, xl: 1280 });
22
+
23
+ /** Names from widest to narrowest, which is the order a lookup has to test them in. */
24
+ const ORDER = /** @type {const} */ (['xl', 'lg', 'md', 'sm']);
25
+
26
+ /**
27
+ * The breakpoint a width falls into.
28
+ * @param {number} width Width in pixels.
29
+ * @returns {BreakpointName}
30
+ */
31
+ export function breakpointOf(width) {
32
+ const value = Number(width);
33
+ if (!Number.isFinite(value)) return 'xs';
34
+ for (const name of ORDER) {
35
+ if (value >= breakpoints[name]) return name;
36
+ }
37
+ return 'xs';
38
+ }
39
+
40
+ /**
41
+ * Whether a width has reached a named breakpoint. `matchBreakpoint('md', 900)` is the script
42
+ * equivalent of `@media (min-width: 768px)`.
43
+ * @param {BreakpointName} name Breakpoint to test against.
44
+ * @param {number} width Width in pixels.
45
+ * @returns {boolean}
46
+ */
47
+ export function matchBreakpoint(name, width) {
48
+ if (name === 'xs') return true;
49
+ const min = breakpoints[name];
50
+ if (min === undefined) throw new RangeError(`Unknown breakpoint "${name}"`);
51
+ return Number(width) >= min;
52
+ }
53
+
54
+ /**
55
+ * @typedef {Object} BreakpointOptions
56
+ * @property {Element|Window} [target=window] What to measure. An Element is watched with a
57
+ * `ResizeObserver`, which is what a component inside a split pane or a modal needs — the
58
+ * viewport width says nothing about the space it actually has.
59
+ */
60
+
61
+ /**
62
+ * @typedef {Object} BreakpointController
63
+ * @property {() => BreakpointName} current The breakpoint as last measured.
64
+ * @property {() => void} destroy Stops observing.
65
+ */
66
+
67
+ /**
68
+ * Calls back whenever the observed width crosses into another breakpoint, and once immediately
69
+ * with the current one.
70
+ * @param {(name: BreakpointName, width: number) => void} handler Called on every change.
71
+ * @param {BreakpointOptions} [options={}] What to observe.
72
+ * @returns {BreakpointController}
73
+ */
74
+ export function onBreakpoint(handler, options = {}) {
75
+ if (typeof handler !== 'function') throw new TypeError('onBreakpoint requires a handler');
76
+ /*
77
+ * An explicitly passed target that is not an element is a mistake worth shouting about. Falling
78
+ * back to the window looks like it works — the callback fires, the values are plausible — while
79
+ * measuring something entirely different from what the caller asked for. Table hit exactly this:
80
+ * it passed `this.el` before the base constructor had assigned it, and quietly watched the
81
+ * viewport instead of itself.
82
+ */
83
+ if ('target' in options && !isElement(options.target)
84
+ && /** @type {unknown} */ (options.target) !== /** @type {unknown} */ (globalThis)) {
85
+ throw new TypeError('onBreakpoint target must be an Element or the window');
86
+ }
87
+ const target = options.target ?? globalThis;
88
+ const element = isElement(target) ? /** @type {Element} */ (target) : null;
89
+ /** Null until the first real measurement — see `update()` on why 0 is not a band. */
90
+ let name = /** @type {BreakpointName|null} */ (null);
91
+ let destroyed = false;
92
+ let observer = null;
93
+
94
+ const measure = () => (element
95
+ ? element.getBoundingClientRect().width
96
+ : Number(globalThis.innerWidth) || 0);
97
+
98
+ const update = () => {
99
+ if (destroyed) return;
100
+ const width = measure();
101
+ /*
102
+ * A zero width is not the `xs` band — it is no measurement at all, which is what an element
103
+ * that has not been laid out yet reports. Recording it as a band silently disarms the whole
104
+ * watcher: the next real width lands in the same band and never notifies. Table hit exactly
105
+ * this, rendering before it was inserted, measuring 0, and then sitting at 154px inside a
106
+ * narrow pane without ever being told it should stack.
107
+ */
108
+ if (element && width === 0) return;
109
+ const next = breakpointOf(width);
110
+ if (next === name) return;
111
+ name = next;
112
+ handler(name, width);
113
+ };
114
+
115
+ update();
116
+
117
+ /*
118
+ * Resize notifications are answered on the next frame rather than inside the observer callback.
119
+ * A handler that reacts to a width by changing layout — which is the entire point of this
120
+ * module; Table restyles itself — resizes the very element being observed, and the browser
121
+ * reports that as "ResizeObserver loop completed with undelivered notifications". Deferring
122
+ * breaks the cycle, and coalesces a burst of notifications into one.
123
+ */
124
+ let frame = 0;
125
+ const schedule = () => {
126
+ if (frame || destroyed) return;
127
+ frame = requestAnimationFrame(() => {
128
+ frame = 0;
129
+ update();
130
+ });
131
+ };
132
+
133
+ if (element && typeof ResizeObserver === 'function') {
134
+ observer = new ResizeObserver(schedule);
135
+ observer.observe(element);
136
+ } else {
137
+ globalThis.addEventListener?.('resize', update, { passive: true });
138
+ }
139
+
140
+ return {
141
+ current: () => name ?? breakpointOf(measure()),
142
+ destroy: () => {
143
+ if (destroyed) return;
144
+ destroyed = true;
145
+ if (frame) cancelAnimationFrame(frame);
146
+ observer?.disconnect();
147
+ globalThis.removeEventListener?.('resize', update);
148
+ }
149
+ };
150
+ }
@@ -0,0 +1,292 @@
1
+ // @ts-check
2
+ import { h, resolveElement } from './dom.js';
3
+ import { getLanguage, printf, translate } from './i18n.js';
4
+ import { deepMerge, isElement } from './util.js';
5
+
6
+ /** @typedef {Record<string, any>} ComponentOptions */
7
+ /** @typedef {(event: Event & {detail?: Record<string, unknown>}) => void} ComponentListener */
8
+
9
+ /** @type {WeakMap<Element, Component>} */
10
+ const registry = new WeakMap();
11
+
12
+ /**
13
+ * Base class for lifecycle-safe Zx components.
14
+ *
15
+ * Generic over its own options type so a subclass can bind its `XOptions` typedef with
16
+ * `@extends {Component<XOptions>}`. That is what types `new Select(target, { … })` for editors and
17
+ * for TypeScript consumers: a subclass declares no constructor of its own, so it inherits this
18
+ * signature, and without the binding every component would accept any object at all.
19
+ *
20
+ * @template {ComponentOptions} [TOptions=ComponentOptions]
21
+ * @fires Component#event
22
+ */
23
+ export class Component extends EventTarget {
24
+ /** @type {ComponentOptions} */
25
+ static defaults = {};
26
+
27
+ /**
28
+ * BEM block a subclass wants on its root, without the `zx-` prefix. Empty on the base, which is
29
+ * the same falsy answer the undeclared property gave — declared so the statics are visible to
30
+ * `this.constructor` and to the generated declarations.
31
+ * @type {string}
32
+ */
33
+ static cssName = '';
34
+
35
+ /** @type {Element} Root element. */
36
+ el;
37
+
38
+ /** @type {Record<string, Element>} Elements captured by `h()` ref properties. */
39
+ refs;
40
+
41
+ /** @type {Readonly<TOptions>} Merged component options. */
42
+ options;
43
+
44
+ #abort = new AbortController();
45
+ #created = false;
46
+ #destroyed = false;
47
+ #addedRootClass = false;
48
+
49
+ /**
50
+ * Creates a component around an existing target, or asks `render()` to create its root.
51
+ * @param {Element|string|null} target Existing element, selector, or null.
52
+ * @param {TOptions} [options={}] Component options.
53
+ */
54
+ constructor(target, options = /** @type {TOptions} */ ({})) {
55
+ super();
56
+ /** `this.constructor` is typed `Function`, which hides the statics this class declares. */
57
+ const self = /** @type {typeof Component} */ (this.constructor);
58
+ this.#created = target === null;
59
+ const resolved = resolveElement(target);
60
+ if (target !== null && !resolved) throw new TypeError('Component target could not be resolved');
61
+ this.el = /** @type {Element} */ (resolved);
62
+ this.refs = {};
63
+
64
+ const merged = /** @type {TOptions} */ (mergeOptions(self, options));
65
+ const eventOptions = [];
66
+ for (const [key, value] of Object.entries(merged)) {
67
+ if (/^on[a-z]/.test(key) && typeof value === 'function') {
68
+ eventOptions.push([key.slice(2), value]);
69
+ delete merged[key];
70
+ }
71
+ }
72
+ if (merged.msg && typeof merged.msg === 'object') Object.freeze(merged.msg);
73
+ this.options = Object.freeze(merged);
74
+ for (const [type, listener] of eventOptions) this.on(type, listener);
75
+
76
+ let rendered;
77
+ try {
78
+ rendered = h.scope(this.refs, () => this.render());
79
+ } catch (error) {
80
+ this.#abort.abort();
81
+ throw error;
82
+ }
83
+ if (this.#created) {
84
+ const root = isElement(rendered) ? rendered : this.el;
85
+ if (!isElement(root)) {
86
+ this.#abort.abort();
87
+ throw new TypeError('render() must return an Element (or assign this.el) when target is null');
88
+ }
89
+ this.el = root;
90
+ }
91
+ if (!isElement(this.el)) {
92
+ this.#abort.abort();
93
+ throw new TypeError('Component requires a root Element');
94
+ }
95
+
96
+ const cssName = self.cssName;
97
+ if (typeof cssName === 'string' && cssName) {
98
+ const className = `zx-${cssName}`;
99
+ this.#addedRootClass = !this.el.classList.contains(className);
100
+ this.el.classList.add(className);
101
+ }
102
+ registry.set(this.el, this);
103
+ }
104
+
105
+ /**
106
+ * Renders or enhances the component root. Subclasses creating a root must return it.
107
+ * @returns {Element|null}
108
+ */
109
+ render() {
110
+ return this.el ?? null;
111
+ }
112
+
113
+ /**
114
+ * Subscribes to a component-level event for this component's lifetime.
115
+ * @param {string} type Lowercase event name.
116
+ * @param {ComponentListener|EventListenerObject} fn Listener.
117
+ * @returns {this}
118
+ */
119
+ on(type, fn) {
120
+ this.addEventListener(type, fn, { signal: this.#abort.signal });
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * Removes a component-level event listener.
126
+ * @param {string} type Event name.
127
+ * @param {ComponentListener|EventListenerObject} fn Listener.
128
+ * @returns {this}
129
+ */
130
+ off(type, fn) {
131
+ this.removeEventListener(type, fn);
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * Subscribes to one component-level event occurrence.
137
+ * @param {string} type Event name.
138
+ * @param {ComponentListener|EventListenerObject} fn Listener.
139
+ * @returns {this}
140
+ */
141
+ once(type, fn) {
142
+ this.addEventListener(type, fn, { once: true, signal: this.#abort.signal });
143
+ return this;
144
+ }
145
+
146
+ /**
147
+ * MooTools-compatible alias of `on()`.
148
+ * @param {string} type Event name.
149
+ * @param {ComponentListener|EventListenerObject} fn Listener.
150
+ * @returns {this}
151
+ */
152
+ addEvent(type, fn) {
153
+ return this.on(type, fn);
154
+ }
155
+
156
+ /**
157
+ * Dispatches a component event and a bubbling, composed `zx-*` DOM event.
158
+ * @param {string} type Lowercase event name.
159
+ * @param {Record<string, unknown>} [detail={}] Single event detail object.
160
+ * @returns {Event & {detail: Record<string, unknown>}}
161
+ * @fires Component#event
162
+ */
163
+ emit(type, detail = {}) {
164
+ if (detail === null || typeof detail !== 'object' || Array.isArray(detail)) {
165
+ throw new TypeError('Event detail must be an object');
166
+ }
167
+ const componentEvent = createCustomEvent(type, detail, { cancelable: true });
168
+ this.dispatchEvent(componentEvent);
169
+ if (this.el) {
170
+ this.el.dispatchEvent(createCustomEvent(`zx-${type}`, detail, {
171
+ bubbles: true,
172
+ composed: true,
173
+ cancelable: true
174
+ }));
175
+ }
176
+ return componentEvent;
177
+ }
178
+
179
+ /**
180
+ * Adds an automatically cleaned-up DOM event listener.
181
+ * @param {EventTarget} element DOM event target.
182
+ * @param {string} type Event name.
183
+ * @param {EventListenerOrEventListenerObject} fn Listener.
184
+ * @param {boolean|AddEventListenerOptions} [opts={}] Listener options.
185
+ * @returns {this}
186
+ */
187
+ listen(element, type, fn, opts = {}) {
188
+ const options = typeof opts === 'boolean' ? { capture: opts } : { ...opts };
189
+ element.addEventListener(type, fn, { ...options, signal: this.#abort.signal });
190
+ return this;
191
+ }
192
+
193
+ /**
194
+ * Returns the component's root element.
195
+ * @returns {Element}
196
+ */
197
+ toElement() {
198
+ return this.el;
199
+ }
200
+
201
+ /**
202
+ * Resolves and interpolates a component-local or global translation.
203
+ * @param {string} key Message key.
204
+ * @param {...unknown} args Positional interpolation values.
205
+ * @returns {string}
206
+ */
207
+ msg(key, ...args) {
208
+ const messages = this.options.msg;
209
+ const lang = this.options.lang ?? this.options.language ?? getLanguage();
210
+ const localized = messages?.[lang]?.[key];
211
+ if (localized != null) return printf(String(localized), args);
212
+ const direct = messages?.[key];
213
+ if (direct != null) return printf(String(direct), args);
214
+ return translate(key, args);
215
+ }
216
+
217
+ /**
218
+ * Aborts listeners, unregisters the component, and removes roots created by the component.
219
+ * Safe to call repeatedly.
220
+ * @returns {void}
221
+ */
222
+ destroy() {
223
+ if (this.#destroyed) return;
224
+ this.#destroyed = true;
225
+ this.#abort.abort();
226
+ if (registry.get(this.el) === this) registry.delete(this.el);
227
+ const cssName = /** @type {typeof Component} */ (this.constructor).cssName;
228
+ if (this.#addedRootClass && typeof cssName === 'string' && this.el) {
229
+ this.el.classList.remove(`zx-${cssName}`);
230
+ }
231
+ if (this.#created && this.el) this.el.remove();
232
+ }
233
+
234
+ /**
235
+ * Retrieves the component currently registered for an element.
236
+ * @param {Element} element Root element.
237
+ * @returns {Component|null}
238
+ */
239
+ static from(element) {
240
+ return registry.get(element) ?? null;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Generic component event emitted under the name passed to `emit()`.
246
+ * @event Component#event
247
+ * @type {CustomEvent<Record<string, unknown>>}
248
+ */
249
+
250
+ /** @param {typeof Component} constructor @param {ComponentOptions} options @returns {ComponentOptions} */
251
+ function mergeOptions(constructor, options) {
252
+ const chain = [];
253
+ let current = constructor;
254
+ while (current && current !== Function.prototype) {
255
+ if (Object.prototype.hasOwnProperty.call(current, 'defaults')) chain.unshift(current.defaults);
256
+ current = Object.getPrototypeOf(current);
257
+ }
258
+
259
+ const result = {};
260
+ let messages = {};
261
+ let hasMessages = false;
262
+ for (const defaults of chain) {
263
+ if (!defaults || typeof defaults !== 'object') continue;
264
+ Object.assign(result, defaults);
265
+ if (defaults.msg && typeof defaults.msg === 'object') {
266
+ hasMessages = true;
267
+ messages = deepMerge(messages, defaults.msg);
268
+ }
269
+ }
270
+ Object.assign(result, options && typeof options === 'object' ? options : {});
271
+ if (options?.msg && typeof options.msg === 'object') {
272
+ hasMessages = true;
273
+ messages = deepMerge(messages, options.msg);
274
+ }
275
+ if (hasMessages) result.msg = messages;
276
+ return result;
277
+ }
278
+
279
+ /**
280
+ * @param {string} type
281
+ * @param {Record<string, unknown>} detail
282
+ * @param {CustomEventInit<Record<string, unknown>>} init
283
+ * @returns {Event & {detail: Record<string, unknown>}}
284
+ */
285
+ function createCustomEvent(type, detail, init) {
286
+ if (typeof CustomEvent === 'function') {
287
+ return new CustomEvent(type, { ...init, detail });
288
+ }
289
+ const event = new Event(type, init);
290
+ Object.defineProperty(event, 'detail', { configurable: true, enumerable: true, value: detail });
291
+ return /** @type {Event & {detail: Record<string, unknown>}} */ (event);
292
+ }
@@ -0,0 +1,216 @@
1
+ // @ts-check
2
+ import { getLanguage, translate } from './i18n.js';
3
+
4
+ const MONTHS = [
5
+ 'January', 'February', 'March', 'April', 'May', 'June',
6
+ 'July', 'August', 'September', 'October', 'November', 'December'
7
+ ];
8
+ const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
9
+ const TOKEN_PATTERN = /%[demYyHMSaAbBs]/g;
10
+
11
+ /**
12
+ * Formats a local Date using strftime-style kernel tokens.
13
+ * @param {Date} date Date to format.
14
+ * @param {string} fmt Format containing `%d %e %m %Y %y %H %M %S %a %A %b %B %s` tokens.
15
+ * @returns {string}
16
+ */
17
+ export function formatDate(date, fmt) {
18
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
19
+ return String(fmt).replace(TOKEN_PATTERN, (token) => {
20
+ switch (token) {
21
+ case '%d': return pad(date.getDate());
22
+ case '%e': return String(date.getDate());
23
+ case '%m': return pad(date.getMonth() + 1);
24
+ case '%Y': return String(date.getFullYear()).padStart(4, '0');
25
+ case '%y': return pad(date.getFullYear() % 100);
26
+ case '%H': return pad(date.getHours());
27
+ case '%M': return pad(date.getMinutes());
28
+ case '%S': return pad(date.getSeconds());
29
+ case '%a': return localizedShortName('weekday', date.getDay(), WEEKDAYS[date.getDay()]);
30
+ case '%A': return localizedName('weekday', date.getDay(), WEEKDAYS[date.getDay()]);
31
+ case '%b': return localizedShortName('month', date.getMonth(), MONTHS[date.getMonth()]);
32
+ case '%B': return localizedName('month', date.getMonth(), MONTHS[date.getMonth()]);
33
+ case '%s': return String(Math.floor(date.getTime() / 1000));
34
+ default: return token;
35
+ }
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Parses numeric date tokens from a string, returning null for mismatches or invalid dates.
41
+ * Unspecified fields default to 1970-01-01 00:00:00 local time.
42
+ * @param {string} str Date text.
43
+ * @param {string} fmt Matching numeric format.
44
+ * @returns {Date|null}
45
+ */
46
+ export function parseDate(str, fmt) {
47
+ const fields = [];
48
+ let pattern = '^';
49
+ let cursor = 0;
50
+
51
+ for (const match of String(fmt).matchAll(TOKEN_PATTERN)) {
52
+ pattern += escapeRegExp(String(fmt).slice(cursor, match.index));
53
+ const token = match[0];
54
+ const tokenPattern = numericPattern(token);
55
+ if (tokenPattern === null) return null;
56
+ pattern += `(${tokenPattern})`;
57
+ fields.push(token);
58
+ cursor = match.index + token.length;
59
+ }
60
+ pattern += escapeRegExp(String(fmt).slice(cursor)) + '$';
61
+
62
+ const matched = new RegExp(pattern).exec(String(str));
63
+ if (!matched) return null;
64
+ const values = Object.create(null);
65
+ fields.forEach((field, index) => {
66
+ values[field] = Number(matched[index + 1]);
67
+ });
68
+
69
+ if (values['%s'] !== undefined) {
70
+ const timestamp = values['%s'] * 1000;
71
+ const parsed = new Date(timestamp);
72
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
73
+ }
74
+
75
+ const year = values['%Y'] ?? yearFromTwoDigits(values['%y']) ?? 1970;
76
+ const month = (values['%m'] ?? 1) - 1;
77
+ const day = values['%d'] ?? values['%e'] ?? 1;
78
+ const hour = values['%H'] ?? 0;
79
+ const minute = values['%M'] ?? 0;
80
+ const second = values['%S'] ?? 0;
81
+ const parsed = new Date(0);
82
+ parsed.setHours(0, 0, 0, 0);
83
+ parsed.setFullYear(year, month, day);
84
+ parsed.setHours(hour, minute, second, 0);
85
+
86
+ if (Number.isNaN(parsed.getTime())) return null;
87
+ if (values['%Y'] !== undefined && parsed.getFullYear() !== values['%Y']) return null;
88
+ if (values['%y'] !== undefined && parsed.getFullYear() % 100 !== values['%y']) return null;
89
+ if (values['%m'] !== undefined && parsed.getMonth() !== month) return null;
90
+ if (values['%d'] !== undefined && parsed.getDate() !== day) return null;
91
+ if (values['%e'] !== undefined && parsed.getDate() !== day) return null;
92
+ if (values['%H'] !== undefined && parsed.getHours() !== hour) return null;
93
+ if (values['%M'] !== undefined && parsed.getMinutes() !== minute) return null;
94
+ if (values['%S'] !== undefined && parsed.getSeconds() !== second) return null;
95
+ return parsed;
96
+ }
97
+
98
+ /**
99
+ * Clamps a date to optional inclusive bounds and returns a new Date.
100
+ * @param {Date} date Date to clamp.
101
+ * @param {Date|null|undefined} min Minimum date.
102
+ * @param {Date|null|undefined} max Maximum date.
103
+ * @returns {Date}
104
+ */
105
+ export function clampDate(date, min, max) {
106
+ const value = date.getTime();
107
+ const minimum = min instanceof Date ? min.getTime() : -Infinity;
108
+ const maximum = max instanceof Date ? max.getTime() : Infinity;
109
+ return new Date(Math.min(maximum, Math.max(minimum, value)));
110
+ }
111
+
112
+ /**
113
+ * Reports whether two dates share the same local calendar day.
114
+ * @param {Date} a First date.
115
+ * @param {Date} b Second date.
116
+ * @returns {boolean}
117
+ */
118
+ export function isSameDay(a, b) {
119
+ return a.getFullYear() === b.getFullYear() &&
120
+ a.getMonth() === b.getMonth() &&
121
+ a.getDate() === b.getDate();
122
+ }
123
+
124
+ /**
125
+ * Adds local calendar days without mutating the input.
126
+ * @param {Date} date Base date.
127
+ * @param {number} n Number of days.
128
+ * @returns {Date}
129
+ */
130
+ export function addDays(date, n) {
131
+ const result = new Date(date.getTime());
132
+ result.setDate(result.getDate() + n);
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ * Adds local calendar months without mutating the input, clamping to the target month's end.
138
+ * @param {Date} date Base date.
139
+ * @param {number} n Number of months.
140
+ * @returns {Date}
141
+ */
142
+ export function addMonths(date, n) {
143
+ const result = new Date(date.getTime());
144
+ const day = result.getDate();
145
+ result.setDate(1);
146
+ result.setMonth(result.getMonth() + n);
147
+ const lastDay = new Date(result.getFullYear(), result.getMonth() + 1, 0).getDate();
148
+ result.setDate(Math.min(day, lastDay));
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Returns the locale's first weekday (`0` Sunday through `6` Saturday), Monday by default.
154
+ * @param {string} [lang=getLanguage()] BCP 47 language tag.
155
+ * @returns {number}
156
+ */
157
+ export function getWeekStart(lang = getLanguage()) {
158
+ if (!lang) return 1;
159
+ if (lang.toLowerCase() === 'en') return 1;
160
+ try {
161
+ /** `getWeekInfo()` and `weekInfo` are newer Intl surface than the bundled lib types know. */
162
+ const locale = /** @type {Intl.Locale & {getWeekInfo?: () => {firstDay?: number}, weekInfo?: {firstDay?: number}}} */
163
+ (new Intl.Locale(lang));
164
+ const weekInfo = locale.getWeekInfo?.() ?? locale.weekInfo;
165
+ if (weekInfo?.firstDay) return weekInfo.firstDay % 7;
166
+ } catch {
167
+ // Invalid or unsupported locale data uses the APG-friendly Monday default.
168
+ }
169
+ return 1;
170
+ }
171
+
172
+ /** @param {number} value @returns {string} */
173
+ function pad(value) {
174
+ return String(value).padStart(2, '0');
175
+ }
176
+
177
+ /** @param {'month'|'weekday'} type @param {number} index @param {string} fallback @returns {string} */
178
+ function localizedName(type, index, fallback) {
179
+ const key = `date.${type}.${index}`;
180
+ const value = translate(key);
181
+ return value === key ? fallback : value;
182
+ }
183
+
184
+ /** @param {'month'|'weekday'} type @param {number} index @param {string} fallback @returns {string} */
185
+ function localizedShortName(type, index, fallback) {
186
+ const shortKey = `date.${type}.short.${index}`;
187
+ const shortValue = translate(shortKey);
188
+ if (shortValue !== shortKey) return shortValue;
189
+ return localizedName(type, index, fallback).slice(0, 3);
190
+ }
191
+
192
+ /** @param {string} token @returns {string|null} */
193
+ function numericPattern(token) {
194
+ return {
195
+ '%d': '\\d{1,2}',
196
+ '%e': '\\d{1,2}',
197
+ '%m': '\\d{1,2}',
198
+ '%Y': '\\d{4}',
199
+ '%y': '\\d{2}',
200
+ '%H': '\\d{1,2}',
201
+ '%M': '\\d{1,2}',
202
+ '%S': '\\d{1,2}',
203
+ '%s': '-?\\d+'
204
+ }[token] ?? null;
205
+ }
206
+
207
+ /** @param {number|undefined} year @returns {number|undefined} */
208
+ function yearFromTwoDigits(year) {
209
+ if (year === undefined) return undefined;
210
+ return year >= 69 ? 1900 + year : 2000 + year;
211
+ }
212
+
213
+ /** @param {string} value @returns {string} */
214
+ function escapeRegExp(value) {
215
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
216
+ }