@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,492 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { formatDate, parseDate } from '../../core/date.js';
3
+ import { h } from '../../core/dom.js';
4
+ import { printf, translate } from '../../core/i18n.js';
5
+ import { icon } from '../../core/icons.js';
6
+ import { position } from '../../core/position.js';
7
+ import { escapeRegExp, uid } from '../../core/util.js';
8
+ import { DateRangePicker, clampRange, normalizeRange } from '../date-picker/date-range-picker.js';
9
+
10
+ /** @typedef {import('../date-picker/date-range-picker.js').DateRange} DateRange */
11
+ /** @typedef {import('../date-picker/date-range-picker.js').DateRangePreset} DateRangePreset */
12
+ /** @typedef {{start: number|null, end: number|null}} DateRangeSeconds */
13
+ /** @typedef {Date|number|string|null} DateRangeInput */
14
+
15
+ /**
16
+ * @typedef {Object} DateRangeBoxOptions
17
+ * @property {Date|number|string|null} [start=null] Initial start: Date, Unix seconds, or formatted text.
18
+ * @property {Date|number|string|null} [end=null] Initial end: Date, Unix seconds, or formatted text.
19
+ * @property {string} [format='%d.%m.%Y'] Kernel date format used for both endpoints.
20
+ * @property {string} [separator=' – '] Text placed between the two formatted dates.
21
+ * @property {string|null} [placeholder=null] Input placeholder; null derives one from the format.
22
+ * @property {boolean} [clearable=true] Show a clear button while a range is present.
23
+ * @property {Date|null} [min=null] Earliest selectable local calendar day.
24
+ * @property {Date|null} [max=null] Latest selectable local calendar day.
25
+ * @property {number} [months=2] Month panels in the popover; narrow ones show only the first.
26
+ * @property {number} [weekStart=1] First weekday, from 0 (Sunday) through 6 (Saturday).
27
+ * @property {boolean} [showWeekNumbers=false] Show ISO week numbers.
28
+ * @property {number} [minNights=0] Smallest number of nights a complete range may span.
29
+ * @property {number|null} [maxNights=null] Largest number of nights a complete range may span.
30
+ * @property {DateRangePreset[]|boolean} [presets=[]] Preset buttons, or `true` for the built-in set.
31
+ * @property {boolean} [disabled=false] Disable all controls.
32
+ * @property {(event: CustomEvent<DateRange>) => void} [onchange] Change listener.
33
+ * @property {(event: CustomEvent<{text: string}>) => void} [oninvalid] Invalid-input listener.
34
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open listener.
35
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close listener.
36
+ */
37
+ /** @typedef {{silent?: boolean}} DateRangeBoxSetOptions */
38
+ /** @typedef {'date'|'seconds'} DateRangeBoxUnit */
39
+
40
+ const PLACEHOLDER_TOKENS = {
41
+ '%d': 'DD',
42
+ '%e': 'D',
43
+ '%m': 'MM',
44
+ '%Y': 'YYYY',
45
+ '%y': 'YY'
46
+ };
47
+
48
+ /**
49
+ * Parses `"<start><separator><end>"` into a range. A lone date is read as a one-day range, and a
50
+ * trailing separator as a start-only range, so a half-typed value is not thrown away.
51
+ * @param {string} text Input text.
52
+ * @param {string} format Kernel date format for each endpoint.
53
+ * @param {string} separator Separator between the two dates; surrounding spaces are optional.
54
+ * @returns {DateRange|null} The parsed range, or null when the text does not match.
55
+ */
56
+ export function parseRangeText(text, format, separator) {
57
+ const source = String(text ?? '').trim();
58
+ if (source === '') return { start: null, end: null };
59
+ const raw = String(separator ?? ' – ');
60
+ const core = raw.trim();
61
+ // Spacing is optional unless the separator also occurs inside the format itself — a " - "
62
+ // separator with a `%Y-%m-%d` format only splits where the spaces are, never on a date's dashes.
63
+ const strict = core !== '' && String(format).includes(core) && /^\s|\s$/.test(raw);
64
+ const lead = strict ? '\\s+' : '\\s*';
65
+ const tail = strict ? '(?:\\s+|$)' : '\\s*';
66
+ const parts = core === ''
67
+ ? [source]
68
+ : source.split(new RegExp(`${lead}${escapeRegExp(core)}${tail}`));
69
+ if (parts.length > 2) return null;
70
+
71
+ const first = parseDate(parts[0].trim(), format);
72
+ if (!first) return null;
73
+ if (parts.length === 1) return { start: first, end: new Date(first.getTime()) };
74
+ if (parts[1].trim() === '') return { start: first, end: null };
75
+ const second = parseDate(parts[1].trim(), format);
76
+ if (!second) return null;
77
+ return normalizeRange(first, second);
78
+ }
79
+
80
+ /**
81
+ * Renders a range as `"<start><separator><end>"`. An incomplete range yields the endpoint it has.
82
+ * @param {DateRange|null} range Range to render.
83
+ * @param {string} format Kernel date format for each endpoint.
84
+ * @param {string} separator Separator between the two dates.
85
+ * @returns {string}
86
+ */
87
+ export function formatRangeText(range, format, separator) {
88
+ const start = range?.start ?? null;
89
+ const end = range?.end ?? null;
90
+ if (!start && !end) return '';
91
+ if (!end) return formatDate(start, format);
92
+ if (!start) return formatDate(end, format);
93
+ return `${formatDate(start, format)}${separator}${formatDate(end, format)}`;
94
+ }
95
+
96
+ /**
97
+ * Formatted date-range input with an anchored two-month range calendar.
98
+ * @fires DateRangeBox#change
99
+ * @fires DateRangeBox#invalid
100
+ * @fires DateRangeBox#open
101
+ * @fires DateRangeBox#close
102
+ * @extends {Component<DateRangeBoxOptions>}
103
+ */
104
+ export class DateRangeBox extends Component {
105
+ static cssName = 'date-range-box';
106
+
107
+ /** @type {DateRangeBoxOptions} */
108
+ static defaults = {
109
+ start: null,
110
+ end: null,
111
+ format: '%d.%m.%Y',
112
+ separator: ' – ',
113
+ placeholder: null,
114
+ clearable: true,
115
+ min: null,
116
+ max: null,
117
+ months: 2,
118
+ weekStart: 1,
119
+ showWeekNumbers: false,
120
+ minNights: 0,
121
+ maxNights: null,
122
+ presets: [],
123
+ disabled: false
124
+ };
125
+
126
+ // State used by render() is initialized inside it, never as class fields: render() runs from the
127
+ // base constructor, before field initializers would reset it (see AGENTS.md).
128
+
129
+ /** @returns {HTMLElement} */
130
+ render() {
131
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
132
+ this.el = root;
133
+ this._originalState = root.getAttribute('data-state');
134
+ this._originalAriaDisabled = root.getAttribute('aria-disabled');
135
+ this._format = String(this.options.format || '%d.%m.%Y');
136
+ this._separator = String(this.options.separator ?? ' – ');
137
+ this._disabled = Boolean(this.options.disabled);
138
+ this._open = false;
139
+ this._position = null;
140
+ this._range = this._coerce({ start: this.options.start, end: this.options.end });
141
+ const popoverId = uid('zx-date-range-box-popover');
142
+
143
+ const content = h('div', { class: 'zx-date-range-box__content' },
144
+ h('div', { ref: 'control', class: 'zx-date-range-box__control' },
145
+ h('input', {
146
+ ref: 'input',
147
+ class: 'zx-date-range-box__input',
148
+ type: 'text',
149
+ autocomplete: 'off',
150
+ placeholder: this.options.placeholder ?? this._placeholder(),
151
+ 'aria-haspopup': 'grid',
152
+ ariaControls: popoverId,
153
+ ariaExpanded: 'false'
154
+ }),
155
+ h('button', {
156
+ ref: 'clear',
157
+ class: 'zx-icon-btn zx-date-range-box__clear',
158
+ type: 'button',
159
+ ariaLabel: text('daterange.clear', 'Clear date range')
160
+ }, icon('x', { size: 13 })),
161
+ h('button', {
162
+ ref: 'toggle',
163
+ class: 'zx-date-range-box__toggle',
164
+ type: 'button',
165
+ ariaLabel: text('daterange.choose', 'Choose date range'),
166
+ 'aria-haspopup': 'grid',
167
+ ariaControls: popoverId,
168
+ ariaExpanded: 'false'
169
+ }, icon('calendar'))
170
+ ),
171
+ h('div', {
172
+ ref: 'popover',
173
+ id: popoverId,
174
+ class: 'zx-date-range-box__popover',
175
+ popover: 'manual',
176
+ dataset: { state: 'closed' }
177
+ })
178
+ );
179
+ this._content = content;
180
+ root.append(content);
181
+
182
+ this._picker = new DateRangePicker(null, {
183
+ start: this._range.start,
184
+ end: this._range.end,
185
+ min: this.options.min,
186
+ max: this.options.max,
187
+ months: this.options.months,
188
+ weekStart: this.options.weekStart,
189
+ showWeekNumbers: this.options.showWeekNumbers,
190
+ minNights: this.options.minNights,
191
+ maxNights: this.options.maxNights,
192
+ presets: this.options.presets
193
+ });
194
+ this.refs.popover.append(this._picker.el);
195
+ // `select` mirrors the half-open state into the input; `change` commits and closes.
196
+ this._picker.on('select', (event) => {
197
+ this._range = { start: event.detail.start, end: event.detail.end };
198
+ this._setValid();
199
+ this._syncDisplay();
200
+ });
201
+ this._picker.on('change', (event) => {
202
+ this._apply({ start: event.detail.start, end: event.detail.end }, { push: false });
203
+ if (event.detail.start && event.detail.end) this._close(true);
204
+ });
205
+
206
+ // Moving focus into the open calendar blurs the input; committing then would turn a
207
+ // half-picked range into a one-day range behind the user's back.
208
+ this.listen(this.refs.input, 'blur', () => {
209
+ if (!this._open) this._commitText();
210
+ });
211
+ this.listen(this.refs.input, 'keydown', (event) => this._onInputKeydown(event));
212
+ this.listen(this.refs.toggle, 'click', () => this._open ? this.close() : this.open());
213
+ this.listen(this.refs.toggle, 'keydown', (event) => {
214
+ if (event.key !== 'ArrowDown') return;
215
+ event.preventDefault();
216
+ this.open();
217
+ });
218
+ this.listen(this.refs.clear, 'click', () => {
219
+ this.clear();
220
+ this.focus();
221
+ });
222
+ this.listen(document, 'keydown', (event) => {
223
+ if (!this._open || event.key !== 'Escape') return;
224
+ event.preventDefault();
225
+ this._close(true);
226
+ });
227
+ this.listen(document, 'pointerdown', (event) => {
228
+ if (this._open && !root.contains(event.target)) this.close();
229
+ });
230
+
231
+ this._syncDisplay();
232
+ this._syncDisabled();
233
+ return root;
234
+ }
235
+
236
+ /**
237
+ * Returns the selected range as dates or as Unix timestamps.
238
+ * @param {DateRangeBoxUnit} [unit='date'] Return unit.
239
+ * @returns {DateRange|DateRangeSeconds}
240
+ */
241
+ get(unit = 'date') {
242
+ if (unit === 'seconds') {
243
+ return {
244
+ start: this._range.start ? Math.floor(this._range.start.getTime() / 1000) : null,
245
+ end: this._range.end ? Math.floor(this._range.end.getTime() / 1000) : null
246
+ };
247
+ }
248
+ return {
249
+ start: this._range.start ? new Date(this._range.start.getTime()) : null,
250
+ end: this._range.end ? new Date(this._range.end.getTime()) : null
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Sets a range. Each endpoint may be a Date, Unix seconds, or text in the configured format;
256
+ * a whole formatted range string and `null` are accepted too.
257
+ * @param {DateRange|{start: DateRangeInput, end: DateRangeInput}|string|null} range New range.
258
+ * @param {DateRangeBoxSetOptions} [options={}] Update behavior.
259
+ * @returns {this}
260
+ * @fires DateRangeBox#change
261
+ */
262
+ set(range, { silent = false } = {}) {
263
+ this._apply(this._coerce(range), { silent });
264
+ return this;
265
+ }
266
+
267
+ /**
268
+ * Clears the selection.
269
+ * @param {DateRangeBoxSetOptions} [options={}] Update behavior.
270
+ * @returns {this}
271
+ * @fires DateRangeBox#change
272
+ */
273
+ clear({ silent = false } = {}) {
274
+ return this.set({ start: null, end: null }, { silent });
275
+ }
276
+
277
+ /**
278
+ * Opens and positions the range calendar, then moves focus into its grid.
279
+ * @returns {this}
280
+ * @fires DateRangeBox#open
281
+ */
282
+ open() {
283
+ if (this._disabled || this._open) return this;
284
+ this._picker.set(this._range, { silent: true });
285
+ this._open = true;
286
+ this.refs.popover.dataset.state = 'open';
287
+ this.refs.input.setAttribute('aria-expanded', 'true');
288
+ this.refs.toggle.setAttribute('aria-expanded', 'true');
289
+ this._position = position(this.refs.control, this.refs.popover, {
290
+ placement: 'bottom-start',
291
+ flip: true
292
+ });
293
+ this._picker.focus();
294
+ this.emit('open');
295
+ return this;
296
+ }
297
+
298
+ /**
299
+ * Closes the range calendar.
300
+ * @returns {this}
301
+ * @fires DateRangeBox#close
302
+ */
303
+ close() {
304
+ this._close(false);
305
+ return this;
306
+ }
307
+
308
+ /** Enables the box. @returns {this} */
309
+ enable() {
310
+ this._disabled = false;
311
+ this._syncDisabled();
312
+ return this;
313
+ }
314
+
315
+ /** Disables the box and closes its popover. @returns {this} */
316
+ disable() {
317
+ this._disabled = true;
318
+ this.close();
319
+ this._syncDisabled();
320
+ return this;
321
+ }
322
+
323
+ /** Moves focus to the text input. @returns {this} */
324
+ focus() {
325
+ this.refs.input.focus();
326
+ return this;
327
+ }
328
+
329
+ /** Cleans up the open popover, child picker, and generated host content. @returns {void} */
330
+ destroy() {
331
+ this._position?.destroy();
332
+ this._position = null;
333
+ this._open = false;
334
+ this._picker?.destroy();
335
+ this._picker = null;
336
+ this._content?.remove();
337
+ restoreAttribute(this.el, 'data-state', this._originalState);
338
+ restoreAttribute(this.el, 'aria-disabled', this._originalAriaDisabled);
339
+ super.destroy();
340
+ }
341
+
342
+ /* --------------------------------------------------------------------- internals -- */
343
+
344
+ /**
345
+ * Stores a range, optionally pushing it back into the picker, and reports it.
346
+ * @param {DateRange} range Already-coerced range.
347
+ * @param {{silent?: boolean, push?: boolean}} [options={}] Update behavior.
348
+ * @returns {void}
349
+ * @fires DateRangeBox#change
350
+ */
351
+ _apply(range, { silent = false, push = true } = {}) {
352
+ this._range = range;
353
+ if (push) this._picker?.set(this._range, { silent: true });
354
+ this._setValid();
355
+ this._syncDisplay();
356
+ if (!silent) this.emit('change', this.get());
357
+ }
358
+
359
+ /** @param {KeyboardEvent} event @returns {void} */
360
+ _onInputKeydown(event) {
361
+ if (event.key === 'ArrowDown') {
362
+ event.preventDefault();
363
+ this.open();
364
+ } else if (event.key === 'Enter') {
365
+ event.preventDefault();
366
+ this._commitText();
367
+ } else if (event.key === 'Escape' && this._open) {
368
+ event.preventDefault();
369
+ this._close(true);
370
+ }
371
+ }
372
+
373
+ /** @returns {void} */
374
+ _commitText() {
375
+ if (this._disabled) return;
376
+ const value = this.refs.input.value.trim();
377
+ if (value === '') {
378
+ this.set({ start: null, end: null });
379
+ return;
380
+ }
381
+ const parsed = parseRangeText(value, this._format, this._separator);
382
+ if (!parsed) {
383
+ this.el.dataset.state = 'invalid';
384
+ this.refs.input.setAttribute('aria-invalid', 'true');
385
+ this.emit('invalid', { text: value });
386
+ return;
387
+ }
388
+ this._apply(this._clamp(parsed));
389
+ }
390
+
391
+ /**
392
+ * Coerces any accepted input shape into a bounded range of local dates.
393
+ * @param {unknown} value Range-ish input.
394
+ * @returns {DateRange}
395
+ */
396
+ _coerce(value) {
397
+ if (value === null || value === undefined) return { start: null, end: null };
398
+ if (typeof value === 'string') {
399
+ return this._clamp(parseRangeText(value, this._format, this._separator) ?? { start: null, end: null });
400
+ }
401
+ if (Array.isArray(value)) {
402
+ return this._clamp({ start: this._toDate(value[0]), end: this._toDate(value[1]) });
403
+ }
404
+ if (typeof value !== 'object') return { start: null, end: null };
405
+ return this._clamp({
406
+ start: this._toDate(/** @type {Record<string, unknown>} */ (value).start),
407
+ end: this._toDate(/** @type {Record<string, unknown>} */ (value).end)
408
+ });
409
+ }
410
+
411
+ /** @param {unknown} value @returns {Date|null} */
412
+ _toDate(value) {
413
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : new Date(value.getTime());
414
+ if (typeof value === 'number' && Number.isFinite(value)) return new Date(value * 1000);
415
+ if (typeof value === 'string' && value.trim() !== '') return parseDate(value.trim(), this._format);
416
+ return null;
417
+ }
418
+
419
+ /** @param {DateRange} range @returns {DateRange} */
420
+ _clamp(range) {
421
+ return clampRange(range, {
422
+ min: this.options.min,
423
+ max: this.options.max,
424
+ minNights: this.options.minNights,
425
+ maxNights: this.options.maxNights
426
+ });
427
+ }
428
+
429
+ /** @returns {string} */
430
+ _placeholder() {
431
+ const one = this._format.replace(/%[demYy]/g, (token) => PLACEHOLDER_TOKENS[token] ?? token);
432
+ return `${one}${this._separator}${one}`;
433
+ }
434
+
435
+ /** @returns {void} */
436
+ _syncDisplay() {
437
+ this.refs.input.value = formatRangeText(this._range, this._format, this._separator);
438
+ this.refs.clear.hidden = !this.options.clearable || !(this._range.start || this._range.end);
439
+ }
440
+
441
+ /** @returns {void} */
442
+ _syncDisabled() {
443
+ this.refs.input.disabled = this._disabled;
444
+ this.refs.toggle.disabled = this._disabled;
445
+ this.refs.clear.disabled = this._disabled;
446
+ this.el.setAttribute('aria-disabled', String(this._disabled));
447
+ if (this._disabled) this._picker?.disable();
448
+ else this._picker?.enable();
449
+ }
450
+
451
+ /** @returns {void} */
452
+ _setValid() {
453
+ if (this.el.dataset.state === 'invalid') restoreAttribute(this.el, 'data-state', this._originalState);
454
+ this.refs.input.removeAttribute('aria-invalid');
455
+ }
456
+
457
+ /** @param {boolean} returnFocus @returns {void} */
458
+ _close(returnFocus) {
459
+ if (!this._open) return;
460
+ this._position?.destroy();
461
+ this._position = null;
462
+ this._open = false;
463
+ this.refs.popover.dataset.state = 'closed';
464
+ this.refs.input.setAttribute('aria-expanded', 'false');
465
+ this.refs.toggle.setAttribute('aria-expanded', 'false');
466
+ if (returnFocus) this.focus();
467
+ this.emit('close');
468
+ }
469
+ }
470
+
471
+ /** @event DateRangeBox#change @type {CustomEvent<DateRange>} */
472
+ /** @event DateRangeBox#invalid @type {CustomEvent<{text: string}>} */
473
+ /** @event DateRangeBox#open @type {CustomEvent<Record<string, never>>} */
474
+ /** @event DateRangeBox#close @type {CustomEvent<Record<string, never>>} */
475
+
476
+ /**
477
+ * Resolves a built-in string through the host translator, falling back to the English original.
478
+ * @param {string} key Translation key.
479
+ * @param {string} fallback English text used when the key is untranslated.
480
+ * @param {unknown[]} [args] Interpolation values for `%1`, `%2`, …
481
+ * @returns {string}
482
+ */
483
+ function text(key, fallback, args) {
484
+ const translated = translate(key);
485
+ return printf(translated === key ? fallback : translated, args);
486
+ }
487
+
488
+ /** @param {Element} element @param {string} name @param {string|null} value @returns {void} */
489
+ function restoreAttribute(element, name, value) {
490
+ if (value === null) element.removeAttribute(name);
491
+ else element.setAttribute(name, value);
492
+ }
@@ -0,0 +1,108 @@
1
+ .zx-datebox {
2
+ display: inline-block;
3
+ color: var(--zx-color-text);
4
+ font-size: var(--zx-control-font-size);
5
+ }
6
+
7
+ .zx-datebox__content,
8
+ .zx-datebox__control {
9
+ display: inline-flex;
10
+ align-items: stretch;
11
+ }
12
+
13
+ .zx-datebox__control {
14
+ position: relative;
15
+ border: 1px solid var(--zx-color-border-control);
16
+ border-radius: var(--zx-control-radius);
17
+ background: var(--zx-color-bg-control);
18
+ }
19
+
20
+ .zx-datebox__control:focus-within {
21
+ box-shadow: var(--zx-focus-ring);
22
+ }
23
+
24
+ .zx-datebox[data-state="invalid"] .zx-datebox__control {
25
+ border-color: var(--zx-color-danger);
26
+ background: var(--zx-color-danger-bg);
27
+ }
28
+
29
+ /* The input always reserves room for the clear button, which floats over its trailing edge —
30
+ showing or hiding the button must not change the control's total width. */
31
+ .zx-datebox__input {
32
+ min-inline-size: 10em;
33
+ border: 0;
34
+ border-radius: var(--zx-control-radius) 0 0 var(--zx-control-radius);
35
+ background: inherit;
36
+ padding-inline-end: var(--zx-control-height);
37
+ }
38
+
39
+ /* Focused controls lift above their siblings so the ring is never clipped. */
40
+ .zx-datebox__input:focus-visible,
41
+ .zx-datebox__toggle:focus-visible {
42
+ position: relative;
43
+ z-index: 1;
44
+ box-shadow: var(--zx-focus-ring);
45
+ }
46
+
47
+ .zx-datebox__clear:focus-visible {
48
+ box-shadow: var(--zx-focus-ring);
49
+ }
50
+
51
+ .zx-datebox__clear,
52
+ .zx-datebox__toggle {
53
+ display: inline-grid;
54
+ place-items: center;
55
+ min-inline-size: var(--zx-control-height);
56
+ padding: 0;
57
+ border: 0;
58
+ border-radius: 0;
59
+ background: inherit;
60
+ color: var(--zx-color-text-muted);
61
+ }
62
+
63
+ .zx-datebox__toggle {
64
+ border-inline-start: 1px solid var(--zx-color-border-control);
65
+ border-radius: 0 var(--zx-control-radius) var(--zx-control-radius) 0;
66
+ }
67
+
68
+ .zx-datebox__clear {
69
+ position: absolute;
70
+ /* The clear button floats over the input's trailing padding, so it has to outrank the input in
71
+ the stacking order — a focused input raises itself to z-index 1 and would otherwise cover the
72
+ button and swallow its clicks. */
73
+ z-index: 2;
74
+ inset-block-start: 50%;
75
+ /* The picker toggle sits to the right of the input; float the clear button just inside it. */
76
+ inset-inline-end: var(--zx-control-height);
77
+ transform: translateY(-50%);
78
+ background: none;
79
+ }
80
+
81
+ .zx-datebox__clear:hover {
82
+ color: var(--zx-color-text);
83
+ }
84
+
85
+ .zx-datebox__clear[hidden] {
86
+ display: none;
87
+ }
88
+
89
+ .zx-datebox__popover {
90
+ overflow: visible;
91
+ max-inline-size: calc(100vw - var(--zx-space-4));
92
+ border: 0;
93
+ background: var(--zx-color-bg-raised);
94
+ padding: 0;
95
+ }
96
+
97
+ .zx-datebox__popover:not(:popover-open)[data-state="closed"] {
98
+ display: none;
99
+ }
100
+
101
+ .zx-datebox__popover[data-state="open"] {
102
+ display: block;
103
+ }
104
+
105
+ .zx-datebox__popover .zx-date-picker__surface {
106
+ background: var(--zx-color-bg-raised);
107
+ box-shadow: var(--zx-overlay-shadow);
108
+ }