@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,260 @@
1
+ // @ts-check
2
+ import { escapeRegExp, isElement } from './util.js';
3
+
4
+ /** @typedef {{toElement: () => Node|null}} ElementProvider */
5
+ /** @typedef {Node|string|number|ElementProvider|null|undefined|Array<DomChild>} DomChild */
6
+ /**
7
+ * @typedef {Object} HProps
8
+ * @property {string|string[]} [class] Class name or names.
9
+ * @property {string|Record<string, string|number>} [style] Inline styles.
10
+ * @property {Record<string, unknown>} [dataset] Data properties.
11
+ * @property {string} [ref] Ref name collected by `h.scope()`.
12
+ */
13
+
14
+ /** @type {Record<string, Element>|null} */
15
+ let activeRefs = null;
16
+
17
+ /**
18
+ * Creates an element and appends normalized children.
19
+ * @param {string} tag Element tag name.
20
+ * @param {(HProps & Record<string, any>)|DomChild} [props] Properties and attributes, or the first child.
21
+ * @param {...DomChild} children Child values.
22
+ * @returns {HTMLElement}
23
+ */
24
+ export function h(tag, props, ...children) {
25
+ const element = document.createElement(tag);
26
+ let properties = props;
27
+ if (!isPropsObject(properties)) {
28
+ children.unshift(/** @type {DomChild} */ (properties));
29
+ properties = {};
30
+ }
31
+
32
+ for (const [key, value] of Object.entries(properties)) {
33
+ applyProperty(element, key, value);
34
+ }
35
+ appendChildren(element, children);
36
+ return element;
37
+ }
38
+
39
+ /**
40
+ * Runs a render operation with a ref collector active.
41
+ * @template T
42
+ * @param {Record<string, Element>} refs Object populated by `ref` properties.
43
+ * @param {() => T} fn Synchronous render operation.
44
+ * @returns {T}
45
+ */
46
+ h.scope = function scope(refs, fn) {
47
+ const previous = activeRefs;
48
+ activeRefs = refs;
49
+ try {
50
+ return fn();
51
+ } finally {
52
+ activeRefs = previous;
53
+ }
54
+ };
55
+
56
+ /**
57
+ * Parses explicitly trusted component-generated HTML into a document fragment.
58
+ * User and server content must never be passed to this function.
59
+ * @param {string} html Trusted HTML string.
60
+ * @returns {DocumentFragment}
61
+ */
62
+ h.raw = function raw(html) {
63
+ const template = document.createElement('template');
64
+ template.innerHTML = String(html);
65
+ return template.content;
66
+ };
67
+
68
+ /** @typedef {{children: Node[], attributes: [string, string][]}} TargetSnapshot */
69
+
70
+ /**
71
+ * Captures an enhanced target's children and attributes so `destroy()` can put it back exactly as
72
+ * it was found.
73
+ * @param {Element} element Target being taken over.
74
+ * @returns {TargetSnapshot}
75
+ */
76
+ export function snapshotTarget(element) {
77
+ return {
78
+ children: Array.from(element.childNodes),
79
+ attributes: Array.from(element.attributes, (attribute) => [attribute.name, attribute.value])
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Restores a target captured by `snapshotTarget()`.
85
+ * @param {Element} element Target to restore.
86
+ * @param {TargetSnapshot|null} snapshot Snapshot taken before the takeover, or null to do nothing.
87
+ * @returns {void}
88
+ */
89
+ export function restoreTarget(element, snapshot) {
90
+ if (!snapshot) return;
91
+ for (const attribute of Array.from(element.attributes)) element.removeAttribute(attribute.name);
92
+ for (const [name, value] of snapshot.attributes) element.setAttribute(name, value);
93
+ element.replaceChildren(...snapshot.children);
94
+ }
95
+
96
+ /**
97
+ * Escapes text for safe insertion into an HTML string.
98
+ * @param {unknown} str Value to escape.
99
+ * @returns {string}
100
+ */
101
+ export function htmlEscape(str) {
102
+ return String(str).replace(/[&<>"']/g, (character) => ({
103
+ '&': '&amp;',
104
+ '<': '&lt;',
105
+ '>': '&gt;',
106
+ '"': '&quot;',
107
+ "'": '&#39;'
108
+ })[character]);
109
+ }
110
+
111
+ /**
112
+ * @typedef {Object} HighlightOptions
113
+ * @property {string} [className='zx-mark'] Class applied to each generated `<mark>`.
114
+ */
115
+
116
+ /**
117
+ * Wraps every case-insensitive occurrence of a query in a `<mark>` element and returns the result
118
+ * as a fragment. The text is placed in text nodes and the marks are built element by element, so
119
+ * this is safe for user and server data — no HTML is ever parsed. An empty or whitespace-only
120
+ * query yields the text unchanged in a single text node.
121
+ * @param {unknown} text Text to search.
122
+ * @param {unknown} query Search term, matched literally.
123
+ * @param {HighlightOptions} [options={}] Marking options.
124
+ * @returns {DocumentFragment}
125
+ */
126
+ export function highlightMatch(text, query, options = {}) {
127
+ const { className = 'zx-mark' } = options;
128
+ const source = text == null ? '' : String(text);
129
+ const needle = query == null ? '' : String(query).trim();
130
+ const fragment = document.createDocumentFragment();
131
+
132
+ if (needle === '' || source === '') {
133
+ fragment.append(document.createTextNode(source));
134
+ return fragment;
135
+ }
136
+
137
+ const pattern = new RegExp(escapeRegExp(needle), 'gi');
138
+ let cursor = 0;
139
+ for (const match of source.matchAll(pattern)) {
140
+ if (match.index > cursor) {
141
+ fragment.append(document.createTextNode(source.slice(cursor, match.index)));
142
+ }
143
+ const mark = document.createElement('mark');
144
+ if (className) mark.className = String(className);
145
+ mark.append(document.createTextNode(match[0]));
146
+ fragment.append(mark);
147
+ cursor = match.index + match[0].length;
148
+ }
149
+ if (cursor < source.length) fragment.append(document.createTextNode(source.slice(cursor)));
150
+ return fragment;
151
+ }
152
+
153
+ /**
154
+ * Resolves an Element, selector, or object exposing `toElement()`.
155
+ * @param {Element|string|ElementProvider|null|undefined} target Target to resolve.
156
+ * @returns {Element|null}
157
+ */
158
+ export function resolveElement(target) {
159
+ if (target == null) return null;
160
+ if (isElement(target)) return target;
161
+ if (typeof target === 'string') return document.querySelector(target);
162
+ if (typeof target.toElement === 'function') {
163
+ const resolved = target.toElement();
164
+ return isElement(resolved) ? resolved : null;
165
+ }
166
+ return null;
167
+ }
168
+
169
+ /** @param {unknown} value @returns {value is Record<string, any>} */
170
+ function isPropsObject(value) {
171
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
172
+ // Duck-typed on purpose: a Node or a component is a child, anything else is the props object.
173
+ const candidate = /** @type {Record<string, unknown>} */ (value);
174
+ if (typeof candidate.nodeType === 'number') return false;
175
+ return typeof candidate.toElement !== 'function';
176
+ }
177
+
178
+ /** @param {HTMLElement} element @param {string} key @param {any} value @returns {void} */
179
+ function applyProperty(element, key, value) {
180
+ if (key === 'ref') {
181
+ if (activeRefs && typeof value === 'string') activeRefs[value] = element;
182
+ return;
183
+ }
184
+ if (key === 'class') {
185
+ element.className = Array.isArray(value) ? value.flat(Infinity).filter(Boolean).join(' ') : String(value ?? '');
186
+ return;
187
+ }
188
+ if (key === 'style') {
189
+ if (typeof value === 'string') element.style.cssText = value;
190
+ else if (value && typeof value === 'object') {
191
+ for (const [property, styleValue] of Object.entries(value)) {
192
+ if (property.startsWith('--') || property.includes('-')) {
193
+ element.style.setProperty(property, String(styleValue));
194
+ } else {
195
+ element.style[property] = styleValue;
196
+ }
197
+ }
198
+ }
199
+ return;
200
+ }
201
+ if (key === 'dataset' && value && typeof value === 'object') {
202
+ for (const [name, datasetValue] of Object.entries(value)) {
203
+ if (datasetValue == null) delete element.dataset[name];
204
+ else element.dataset[name] = String(datasetValue);
205
+ }
206
+ return;
207
+ }
208
+ if (/^on[a-z]/.test(key) && typeof value === 'function') {
209
+ element.addEventListener(key.slice(2), value);
210
+ return;
211
+ }
212
+ if (value == null) return;
213
+
214
+ const attribute = ariaAttribute(key);
215
+ if (attribute !== null) {
216
+ element.setAttribute(attribute, String(value));
217
+ } else if (key in element) {
218
+ try {
219
+ element[key] = value;
220
+ } catch {
221
+ element.setAttribute(key, String(value));
222
+ }
223
+ } else if (typeof value === 'boolean') {
224
+ if (value) element.setAttribute(key, '');
225
+ else element.removeAttribute(key);
226
+ } else {
227
+ element.setAttribute(key, String(value));
228
+ }
229
+ }
230
+
231
+ /** @param {string} key @returns {string|null} */
232
+ function ariaAttribute(key) {
233
+ if (key.startsWith('aria-')) return key;
234
+ if (!/^aria[A-Z]/.test(key)) return null;
235
+ // ARIAMixin reflection: ariaHasPopup -> aria-haspopup, ariaValueMin -> aria-valuemin.
236
+ return 'aria-' + key.slice(4).toLowerCase();
237
+ }
238
+
239
+ /** @param {Node} parent @param {DomChild[]} children @returns {void} */
240
+ function appendChildren(parent, children) {
241
+ // `flat(Infinity)` erases the element type, and `append` lives on ParentNode rather than Node.
242
+ const host = /** @type {ParentNode} */ (parent);
243
+ // Cast before flattening: `DomChild` is recursive, and flattening it to an unbounded depth is a
244
+ // type the compiler gives up on ("excessively deep") though the runtime behaviour is trivial.
245
+ for (const raw of /** @type {any[]} */ (children).flat(Infinity)) {
246
+ if (raw == null) continue;
247
+ if (typeof raw === 'string' || typeof raw === 'number') {
248
+ host.append(document.createTextNode(String(raw)));
249
+ continue;
250
+ }
251
+ if (typeof raw !== 'object') continue;
252
+ const child = /** @type {{nodeType?: number, toElement?: () => Node|null}} */ (raw);
253
+ if (typeof child.nodeType === 'number') {
254
+ host.append(/** @type {Node} */ (raw));
255
+ } else if (typeof child.toElement === 'function') {
256
+ const element = child.toElement();
257
+ if (element) host.append(element);
258
+ }
259
+ }
260
+ }
@@ -0,0 +1,204 @@
1
+ // @ts-check
2
+ /**
3
+ * @typedef {Object} CsvColumn
4
+ * @property {string} id Row property read when no `value` accessor is given, and the default label.
5
+ * @property {string} [label] Header text; defaults to `id`.
6
+ * @property {(row: any) => unknown} [value] Accessor returning the cell value for a row.
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} CsvOptions
11
+ * @property {string} [delimiter=','] Field separator; use `';'` for German Excel locales.
12
+ * @property {boolean} [header=true] Whether to emit the header row.
13
+ * @property {string} [newline='\r\n'] Record separator; RFC 4180 uses CRLF.
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} DownloadOptions
18
+ * @property {string} [type='text/plain;charset=utf-8'] MIME type of the generated blob.
19
+ */
20
+
21
+ /** Characters that make a spreadsheet treat a text cell as a formula. */
22
+ const FORMULA_STARTERS = ['=', '+', '-', '@'];
23
+ /** Object URLs are revoked late so the browser has started the download before they disappear. */
24
+ const REVOKE_DELAY = 1000;
25
+
26
+ /**
27
+ * Serialises rows to RFC 4180 CSV text.
28
+ *
29
+ * Fields containing the delimiter, a double quote, CR, or LF are quoted with inner quotes doubled.
30
+ * Dates become ISO strings, null and undefined become empty fields, and text that starts with
31
+ * `=`, `+`, `-`, or `@` is prefixed with a tab so spreadsheets treat it as text rather than a
32
+ * formula. Numbers are never prefixed, so negative amounts survive intact.
33
+ *
34
+ * @param {Iterable<any>|null|undefined} rows Row objects.
35
+ * @param {Array<string|CsvColumn>} columns Column ids, or descriptors with a label and accessor.
36
+ * @param {CsvOptions} [options={}] Serialisation options.
37
+ * @returns {string}
38
+ */
39
+ export function toCsv(rows, columns, options = {}) {
40
+ const { delimiter = ',', header = true, newline = '\r\n' } = options;
41
+ const definitions = normalizeColumns(columns);
42
+ const lines = [];
43
+
44
+ if (header) {
45
+ lines.push(definitions
46
+ .map((column) => escapeField(column.label, delimiter, needsFormulaGuard(column.label)))
47
+ .join(delimiter));
48
+ }
49
+ for (const row of rows ?? []) {
50
+ const fields = definitions.map((column) => {
51
+ const value = typeof column.value === 'function' ? column.value(row) : readProperty(row, column.id);
52
+ return escapeField(formatValue(value), delimiter, needsFormulaGuard(value));
53
+ });
54
+ lines.push(fields.join(delimiter));
55
+ }
56
+ return lines.join(newline);
57
+ }
58
+
59
+ /**
60
+ * Saves data to the user's downloads through a temporary object URL and anchor click.
61
+ * A UTF-8 BOM is prepended to CSV strings so Excel reads them as UTF-8.
62
+ * @param {string} filename Suggested file name including its extension.
63
+ * @param {string|Blob|ArrayBuffer|ArrayBufferView} data Payload to save.
64
+ * @param {DownloadOptions} [options={}] Blob options.
65
+ * @returns {void}
66
+ */
67
+ export function downloadBlob(filename, data, options = {}) {
68
+ const { type = 'text/plain;charset=utf-8' } = options;
69
+ const blob = toBlob(data, type);
70
+ const url = URL.createObjectURL(blob);
71
+ const anchor = document.createElement('a');
72
+
73
+ anchor.href = url;
74
+ anchor.download = String(filename ?? 'download');
75
+ anchor.rel = 'noopener';
76
+ anchor.style.display = 'none';
77
+ document.body.append(anchor);
78
+ anchor.click();
79
+ anchor.remove();
80
+ setTimeout(() => URL.revokeObjectURL(url), REVOKE_DELAY);
81
+ }
82
+
83
+ /**
84
+ * Copies text to the clipboard, falling back to a hidden textarea when the async Clipboard API is
85
+ * unavailable or blocked. Never rejects.
86
+ * @param {string} text Text to copy.
87
+ * @returns {Promise<boolean>} Whether the copy succeeded.
88
+ */
89
+ export async function copyToClipboard(text) {
90
+ const value = String(text ?? '');
91
+ if (globalThis.navigator?.clipboard?.writeText) {
92
+ try {
93
+ await navigator.clipboard.writeText(value);
94
+ return true;
95
+ } catch {
96
+ // Denied permission or a non-secure context falls through to the legacy path.
97
+ }
98
+ }
99
+ return legacyCopy(value);
100
+ }
101
+
102
+ /**
103
+ * Copies through a hidden textarea and `document.execCommand`, restoring focus afterwards.
104
+ * @param {string} value Text to copy.
105
+ * @returns {boolean}
106
+ */
107
+ function legacyCopy(value) {
108
+ if (typeof document === 'undefined' || !document.body) return false;
109
+ const active = document.activeElement;
110
+ const textarea = document.createElement('textarea');
111
+
112
+ textarea.value = value;
113
+ textarea.setAttribute('readonly', '');
114
+ textarea.setAttribute('aria-hidden', 'true');
115
+ textarea.style.cssText = 'position:fixed;inset-block-start:-1000px;opacity:0;pointer-events:none';
116
+ document.body.append(textarea);
117
+
118
+ let copied = false;
119
+ try {
120
+ textarea.select();
121
+ textarea.setSelectionRange(0, value.length);
122
+ copied = document.execCommand?.('copy') === true;
123
+ } catch {
124
+ copied = false;
125
+ } finally {
126
+ textarea.remove();
127
+ if (active instanceof HTMLElement) active.focus({ preventScroll: true });
128
+ }
129
+ return copied;
130
+ }
131
+
132
+ /**
133
+ * @param {Array<string|CsvColumn>} columns Column ids or descriptors.
134
+ * @returns {Array<{id: string, label: string, value?: (row: any) => unknown}>}
135
+ */
136
+ function normalizeColumns(columns) {
137
+ return (columns ?? []).map((column) => {
138
+ if (typeof column === 'string') return { id: column, label: column };
139
+ const id = String(column?.id ?? '');
140
+ return { id, label: String(column?.label ?? id), value: column?.value };
141
+ });
142
+ }
143
+
144
+ /** @param {any} row @param {string} id @returns {unknown} */
145
+ function readProperty(row, id) {
146
+ return row == null ? undefined : row[id];
147
+ }
148
+
149
+ /**
150
+ * Converts a cell value to its CSV text form.
151
+ * @param {unknown} value Cell value.
152
+ * @returns {string}
153
+ */
154
+ function formatValue(value) {
155
+ if (value === null || value === undefined) return '';
156
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? '' : value.toISOString();
157
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '';
158
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
159
+ if (typeof value === 'object') {
160
+ try {
161
+ return JSON.stringify(value);
162
+ } catch {
163
+ return String(value);
164
+ }
165
+ }
166
+ return String(value);
167
+ }
168
+
169
+ /**
170
+ * Reports whether a value is text that a spreadsheet could execute as a formula.
171
+ * @param {unknown} value Cell value.
172
+ * @returns {boolean}
173
+ */
174
+ function needsFormulaGuard(value) {
175
+ if (typeof value !== 'string') return false;
176
+ return FORMULA_STARTERS.includes(value.charAt(0));
177
+ }
178
+
179
+ /**
180
+ * @param {string} field Text form of a cell.
181
+ * @param {string} delimiter Active field separator.
182
+ * @param {boolean} [guard=false] Whether to defuse a leading formula character.
183
+ * @returns {string}
184
+ */
185
+ function escapeField(field, delimiter, guard = false) {
186
+ const text = guard ? `\t${field}` : field;
187
+ const mustQuote = text.includes(delimiter) || text.includes('"') ||
188
+ text.includes('\n') || text.includes('\r');
189
+ return mustQuote ? `"${text.replaceAll('"', '""')}"` : text;
190
+ }
191
+
192
+ /**
193
+ * @param {string|Blob|ArrayBuffer|ArrayBufferView} data Payload.
194
+ * @param {string} type MIME type.
195
+ * @returns {Blob}
196
+ */
197
+ function toBlob(data, type) {
198
+ if (typeof Blob === 'function' && data instanceof Blob) return data;
199
+ // Excel only recognises UTF-8 CSV when the file opens with a byte-order mark.
200
+ const parts = /** @type {BlobPart[]} */ (
201
+ typeof data === 'string' && /^text\/csv\b/i.test(type) ? ['\uFEFF', data] : [data]
202
+ );
203
+ return new Blob(parts, { type });
204
+ }
@@ -0,0 +1,177 @@
1
+ // @ts-check
2
+ /**
3
+ * Font Awesome knowledge for the icon layer: style/family class names, the built-in glyph
4
+ * name → Font Awesome name map, icon-spec parsing, and kit loading.
5
+ *
6
+ * Nothing here is imported by a component. `icons.js` uses it only after an application opts in
7
+ * with `useFontAwesome()` / `loadFontAwesome()`, so the zero-dependency default is untouched: Zx
8
+ * never fetches a kit, a stylesheet, or a webfont on its own.
9
+ */
10
+
11
+ /**
12
+ * Font Awesome style names mapped to their CSS class. `kit` is the pseudo-style for custom icons
13
+ * uploaded to a kit (`fa-kit fa-zeyos-notes`).
14
+ * @type {Readonly<Record<string, string>>}
15
+ */
16
+ export const faStyles = Object.freeze({
17
+ solid: 'fa-solid',
18
+ regular: 'fa-regular',
19
+ light: 'fa-light',
20
+ thin: 'fa-thin',
21
+ duotone: 'fa-duotone',
22
+ brands: 'fa-brands',
23
+ kit: 'fa-kit'
24
+ });
25
+
26
+ /**
27
+ * Font Awesome families mapped to their CSS class. `classic` is the default and adds no class.
28
+ * @type {Readonly<Record<string, string>>}
29
+ */
30
+ export const faFamilies = Object.freeze({
31
+ classic: '',
32
+ sharp: 'fa-sharp',
33
+ 'sharp-duotone': 'fa-sharp-duotone'
34
+ });
35
+
36
+ /** Short prefixes accepted in an icon spec (`fas:user`), mapped to a style name. */
37
+ const stylePrefixes = Object.freeze({
38
+ fa: null, fas: 'solid', far: 'regular', fal: 'light', fat: 'thin', fad: 'duotone',
39
+ fab: 'brands', fak: 'kit', solid: 'solid', regular: 'regular', light: 'light', thin: 'thin',
40
+ duotone: 'duotone', brands: 'brands', kit: 'kit', custom: 'kit'
41
+ });
42
+
43
+ /**
44
+ * Built-in Zx glyph names mapped to their Font Awesome 6/7 counterpart, so the same component
45
+ * code renders either provider. Names not listed here are passed to Font Awesome unchanged.
46
+ * @type {Readonly<Record<string, string>>}
47
+ */
48
+ export const faNames = Object.freeze({
49
+ x: 'xmark',
50
+ search: 'magnifying-glass',
51
+ calendar: 'calendar-days',
52
+ reload: 'arrows-rotate',
53
+ dots: 'ellipsis',
54
+ info: 'circle-info',
55
+ warning: 'triangle-exclamation',
56
+ error: 'circle-exclamation',
57
+ success: 'circle-check',
58
+ drag: 'grip-vertical'
59
+ });
60
+
61
+ /** @type {Map<string, Promise<void>>} In-flight and settled kit loads, keyed by URL. */
62
+ const kitLoads = new Map();
63
+
64
+ /**
65
+ * @typedef {Object} IconSpec
66
+ * @property {'fa'|'builtin'|null} provider Renderer the spec named, null when it named none.
67
+ * @property {string} name Bare icon name (`'user'`, `'zeyos-notes'`), empty for literal specs.
68
+ * @property {string|null} style Style named by the spec, if any.
69
+ * @property {string[]|null} classes Literal class list when the spec was already CSS classes.
70
+ */
71
+
72
+ /**
73
+ * Parses an icon spec into the renderer it names and the icon name it carries. Recognised forms:
74
+ *
75
+ * - `'fa-solid fa-user'`, `'fa-kit fa-zeyos-notes'` — a literal class list, used as given.
76
+ * - `'fa:user'`, `'fas:user'`, `'duotone:user'`, `'kit:zeyos-notes'` — prefix plus icon name.
77
+ * - `'builtin:check'` — forces one of the bundled inline SVG glyphs.
78
+ * - `'user'` — a bare name, left for the caller's active provider to interpret.
79
+ *
80
+ * @param {string} spec Icon spec.
81
+ * @returns {IconSpec}
82
+ */
83
+ export function parseIconSpec(spec) {
84
+ const text = String(spec).trim();
85
+ if (/\s/.test(text) || text.startsWith('fa-')) {
86
+ const classes = text.split(/\s+/).filter(Boolean);
87
+ return { provider: 'fa', name: '', style: null, classes };
88
+ }
89
+
90
+ const separator = text.indexOf(':');
91
+ if (separator > 0) {
92
+ const prefix = text.slice(0, separator).toLowerCase();
93
+ const name = text.slice(separator + 1);
94
+ if (prefix === 'builtin' || prefix === 'zx') {
95
+ return { provider: 'builtin', name, style: null, classes: null };
96
+ }
97
+ if (Object.hasOwn(stylePrefixes, prefix)) {
98
+ return { provider: 'fa', name, style: stylePrefixes[prefix], classes: null };
99
+ }
100
+ }
101
+
102
+ return { provider: null, name: text, style: null, classes: null };
103
+ }
104
+
105
+ /**
106
+ * @typedef {Object} FaClassOptions
107
+ * @property {string} [style='solid'] Style name from `faStyles`.
108
+ * @property {string} [family='classic'] Family name from `faFamilies`.
109
+ * @property {boolean} [fixedWidth=false] Adds `fa-fw` for column-aligned glyphs.
110
+ * @property {boolean} [translate=true] Maps built-in Zx glyph names through `faNames`.
111
+ */
112
+
113
+ /**
114
+ * Builds the Font Awesome class list for an icon name.
115
+ * @param {string} name Font Awesome icon name, with or without the `fa-` prefix.
116
+ * @param {FaClassOptions} [options={}] Style, family, and modifiers.
117
+ * @returns {string[]} Class names, style/family first.
118
+ */
119
+ export function faIconClasses(name, options = {}) {
120
+ const { style = 'solid', family = 'classic', fixedWidth = false, translate = true } = options;
121
+ const bare = String(name).replace(/^fa-/, '');
122
+ const resolved = translate ? faNames[bare] ?? bare : bare;
123
+ const classes = [];
124
+ const familyClass = faFamilies[family];
125
+ if (familyClass) classes.push(familyClass);
126
+ classes.push(faStyles[style] ?? faStyles.solid, `fa-${resolved}`);
127
+ if (fixedWidth) classes.push('fa-fw');
128
+ return classes;
129
+ }
130
+
131
+ /**
132
+ * Expands a kit token into its script URL. Full URLs and paths are returned unchanged.
133
+ * @param {string} kit Kit token (`'ae8320b210'`) or script URL.
134
+ * @returns {string}
135
+ */
136
+ export function kitUrl(kit) {
137
+ const value = String(kit).trim();
138
+ if (/^(https?:)?\/\//.test(value) || value.startsWith('/') || value.endsWith('.js')) return value;
139
+ if (!/^[a-z0-9]+$/.test(value)) throw new RangeError(`Invalid Font Awesome kit token: ${kit}`);
140
+ return `https://kit.fontawesome.com/${value}.js`;
141
+ }
142
+
143
+ /**
144
+ * Loads a Font Awesome kit script once per URL. Repeat calls return the first promise, and a kit
145
+ * the host page already embedded is adopted rather than loaded twice.
146
+ * @param {string} kit Kit token or script URL.
147
+ * @returns {Promise<void>} Resolves when the kit has loaded.
148
+ */
149
+ export function loadFontAwesomeKit(kit) {
150
+ const url = kitUrl(kit);
151
+ const pending = kitLoads.get(url);
152
+ if (pending) return pending;
153
+
154
+ const promise = /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
155
+ const absolute = new URL(url, document.baseURI).href;
156
+ const existing = [...document.scripts].find((script) => script.src === absolute);
157
+ if (existing && globalThis.FontAwesomeKitConfig) {
158
+ resolve();
159
+ return;
160
+ }
161
+
162
+ const script = existing ?? document.createElement('script');
163
+ script.addEventListener('load', () => resolve(), { once: true });
164
+ script.addEventListener('error', () => {
165
+ kitLoads.delete(url);
166
+ reject(new Error(`Font Awesome kit failed to load: ${url}`));
167
+ }, { once: true });
168
+ if (existing) return;
169
+ script.src = url;
170
+ script.async = true;
171
+ script.crossOrigin = 'anonymous';
172
+ document.head.append(script);
173
+ }));
174
+
175
+ kitLoads.set(url, promise);
176
+ return promise;
177
+ }