@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,283 @@
1
+ // @ts-check
2
+ import { getLanguage } from './i18n.js';
3
+
4
+ /**
5
+ * @typedef {Object} NumberFormatOptions
6
+ * @property {string} [locale=getLanguage()] BCP 47 language tag; defaults to the active language.
7
+ * @property {number} [decimals] Shorthand setting both the minimum and maximum fraction digits.
8
+ * @property {number} [minDecimals] Minimum fraction digits.
9
+ * @property {number} [maxDecimals] Maximum fraction digits.
10
+ * @property {boolean} [group=true] Whether to insert the locale's grouping separators.
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} CurrencyFormatOptions
15
+ * @property {string} [locale=getLanguage()] BCP 47 language tag.
16
+ * @property {number} [decimals] Fraction digits; defaults to the currency's own minor-unit count.
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} PercentFormatOptions
21
+ * @property {string} [locale=getLanguage()] BCP 47 language tag.
22
+ * @property {number} [decimals=0] Fraction digits.
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} FileSizeFormatOptions
27
+ * @property {string} [locale=getLanguage()] BCP 47 language tag.
28
+ * @property {number} [decimals=1] Maximum fraction digits; trailing zeros are dropped.
29
+ * @property {'iec'|'si'} [standard='iec'] Binary KiB/MiB/GiB or decimal kB/MB/GB units.
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} RelativeTimeFormatOptions
34
+ * @property {Date|number|string} [now=new Date()] Reference point the date is measured against.
35
+ * @property {string} [locale=getLanguage()] BCP 47 language tag.
36
+ * @property {'auto'|'always'} [numeric='auto'] `auto` produces "yesterday", `always` "1 day ago".
37
+ */
38
+
39
+ /** Fraction-digit bounds accepted by `Intl.NumberFormat`. */
40
+ const MIN_DIGITS = 0;
41
+ const MAX_DIGITS = 20;
42
+ /** `Intl.NumberFormat`'s own default maximum for plain decimal formatting. */
43
+ const DEFAULT_MAX_DIGITS = 3;
44
+
45
+ const IEC_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
46
+ const SI_UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB'];
47
+
48
+ /**
49
+ * Largest-first units with their length in milliseconds and the threshold below which the unit is
50
+ * still the sensible choice: 45 s, 45 min, 22 h, 6 d, 4 w, 11 months.
51
+ */
52
+ const TIME_DIVISIONS = [
53
+ { unit: 'second', ms: 1000, limit: 45 },
54
+ { unit: 'minute', ms: 60000, limit: 45 },
55
+ { unit: 'hour', ms: 3600000, limit: 22 },
56
+ { unit: 'day', ms: 86400000, limit: 6 },
57
+ { unit: 'week', ms: 604800000, limit: 4 },
58
+ { unit: 'month', ms: 2629800000, limit: 11 },
59
+ { unit: 'year', ms: 31557600000, limit: Infinity }
60
+ ];
61
+
62
+ /**
63
+ * Intl formatters are expensive to construct and tables call these helpers once per cell, so every
64
+ * instance is cached under its locale and options.
65
+ * @type {Map<string, Intl.NumberFormat>}
66
+ */
67
+ const numberFormatters = new Map();
68
+ /** @type {Map<string, Intl.RelativeTimeFormat>} */
69
+ const relativeFormatters = new Map();
70
+
71
+ /**
72
+ * Formats a number for the active language.
73
+ * @param {number|string|null|undefined} value Value to format.
74
+ * @param {NumberFormatOptions} [options={}] Formatting options.
75
+ * @returns {string} Formatted number, or an empty string for null, undefined, and NaN.
76
+ */
77
+ export function formatNumber(value, options = {}) {
78
+ const number = toFiniteNumber(value);
79
+ if (number === null) return '';
80
+ const { locale = getLanguage(), decimals, minDecimals, maxDecimals, group = true } = options;
81
+ return format(locale, digitOptions({ decimals, minDecimals, maxDecimals, group }), number);
82
+ }
83
+
84
+ /**
85
+ * Formats a monetary amount, falling back to plain number formatting when the currency code is
86
+ * missing or not shaped like an ISO 4217 code.
87
+ * @param {number|string|null|undefined} value Amount to format.
88
+ * @param {string|null|undefined} currency Three-letter ISO 4217 code such as `EUR`.
89
+ * @param {CurrencyFormatOptions} [options={}] Formatting options.
90
+ * @returns {string} Formatted amount, or an empty string for null, undefined, and NaN.
91
+ */
92
+ export function formatCurrency(value, currency, options = {}) {
93
+ const number = toFiniteNumber(value);
94
+ if (number === null) return '';
95
+ const { locale = getLanguage(), decimals } = options;
96
+ const code = currencyCode(currency);
97
+ if (code === null) return formatNumber(number, { locale, decimals });
98
+
99
+ const digits = digitOptions({ decimals, group: true });
100
+ return format(locale, { ...digits, style: 'currency', currency: code }, number);
101
+ }
102
+
103
+ /**
104
+ * Formats a fraction as a percentage: `0.42` becomes `42%`.
105
+ * @param {number|string|null|undefined} value Fraction to format.
106
+ * @param {PercentFormatOptions} [options={}] Formatting options.
107
+ * @returns {string} Formatted percentage, or an empty string for null, undefined, and NaN.
108
+ */
109
+ export function formatPercent(value, options = {}) {
110
+ const number = toFiniteNumber(value);
111
+ if (number === null) return '';
112
+ const { locale = getLanguage(), decimals = 0 } = options;
113
+ return format(locale, { ...digitOptions({ decimals, group: true }), style: 'percent' }, number);
114
+ }
115
+
116
+ /**
117
+ * Formats a byte count with binary (KiB) or decimal (kB) units.
118
+ * @param {number|string|null|undefined} bytes Byte count.
119
+ * @param {FileSizeFormatOptions} [options={}] Formatting options.
120
+ * @returns {string} Formatted size, `'0 B'` for zero, or an empty string for null and NaN.
121
+ */
122
+ export function formatFileSize(bytes, options = {}) {
123
+ const number = toFiniteNumber(bytes);
124
+ if (number === null) return '';
125
+ const { locale = getLanguage(), decimals = 1, standard = 'iec' } = options;
126
+ if (number === 0) return `${formatNumber(0, { locale, decimals: 0 })} B`;
127
+
128
+ const units = standard === 'si' ? SI_UNITS : IEC_UNITS;
129
+ const base = standard === 'si' ? 1000 : 1024;
130
+ const absolute = Math.abs(number);
131
+ let exponent = Math.min(units.length - 1, Math.max(0, Math.floor(Math.log(absolute) / Math.log(base))));
132
+ let scaled = absolute / base ** exponent;
133
+ // Rounding can push a value up into the next unit (1023.9 KiB at one decimal is 1024.0 KiB).
134
+ // Whole bytes never get fraction digits, so the check runs against the digits actually used.
135
+ if (roundTo(scaled, exponent === 0 ? 0 : decimals) >= base && exponent < units.length - 1) {
136
+ exponent += 1;
137
+ scaled = absolute / base ** exponent;
138
+ }
139
+
140
+ const signed = number < 0 ? -scaled : scaled;
141
+ const digits = exponent === 0 ? 0 : decimals;
142
+ const text = formatNumber(signed, { locale, minDecimals: 0, maxDecimals: digits });
143
+ return `${text} ${units[exponent]}`;
144
+ }
145
+
146
+ /**
147
+ * Formats a date relative to a reference point, picking the largest sensible unit.
148
+ * @param {Date|number|string|null|undefined} date Date, Unix timestamp in seconds, or ISO string.
149
+ * @param {RelativeTimeFormatOptions} [options={}] Formatting options.
150
+ * @returns {string} Formatted phrase, or an empty string when the date cannot be read.
151
+ */
152
+ export function formatRelativeTime(date, options = {}) {
153
+ const target = toDate(date);
154
+ if (target === null) return '';
155
+ const { now = new Date(), locale = getLanguage(), numeric = 'auto' } = options;
156
+ const reference = toDate(now);
157
+ if (reference === null) return '';
158
+
159
+ const diff = target.getTime() - reference.getTime();
160
+ const division = TIME_DIVISIONS.find(({ ms, limit }) => Math.abs(diff) / ms < limit)
161
+ ?? TIME_DIVISIONS[TIME_DIVISIONS.length - 1];
162
+ const value = Math.round(diff / division.ms);
163
+ return relativeFormatter(locale, numeric)
164
+ .format(value, /** @type {Intl.RelativeTimeFormatUnit} */ (division.unit));
165
+ }
166
+
167
+ /**
168
+ * Returns a cached `Intl.NumberFormat`, falling back to the runtime default for unusable locales.
169
+ * @param {string} locale BCP 47 language tag.
170
+ * @param {Intl.NumberFormatOptions} options Formatter options.
171
+ * @param {number} value Number to format.
172
+ * @returns {string}
173
+ */
174
+ function format(locale, options, value) {
175
+ const key = `${locale}\u0000${JSON.stringify(options)}`;
176
+ let formatter = numberFormatters.get(key);
177
+ if (formatter === undefined) {
178
+ formatter = createNumberFormat(locale, options);
179
+ numberFormatters.set(key, formatter);
180
+ }
181
+ return formatter.format(value);
182
+ }
183
+
184
+ /**
185
+ * @param {string} locale BCP 47 language tag.
186
+ * @param {Intl.NumberFormatOptions} options Formatter options.
187
+ * @returns {Intl.NumberFormat}
188
+ */
189
+ function createNumberFormat(locale, options) {
190
+ try {
191
+ return new Intl.NumberFormat(locale, options);
192
+ } catch {
193
+ try {
194
+ return new Intl.NumberFormat(undefined, options);
195
+ } catch {
196
+ // Unusable options (an unsupported currency style, say) still have to produce digits.
197
+ return new Intl.NumberFormat();
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * @param {string} locale BCP 47 language tag.
204
+ * @param {'auto'|'always'} numeric Numeric-phrasing mode.
205
+ * @returns {Intl.RelativeTimeFormat}
206
+ */
207
+ function relativeFormatter(locale, numeric) {
208
+ const key = `${locale}\u0000${numeric}`;
209
+ let formatter = relativeFormatters.get(key);
210
+ if (formatter === undefined) {
211
+ try {
212
+ formatter = new Intl.RelativeTimeFormat(locale, { numeric });
213
+ } catch {
214
+ formatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
215
+ }
216
+ relativeFormatters.set(key, formatter);
217
+ }
218
+ return formatter;
219
+ }
220
+
221
+ /**
222
+ * Turns the decimals shorthand and its explicit siblings into `Intl.NumberFormat` digit options.
223
+ * @param {{decimals?: number, minDecimals?: number, maxDecimals?: number, group?: boolean}} options
224
+ * @returns {Intl.NumberFormatOptions}
225
+ */
226
+ function digitOptions({ decimals, minDecimals, maxDecimals, group = true }) {
227
+ /** @type {Intl.NumberFormatOptions} */
228
+ const result = { useGrouping: group !== false };
229
+ const min = digitCount(minDecimals ?? decimals);
230
+ const max = digitCount(maxDecimals ?? decimals);
231
+
232
+ if (min !== null) result.minimumFractionDigits = min;
233
+ if (max !== null) {
234
+ result.maximumFractionDigits = max;
235
+ // Intl throws when the minimum exceeds the maximum, so the pair is reconciled here instead.
236
+ if (min !== null && min > max) result.minimumFractionDigits = max;
237
+ } else if (min !== null) {
238
+ result.maximumFractionDigits = Math.max(min, DEFAULT_MAX_DIGITS);
239
+ }
240
+ return result;
241
+ }
242
+
243
+ /** @param {unknown} value @returns {number|null} */
244
+ function digitCount(value) {
245
+ if (value == null) return null;
246
+ const number = Number(value);
247
+ if (!Number.isFinite(number)) return null;
248
+ return Math.min(MAX_DIGITS, Math.max(MIN_DIGITS, Math.round(number)));
249
+ }
250
+
251
+ /** @param {unknown} value @returns {number|null} */
252
+ function toFiniteNumber(value) {
253
+ if (value === null || value === undefined || value === '') return null;
254
+ const number = typeof value === 'number' ? value : Number(value);
255
+ return Number.isFinite(number) ? number : null;
256
+ }
257
+
258
+ /** @param {unknown} value @returns {string|null} */
259
+ function currencyCode(value) {
260
+ if (typeof value !== 'string') return null;
261
+ const code = value.trim().toUpperCase();
262
+ return /^[A-Z]{3}$/.test(code) ? code : null;
263
+ }
264
+
265
+ /** @param {Date|number|string|null|undefined} value @returns {Date|null} */
266
+ function toDate(value) {
267
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
268
+ if (typeof value === 'number') return Number.isFinite(value) ? new Date(value * 1000) : null;
269
+ if (typeof value === 'string') {
270
+ const text = value.trim();
271
+ if (text === '') return null;
272
+ // Unix seconds arrive as strings from ZeyOS kernel payloads often enough to accept them here.
273
+ const parsed = /^-?\d+$/.test(text) ? new Date(Number(text) * 1000) : new Date(text);
274
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
275
+ }
276
+ return null;
277
+ }
278
+
279
+ /** @param {number} value @param {number} digits @returns {number} */
280
+ function roundTo(value, digits) {
281
+ const factor = 10 ** digits;
282
+ return Math.round(value * factor) / factor;
283
+ }
@@ -0,0 +1,246 @@
1
+ // @ts-check
2
+ /**
3
+ * @typedef {Object} HttpOptions
4
+ * @property {string} [base=''] URL prefix.
5
+ * @property {Record<string, string>} [headers={}] Default request headers.
6
+ * @property {((error: Error, context: {path: string, options: RequestOptions}) => void)|null} [onError=null]
7
+ * @property {number} [timeout=30000] Timeout in milliseconds; zero disables it.
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} RequestOptions
12
+ * @property {string} [method='GET'] HTTP method.
13
+ * @property {unknown} [data=null] JSON or form data.
14
+ * @property {File|FileList|File[]} [files=null] Files that switch the body to FormData.
15
+ * @property {string|URLSearchParams|Record<string, unknown>} [query=null] Query parameters.
16
+ * @property {Record<string, string>} [headers={}] Per-request headers.
17
+ */
18
+
19
+ /** Dependency-free JSON fetch wrapper with legacy ZeyOS envelope handling. */
20
+ export class Http {
21
+ /** @type {string} */
22
+ base;
23
+ /** @type {Readonly<Record<string, string>>} */
24
+ headers;
25
+ /** @type {HttpOptions['onError']} */
26
+ onError;
27
+ /** @type {number} */
28
+ timeout;
29
+
30
+ /**
31
+ * @param {HttpOptions} [options={}] Client defaults.
32
+ */
33
+ constructor({ base = '', headers = {}, onError = null, timeout = 30000 } = {}) {
34
+ this.base = String(base);
35
+ this.headers = Object.freeze({ ...headers });
36
+ this.onError = onError;
37
+ this.timeout = Number(timeout);
38
+ }
39
+
40
+ /**
41
+ * Performs a request and returns its parsed JSON payload or legacy `result` value.
42
+ * @param {string} path Path relative to the configured base.
43
+ * @param {RequestOptions} [options={}] Request settings.
44
+ * @returns {Promise<any>}
45
+ */
46
+ async request(path, options = {}) {
47
+ const {
48
+ method = 'GET', data = null, files = null, query = null, headers = {}
49
+ } = options;
50
+ const controller = new AbortController();
51
+ const timeoutId = this.timeout > 0 ? setTimeout(() => controller.abort(), this.timeout) : null;
52
+ const requestHeaders = new Headers(this.headers);
53
+ for (const [name, value] of Object.entries(headers)) requestHeaders.set(name, value);
54
+ requestHeaders.set('Accept', requestHeaders.get('Accept') ?? 'application/json');
55
+
56
+ let body;
57
+ if (files !== null) {
58
+ body = createFormData(data, files);
59
+ requestHeaders.delete('Content-Type');
60
+ } else if (data !== null && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
61
+ body = JSON.stringify(data);
62
+ requestHeaders.set('Content-Type', requestHeaders.get('Content-Type') ?? 'application/json');
63
+ }
64
+
65
+ const url = appendQuery(this.base + path, query);
66
+ try {
67
+ const response = await fetch(url, {
68
+ method: method.toUpperCase(),
69
+ headers: requestHeaders,
70
+ body,
71
+ signal: controller.signal
72
+ });
73
+ const text = await response.text();
74
+ let json = null;
75
+ if (text !== '') {
76
+ try {
77
+ json = JSON.parse(text);
78
+ } catch (cause) {
79
+ const error = /** @type {Error & {response?: Response}} */ (
80
+ new Error('Invalid JSON response', { cause })
81
+ );
82
+ error.response = response;
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ if (!response.ok) {
88
+ try {
89
+ parseResult(json);
90
+ } catch (error) {
91
+ /** @type {Error & {response?: Response}} */ (error).response = response;
92
+ throw error;
93
+ }
94
+ const error = /** @type {Error & {response?: Response}} */ (
95
+ new Error(`HTTP ${response.status} ${response.statusText}`.trim())
96
+ );
97
+ error.response = response;
98
+ throw error;
99
+ }
100
+ return parseResult(json);
101
+ } catch (caught) {
102
+ const error = caught instanceof Error ? caught : new Error(String(caught));
103
+ if (this.onError) {
104
+ try {
105
+ this.onError(error, { path, options });
106
+ } catch {
107
+ // Observer failures do not hide the request failure.
108
+ }
109
+ }
110
+ throw error;
111
+ } finally {
112
+ if (timeoutId !== null) clearTimeout(timeoutId);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Sends a GET request with query parameters.
118
+ * @param {string} path Request path.
119
+ * @param {RequestOptions['query']} [query=null] Query parameters.
120
+ * @returns {Promise<any>}
121
+ */
122
+ get(path, query = null) {
123
+ return this.request(path, { method: 'GET', query });
124
+ }
125
+
126
+ /**
127
+ * Sends a POST request with a JSON body.
128
+ * @param {string} path Request path.
129
+ * @param {unknown} [data=null] Request data.
130
+ * @returns {Promise<any>}
131
+ */
132
+ post(path, data = null) {
133
+ return this.request(path, { method: 'POST', data });
134
+ }
135
+
136
+ /**
137
+ * Sends a PUT request with a JSON body.
138
+ * @param {string} path Request path.
139
+ * @param {unknown} [data=null] Request data.
140
+ * @returns {Promise<any>}
141
+ */
142
+ put(path, data = null) {
143
+ return this.request(path, { method: 'PUT', data });
144
+ }
145
+
146
+ /**
147
+ * Sends a DELETE request with a JSON body.
148
+ * @param {string} path Request path.
149
+ * @param {unknown} [data=null] Request data.
150
+ * @returns {Promise<any>}
151
+ */
152
+ delete(path, data = null) {
153
+ return this.request(path, { method: 'DELETE', data });
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Creates an Http client for a ZeyOS remote-call service.
159
+ * @param {string} service Service name.
160
+ * @param {string} [accesskey=''] Optional service access key.
161
+ * @param {HttpOptions & {apiBase?: string}} [options={}] Client and API-base options.
162
+ * @returns {Http}
163
+ */
164
+ export function zeyosService(service, accesskey = '', options = {}) {
165
+ const { apiBase = '../remotecall/', ...httpOptions } = options;
166
+ const base = `${apiBase}${service}${accesskey ? `:${accesskey}` : ''}/`;
167
+ return new Http({ ...httpOptions, base });
168
+ }
169
+
170
+ /**
171
+ * Unwraps a legacy `{result}` response or throws a legacy `{error}` response.
172
+ * JSON values without either envelope key pass through unchanged.
173
+ * @param {any} json Parsed JSON value.
174
+ * @returns {any}
175
+ */
176
+ export function parseResult(json) {
177
+ if (json && typeof json === 'object') {
178
+ if (Object.prototype.hasOwnProperty.call(json, 'error') && json.error != null) {
179
+ const message = typeof json.error === 'string' ? json.error : JSON.stringify(json.error);
180
+ throw new Error(message || 'Server error');
181
+ }
182
+ if (Object.prototype.hasOwnProperty.call(json, 'result')) return json.result;
183
+ }
184
+ return json;
185
+ }
186
+
187
+ /**
188
+ * @param {string} url
189
+ * @param {RequestOptions['query']} query
190
+ * @returns {string}
191
+ */
192
+ function appendQuery(url, query) {
193
+ if (query == null) return url;
194
+ let encoded;
195
+ if (typeof query === 'string') encoded = query.replace(/^\?/, '');
196
+ else if (query instanceof URLSearchParams) encoded = query.toString();
197
+ else {
198
+ const parameters = new URLSearchParams();
199
+ for (const [key, value] of Object.entries(query)) appendParameter(parameters, key, value);
200
+ encoded = parameters.toString();
201
+ }
202
+ if (!encoded) return url;
203
+ return url + (url.includes('?') ? '&' : '?') + encoded;
204
+ }
205
+
206
+ /** @param {URLSearchParams} parameters @param {string} key @param {unknown} value @returns {void} */
207
+ function appendParameter(parameters, key, value) {
208
+ if (value == null) return;
209
+ if (Array.isArray(value)) {
210
+ for (const item of value) appendParameter(parameters, key, item);
211
+ } else {
212
+ parameters.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
213
+ }
214
+ }
215
+
216
+ /** @param {unknown} data @param {File|FileList|File[]} files @returns {FormData} */
217
+ function createFormData(data, files) {
218
+ const form = new FormData();
219
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
220
+ for (const [key, value] of Object.entries(data)) appendFormValue(form, key, value);
221
+ } else if (data != null) {
222
+ form.append('data', typeof data === 'string' ? data : JSON.stringify(data));
223
+ }
224
+
225
+ const list = isFileList(files) || Array.isArray(files) ? Array.from(files) : [files];
226
+ for (const file of list) form.append('files', file);
227
+ return form;
228
+ }
229
+
230
+ /** @param {FormData} form @param {string} key @param {unknown} value @returns {void} */
231
+ function appendFormValue(form, key, value) {
232
+ if (value == null) return;
233
+ if (Array.isArray(value)) {
234
+ for (const item of value) appendFormValue(form, key, item);
235
+ } else {
236
+ const isBlob = typeof Blob === 'function' && value instanceof Blob;
237
+ form.append(key, typeof value === 'object' && !isBlob
238
+ ? JSON.stringify(value)
239
+ : /** @type {string|Blob} */ (value));
240
+ }
241
+ }
242
+
243
+ /** @param {unknown} value @returns {value is FileList} */
244
+ function isFileList(value) {
245
+ return typeof FileList === 'function' && value instanceof FileList;
246
+ }
@@ -0,0 +1,83 @@
1
+ // @ts-check
2
+ /** @typedef {(key: string) => string|null|undefined} Translator */
3
+
4
+ /** @type {Translator|null} */
5
+ let translator = null;
6
+ let language = 'en';
7
+
8
+ /**
9
+ * Installs the host application's translation function, or clears it with null.
10
+ * @param {Translator|null} fn Translator function.
11
+ * @returns {void}
12
+ */
13
+ export function setTranslator(fn) {
14
+ if (fn !== null && typeof fn !== 'function') {
15
+ throw new TypeError('Translator must be a function or null');
16
+ }
17
+ translator = fn;
18
+ }
19
+
20
+ /**
21
+ * Sets the active language identifier.
22
+ * @param {string} lang Language identifier such as "en" or "de-AT".
23
+ * @returns {void}
24
+ */
25
+ export function setLanguage(lang) {
26
+ language = String(lang || 'en');
27
+ }
28
+
29
+ /**
30
+ * Returns the active language identifier.
31
+ * @returns {string}
32
+ */
33
+ export function getLanguage() {
34
+ return language;
35
+ }
36
+
37
+ /**
38
+ * Translates a key through the injected host translator and interpolates arguments.
39
+ * @param {string} key Translation key.
40
+ * @param {unknown[]|Record<string, unknown>|string|number} [args] Interpolation arguments.
41
+ * @returns {string}
42
+ */
43
+ export function translate(key, args) {
44
+ const translated = translator ? translator(key) : key;
45
+ return printf(translated == null ? key : String(translated), args);
46
+ }
47
+
48
+ /**
49
+ * Replaces one-based numeric placeholders and legacy percent-delimited placeholders.
50
+ * Arrays replace `%1`, `%2`, and legacy placeholders in encounter order; objects replace
51
+ * named placeholders such as `%name%`.
52
+ * @param {string} str Template string.
53
+ * @param {unknown[]|Record<string, unknown>|string|number} [args] Replacement values.
54
+ * @returns {string}
55
+ */
56
+ export function printf(str, args) {
57
+ if (args == null) return String(str);
58
+
59
+ const positional = Array.isArray(args) ? args :
60
+ (typeof args === 'object' ? null : [args]);
61
+ let output = String(str).replace(/%(\d+)/g, (placeholder, indexText) => {
62
+ const index = Number(indexText) - 1;
63
+ let value;
64
+ if (positional) value = positional[index];
65
+ else if (Object.prototype.hasOwnProperty.call(args, indexText)) value = args[indexText];
66
+ return value == null ? placeholder : String(value);
67
+ });
68
+
69
+ let legacyIndex = 0;
70
+ output = output.replace(/%([A-Za-z_][\w.-]*)%/g, (placeholder, key) => {
71
+ if (!positional && Object.prototype.hasOwnProperty.call(args, key)) {
72
+ const value = args[key];
73
+ return value == null ? placeholder : String(value);
74
+ }
75
+ if (positional && legacyIndex < positional.length) {
76
+ const value = positional[legacyIndex];
77
+ legacyIndex += 1;
78
+ return value == null ? placeholder : String(value);
79
+ }
80
+ return placeholder;
81
+ });
82
+ return output;
83
+ }