@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,420 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h, resolveElement } from '../../core/dom.js';
3
+ import { position } from '../../core/position.js';
4
+ import { uid } from '../../core/util.js';
5
+
6
+ /** Placements accepted by the option and understood by `position()`. */
7
+ const PLACEMENTS = new Set(['top', 'bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end']);
8
+ /** Trigger modes. `manual` installs no listeners at all. */
9
+ const TRIGGERS = new Set(['hover', 'focus', 'both', 'manual']);
10
+
11
+ /**
12
+ * @typedef {string|number|Node|{toElement: () => Node|null}|null} TooltipContentValue
13
+ * A value the tooltip can render: text, a node, or anything exposing `toElement()`.
14
+ */
15
+ /**
16
+ * @typedef {TooltipContentValue|(() => TooltipContentValue)} TooltipContent
17
+ * Tooltip content, or a function returning it. A function is called on every open, so a tooltip
18
+ * can describe state that changes between hovers.
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} TooltipOptions
23
+ * @property {TooltipContent} [content=''] Bubble content, re-evaluated on each open when a function.
24
+ * @property {'top'|'bottom'|'top-start'|'top-end'|'bottom-start'|'bottom-end'} [placement='top'] Preferred side; flips when it would overflow.
25
+ * @property {number} [offset=6] Gap between anchor and bubble in pixels.
26
+ * @property {number} [delay=400] Delay before a hover opens the tooltip, in milliseconds.
27
+ * @property {number} [hideDelay=80] Delay before a leave or blur closes it, in milliseconds.
28
+ * @property {number|string} [maxWidth=260] Maximum bubble width; a number is pixels, a string is any CSS length.
29
+ * @property {'hover'|'focus'|'both'|'manual'} [trigger='both'] Which interactions open the tooltip.
30
+ * @property {boolean} [disabled=false] Whether the tooltip starts suppressed.
31
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
32
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close event listener.
33
+ */
34
+
35
+ /**
36
+ * A non-interactive description bubble anchored to another element.
37
+ *
38
+ * The bubble is a manual popover in the top layer, positioned by `position()` from the kernel, so
39
+ * it escapes clipping and stacking contexts and flips near a viewport edge. It never takes focus
40
+ * and never receives pointer events: it describes the anchor rather than adding a target of its
41
+ * own. While open, the anchor carries `aria-describedby` pointing at the bubble; closing restores
42
+ * whatever the anchor had before — including nothing at all.
43
+ *
44
+ * A hover opens it after `delay`; keyboard focus opens it at once, because a keyboard user has no
45
+ * way to "hover a little longer". Touch never opens it: a bubble that cannot be dismissed by
46
+ * moving the pointer away would simply stick, so touch pointers are ignored and the anchor is left
47
+ * to its own label.
48
+ * @fires Tooltip#open
49
+ * @fires Tooltip#close
50
+ * @extends {Component<TooltipOptions>}
51
+ */
52
+ export class Tooltip extends Component {
53
+ static cssName = 'tooltip';
54
+
55
+ /** @type {Readonly<TooltipOptions>} */
56
+ static defaults = {
57
+ content: '',
58
+ placement: 'top',
59
+ offset: 6,
60
+ delay: 400,
61
+ hideDelay: 80,
62
+ maxWidth: 260,
63
+ trigger: 'both',
64
+ disabled: false
65
+ };
66
+
67
+ /** @type {Element} */
68
+ #anchor;
69
+ /** @type {TooltipContent} */
70
+ #content;
71
+ /** @type {ReturnType<typeof position>|null} */
72
+ #position = null;
73
+ /** The anchor's `aria-describedby` before this tooltip touched it; `null` when it had none. */
74
+ #describedBy = null;
75
+ /** Handle of the pending open timer; `0` when none is scheduled. */
76
+ #openTimer = 0;
77
+ /** Handle of the pending close timer; `0` when none is scheduled. */
78
+ #closeTimer = 0;
79
+ /** True between `pointerdown` and `pointerup`, so a click-induced focus cannot re-open. */
80
+ #pointerActive = false;
81
+ #disabled = false;
82
+ #destroyed = false;
83
+
84
+ /**
85
+ * Attaches a tooltip to an existing element.
86
+ * @param {Element|string} anchor Anchor element or selector.
87
+ * @param {TooltipOptions} [options={}] Tooltip options.
88
+ */
89
+ constructor(anchor, options = {}) {
90
+ const resolved = resolveElement(anchor);
91
+ if (!resolved) throw new TypeError('Tooltip anchor could not be resolved');
92
+ super(null, options);
93
+
94
+ this.#anchor = resolved;
95
+ this.#content = this.options.content;
96
+ this.#disabled = Boolean(this.options.disabled);
97
+ this.#describedBy = resolved.getAttribute('aria-describedby');
98
+ this.el.style.setProperty('--zx-tooltip-max-inline-size', cssLength(this.options.maxWidth));
99
+
100
+ const trigger = TRIGGERS.has(this.options.trigger) ? this.options.trigger : 'both';
101
+ if (trigger === 'manual') return;
102
+
103
+ if (trigger === 'hover' || trigger === 'both') {
104
+ this.listen(this.#anchor, 'pointerenter', (event) => {
105
+ // A touch tooltip has no "move away" gesture to close it, so it would stick open.
106
+ if (/** @type {PointerEvent} */ (event).pointerType === 'touch') return;
107
+ this.#scheduleShow(Number(this.options.delay));
108
+ });
109
+ this.listen(this.#anchor, 'pointerleave', () => this.#scheduleHide(Number(this.options.hideDelay)));
110
+ }
111
+
112
+ if (trigger === 'focus' || trigger === 'both') {
113
+ this.listen(this.#anchor, 'focusin', () => {
114
+ // Focus that came from a click is not a request for a tooltip; the pointer already closed it.
115
+ if (this.#pointerActive || !isFocusVisible(this.#anchor)) return;
116
+ this.#scheduleShow(0);
117
+ });
118
+ this.listen(this.#anchor, 'focusout', () => this.#scheduleHide(Number(this.options.hideDelay)));
119
+ }
120
+
121
+ // Dismissal. Escape satisfies WCAG 1.4.13; the pointer and focus rules also keep two tooltips
122
+ // from ever being visible at once without a module-level registry to coordinate them.
123
+ this.listen(document, 'keydown', (event) => {
124
+ if (/** @type {KeyboardEvent} */ (event).key !== 'Escape' || !this.isOpen()) return;
125
+ event.preventDefault();
126
+ this.hide();
127
+ });
128
+ this.listen(document, 'pointerdown', () => {
129
+ this.#pointerActive = true;
130
+ this.#cancelTimers();
131
+ this.hide();
132
+ }, { capture: true });
133
+ this.listen(document, 'pointerup', () => { this.#pointerActive = false; }, { capture: true });
134
+ this.listen(document, 'pointercancel', () => { this.#pointerActive = false; }, { capture: true });
135
+ this.listen(document, 'focusin', (event) => {
136
+ if (!this.isOpen()) return;
137
+ const path = event.composedPath();
138
+ if (path.includes(this.#anchor)) return;
139
+ this.hide();
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Creates the owned bubble. Runs inside the base constructor, so it touches nothing but the DOM.
145
+ * @returns {HTMLElement}
146
+ */
147
+ render() {
148
+ const panel = h('div', {
149
+ id: uid('zx-tooltip'),
150
+ role: 'tooltip',
151
+ popover: 'manual'
152
+ });
153
+ panel.dataset.state = 'closed';
154
+ document.body.append(panel);
155
+ return panel;
156
+ }
157
+
158
+ /**
159
+ * Opens the tooltip, evaluating the content first. Does nothing while disabled, already open, or
160
+ * when the content resolves to nothing.
161
+ * @returns {this}
162
+ * @fires Tooltip#open
163
+ */
164
+ show() {
165
+ if (this.#destroyed || this.#disabled || this.isOpen()) return this;
166
+ this.#cancelTimers();
167
+ const node = resolveContent(this.#content);
168
+ if (node === null) return this;
169
+
170
+ this.el.replaceChildren(node);
171
+ this.#position = position(this.#anchor, /** @type {HTMLElement} */ (this.el), {
172
+ placement: PLACEMENTS.has(this.options.placement) ? this.options.placement : 'top',
173
+ offset: Number(this.options.offset),
174
+ flip: true
175
+ });
176
+ this.el.dataset.state = 'open';
177
+ this.#anchor.setAttribute('aria-describedby', appendToken(this.#describedBy, this.el.id));
178
+ this.emit('open');
179
+ return this;
180
+ }
181
+
182
+ /**
183
+ * Closes the tooltip and restores the anchor's original `aria-describedby`.
184
+ * @returns {this}
185
+ * @fires Tooltip#close
186
+ */
187
+ hide() {
188
+ this.#cancelTimers();
189
+ if (!this.isOpen()) return this;
190
+ this.#position?.destroy();
191
+ this.#position = null;
192
+ this.el.dataset.state = 'closed';
193
+ this.el.replaceChildren();
194
+ this.#restoreDescribedBy();
195
+ this.emit('close');
196
+ return this;
197
+ }
198
+
199
+ /**
200
+ * Opens a closed tooltip, closes an open one.
201
+ * @returns {this}
202
+ */
203
+ toggle() {
204
+ return this.isOpen() ? this.hide() : this.show();
205
+ }
206
+
207
+ /**
208
+ * Reports whether the bubble is currently visible.
209
+ * @returns {boolean}
210
+ */
211
+ isOpen() {
212
+ return this.#position !== null;
213
+ }
214
+
215
+ /**
216
+ * Replaces the content. Strings are never parsed as HTML. A function is stored, not called, and
217
+ * runs on every subsequent open.
218
+ * @param {TooltipContent} content Next content.
219
+ * @returns {this}
220
+ */
221
+ setContent(content) {
222
+ this.#content = content;
223
+ if (!this.isOpen()) return this;
224
+ const node = resolveContent(content);
225
+ if (node === null) return this.hide();
226
+ this.el.replaceChildren(node);
227
+ this.#position?.update();
228
+ return this;
229
+ }
230
+
231
+ /**
232
+ * Allows the tooltip to open again.
233
+ * @returns {this}
234
+ */
235
+ enable() {
236
+ this.#disabled = false;
237
+ return this;
238
+ }
239
+
240
+ /**
241
+ * Suppresses the tooltip and closes it if it is open.
242
+ * @returns {this}
243
+ */
244
+ disable() {
245
+ this.#disabled = true;
246
+ return this.hide();
247
+ }
248
+
249
+ /**
250
+ * Reports whether the tooltip is currently suppressed.
251
+ * @returns {boolean}
252
+ */
253
+ isDisabled() {
254
+ return this.#disabled;
255
+ }
256
+
257
+ /**
258
+ * Returns the anchor the tooltip describes.
259
+ * @returns {Element}
260
+ */
261
+ getAnchor() {
262
+ return this.#anchor;
263
+ }
264
+
265
+ /**
266
+ * Closes the tooltip, removes the bubble, and puts the anchor's ARIA back as it was found.
267
+ * @returns {void}
268
+ */
269
+ destroy() {
270
+ if (this.#destroyed) return;
271
+ this.#destroyed = true;
272
+ this.hide();
273
+ this.#restoreDescribedBy();
274
+ super.destroy();
275
+ }
276
+
277
+ /** @param {number} delay @returns {void} */
278
+ #scheduleShow(delay) {
279
+ this.#cancelTimers();
280
+ if (this.#destroyed || this.#disabled || this.isOpen()) return;
281
+ const ms = Number.isFinite(delay) ? Math.max(0, delay) : 0;
282
+ if (ms === 0) {
283
+ this.show();
284
+ return;
285
+ }
286
+ this.#openTimer = setTimeout(() => {
287
+ this.#openTimer = 0;
288
+ this.show();
289
+ }, ms);
290
+ }
291
+
292
+ /** @param {number} delay @returns {void} */
293
+ #scheduleHide(delay) {
294
+ this.#cancelTimers();
295
+ if (!this.isOpen()) return;
296
+ const ms = Number.isFinite(delay) ? Math.max(0, delay) : 0;
297
+ if (ms === 0) {
298
+ this.hide();
299
+ return;
300
+ }
301
+ this.#closeTimer = setTimeout(() => {
302
+ this.#closeTimer = 0;
303
+ this.hide();
304
+ }, ms);
305
+ }
306
+
307
+ /** @returns {void} */
308
+ #cancelTimers() {
309
+ if (this.#openTimer !== 0) clearTimeout(this.#openTimer);
310
+ if (this.#closeTimer !== 0) clearTimeout(this.#closeTimer);
311
+ this.#openTimer = 0;
312
+ this.#closeTimer = 0;
313
+ }
314
+
315
+ /** @returns {void} */
316
+ #restoreDescribedBy() {
317
+ if (this.#describedBy === null) this.#anchor.removeAttribute('aria-describedby');
318
+ else this.#anchor.setAttribute('aria-describedby', this.#describedBy);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Fired when the bubble becomes visible. Never fired for a `show()` on an open tooltip.
324
+ * @event Tooltip#open
325
+ * @type {CustomEvent<Record<string, never>>}
326
+ */
327
+
328
+ /**
329
+ * Fired when the bubble is hidden. Never fired for a `hide()` on a closed tooltip.
330
+ * @event Tooltip#close
331
+ * @type {CustomEvent<Record<string, never>>}
332
+ */
333
+
334
+ /**
335
+ * Creates a tooltip from either a content value or a full option object.
336
+ * @param {Element|string} anchor Anchor element or selector.
337
+ * @param {TooltipContent|TooltipOptions} [contentOrOptions=''] Content, or tooltip options.
338
+ * @returns {Tooltip}
339
+ */
340
+ export function tooltip(anchor, contentOrOptions = '') {
341
+ return new Tooltip(anchor, isOptionsObject(contentOrOptions)
342
+ ? /** @type {TooltipOptions} */ (contentOrOptions)
343
+ : { content: /** @type {TooltipContent} */ (contentOrOptions) });
344
+ }
345
+
346
+ /**
347
+ * Attaches a plain-text tooltip with every default in place — the one-liner replacement for a
348
+ * `title` attribute.
349
+ * @param {Element|string} anchor Anchor element or selector.
350
+ * @param {string} text Description text, inserted as a text node.
351
+ * @returns {Tooltip}
352
+ */
353
+ export function describe(anchor, text) {
354
+ return new Tooltip(anchor, { content: String(text ?? '') });
355
+ }
356
+
357
+ /**
358
+ * Distinguishes an option object from a content value. Nodes, functions, arrays, and primitives
359
+ * are content; anything else that is a plain object is options.
360
+ * @param {unknown} value Second argument of {@link tooltip}.
361
+ * @returns {boolean}
362
+ */
363
+ function isOptionsObject(value) {
364
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
365
+ if (typeof (/** @type {any} */ (value).nodeType) === 'number') return false;
366
+ return typeof (/** @type {any} */ (value).toElement) !== 'function';
367
+ }
368
+
369
+ /**
370
+ * Evaluates tooltip content into a single node, or null when there is nothing to show.
371
+ * @param {TooltipContent} content Stored content.
372
+ * @returns {Node|null}
373
+ */
374
+ function resolveContent(content) {
375
+ const value = typeof content === 'function' ? content() : content;
376
+ if (value === null || value === undefined) return null;
377
+ if (typeof value === 'object' && typeof (/** @type {any} */ (value).nodeType) === 'number') {
378
+ return /** @type {Node} */ (value);
379
+ }
380
+ if (typeof value === 'object' && typeof (/** @type {any} */ (value).toElement) === 'function') {
381
+ return /** @type {any} */ (value).toElement();
382
+ }
383
+ const text = String(value);
384
+ return text === '' ? null : document.createTextNode(text);
385
+ }
386
+
387
+ /**
388
+ * Adds an id to a space-separated token list without duplicating it.
389
+ * @param {string|null} list Existing attribute value.
390
+ * @param {string} token Token to add.
391
+ * @returns {string}
392
+ */
393
+ function appendToken(list, token) {
394
+ const tokens = String(list ?? '').split(/\s+/).filter(Boolean);
395
+ if (!tokens.includes(token)) tokens.push(token);
396
+ return tokens.join(' ');
397
+ }
398
+
399
+ /**
400
+ * Normalizes a length option: a number is pixels, a string is passed through.
401
+ * @param {number|string} value Length option.
402
+ * @returns {string}
403
+ */
404
+ function cssLength(value) {
405
+ return typeof value === 'number' && Number.isFinite(value) ? `${value}px` : String(value);
406
+ }
407
+
408
+ /**
409
+ * Reports whether an element is focused in a way that deserves a visible affordance. Browsers
410
+ * without `:focus-visible` fall back to treating every focus as visible.
411
+ * @param {Element} element Focused element.
412
+ * @returns {boolean}
413
+ */
414
+ function isFocusVisible(element) {
415
+ try {
416
+ return element.matches(':focus-visible');
417
+ } catch {
418
+ return true;
419
+ }
420
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Shared helpers for the hierarchical components (`TreeView` and `Finder`). Both take the same
3
+ * nested node shape and the same `valueKey` / `labelKey` / `childrenKey` readers, so the reading
4
+ * and walking logic lives here once.
5
+ * @module
6
+ */
7
+
8
+ /** @typedef {Record<string, any>} TreeNode */
9
+ /** @typedef {string|((node: TreeNode) => unknown)} NodeReader */
10
+ /**
11
+ * @typedef {Object} NodeReaders
12
+ * @property {(node: TreeNode) => unknown} value Node ID.
13
+ * @property {(node: TreeNode) => string} label Node label.
14
+ * @property {(node: TreeNode) => TreeNode[]|null} children Loaded children, or null when unknown.
15
+ */
16
+
17
+ /**
18
+ * Builds reader functions for one component's key configuration.
19
+ * @param {{valueKey?: NodeReader, labelKey?: NodeReader, childrenKey?: string}} options Key configuration.
20
+ * @returns {NodeReaders}
21
+ */
22
+ export function createReaders({ valueKey = 'ID', labelKey = 'name', childrenKey = 'children' } = {}) {
23
+ return {
24
+ value: (node) => (typeof valueKey === 'function' ? valueKey(node) : node?.[valueKey]),
25
+ label: (node) => String((typeof labelKey === 'function' ? labelKey(node) : node?.[labelKey]) ?? ''),
26
+ children: (node) => {
27
+ const children = node?.[childrenKey];
28
+ return Array.isArray(children) ? children : null;
29
+ }
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Reports whether a node can have children: it already has some, or it is explicitly marked as a
35
+ * branch whose children have not been loaded yet.
36
+ * @param {TreeNode} node Node to inspect.
37
+ * @param {NodeReaders} readers Node readers.
38
+ * @returns {boolean}
39
+ */
40
+ export function isBranch(node, readers) {
41
+ if (node?.hasChildren === true) return true;
42
+ const children = readers.children(node);
43
+ return Array.isArray(children) && children.length > 0;
44
+ }
45
+
46
+ /**
47
+ * Depth-first walk over every node, deepest last.
48
+ * @param {TreeNode[]} nodes Root nodes.
49
+ * @param {NodeReaders} readers Node readers.
50
+ * @param {(node: TreeNode, context: {level: number, parent: TreeNode|null, index: number, siblings: TreeNode[]}) => void} visit Visitor.
51
+ * @param {{level?: number, parent?: TreeNode|null}} [context={}] Starting context.
52
+ * @returns {void}
53
+ */
54
+ export function walk(nodes, readers, visit, { level = 1, parent = null } = {}) {
55
+ nodes.forEach((node, index) => {
56
+ visit(node, { level, parent, index, siblings: nodes });
57
+ const children = readers.children(node);
58
+ if (children) walk(children, readers, visit, { level: level + 1, parent: node });
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Finds a node by ID anywhere in the tree.
64
+ * @param {TreeNode[]} nodes Root nodes.
65
+ * @param {unknown} id Node ID.
66
+ * @param {NodeReaders} readers Node readers.
67
+ * @returns {TreeNode|null}
68
+ */
69
+ export function findNode(nodes, id, readers) {
70
+ let found = null;
71
+ walk(nodes, readers, (node) => {
72
+ if (found === null && Object.is(readers.value(node), id)) found = node;
73
+ });
74
+ return found;
75
+ }
76
+
77
+ /**
78
+ * Returns the chain of nodes from a root down to the node with the given ID, inclusive.
79
+ * @param {TreeNode[]} nodes Root nodes.
80
+ * @param {unknown} id Node ID.
81
+ * @param {NodeReaders} readers Node readers.
82
+ * @returns {TreeNode[]} Empty when the ID is not in the tree.
83
+ */
84
+ export function pathTo(nodes, id, readers) {
85
+ for (const node of nodes) {
86
+ if (Object.is(readers.value(node), id)) return [node];
87
+ const children = readers.children(node);
88
+ if (!children) continue;
89
+ const below = pathTo(children, id, readers);
90
+ if (below.length > 0) return [node, ...below];
91
+ }
92
+ return [];
93
+ }
94
+
95
+ /**
96
+ * Returns every descendant of a node, excluding the node itself.
97
+ * @param {TreeNode} node Root of the sub-tree.
98
+ * @param {NodeReaders} readers Node readers.
99
+ * @returns {TreeNode[]}
100
+ */
101
+ export function descendants(node, readers) {
102
+ const result = [];
103
+ const children = readers.children(node);
104
+ if (children) walk(children, readers, (child) => result.push(child));
105
+ return result;
106
+ }
107
+
108
+ /**
109
+ * Flattens the tree to the rows a tree view currently shows: every root, plus the children of
110
+ * every expanded branch. The returned rows carry the ARIA bookkeeping each row needs.
111
+ * @param {TreeNode[]} nodes Root nodes.
112
+ * @param {NodeReaders} readers Node readers.
113
+ * @param {Set<unknown>} expanded IDs of expanded branches.
114
+ * @returns {{node: TreeNode, id: unknown, level: number, parent: TreeNode|null, posinset: number, setsize: number}[]}
115
+ */
116
+ export function flattenVisible(nodes, readers, expanded) {
117
+ const rows = [];
118
+ const push = (list, level, parent) => {
119
+ list.forEach((node, index) => {
120
+ const id = readers.value(node);
121
+ rows.push({ node, id, level, parent, posinset: index + 1, setsize: list.length });
122
+ if (!expanded.has(id)) return;
123
+ const children = readers.children(node);
124
+ if (children && children.length > 0) push(children, level + 1, node);
125
+ });
126
+ };
127
+ push(nodes, 1, null);
128
+ return rows;
129
+ }
130
+
131
+ /**
132
+ * Filters a tree to the nodes matching a predicate, keeping the ancestors that lead to a match.
133
+ * @param {TreeNode[]} nodes Root nodes.
134
+ * @param {NodeReaders} readers Node readers.
135
+ * @param {(node: TreeNode) => boolean} matches Predicate.
136
+ * @param {string} childrenKey Property the filtered children are written to.
137
+ * @returns {TreeNode[]} New nodes; the originals are not mutated.
138
+ */
139
+ export function filterTree(nodes, readers, matches, childrenKey) {
140
+ const result = [];
141
+ for (const node of nodes) {
142
+ const children = readers.children(node) ?? [];
143
+ const keptChildren = filterTree(children, readers, matches, childrenKey);
144
+ if (matches(node) || keptChildren.length > 0) {
145
+ result.push(keptChildren.length > 0 || children.length > 0
146
+ ? { ...node, [childrenKey]: keptChildren }
147
+ : node);
148
+ }
149
+ }
150
+ return result;
151
+ }
@@ -0,0 +1,99 @@
1
+ .zx-tree {
2
+ --zx-tree-indent: 18px;
3
+ display: block;
4
+ min-inline-size: 0;
5
+ color: var(--zx-color-text);
6
+ font-size: var(--zx-control-font-size);
7
+ }
8
+
9
+ .zx-tree__row {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: var(--zx-space-2);
13
+ min-block-size: calc(var(--zx-control-height) - 6px);
14
+ padding-inline: var(--zx-space-2);
15
+ padding-inline-start: calc(var(--zx-space-2) + var(--zx-tree-level, 0) * var(--zx-tree-indent));
16
+ border-radius: var(--zx-radius-md);
17
+ cursor: pointer;
18
+ }
19
+
20
+ .zx-tree__row:hover {
21
+ background: var(--zx-color-bg-hover);
22
+ }
23
+
24
+ .zx-tree__row[aria-selected="true"] {
25
+ background: var(--zx-color-bg-selected);
26
+ color: var(--zx-color-accent);
27
+ font-weight: 550;
28
+ }
29
+
30
+ .zx-tree__row:focus-visible {
31
+ outline: 0;
32
+ box-shadow: var(--zx-focus-ring);
33
+ }
34
+
35
+ .zx-tree__twisty {
36
+ display: inline-grid;
37
+ place-items: center;
38
+ inline-size: 14px;
39
+ block-size: 14px;
40
+ flex: none;
41
+ color: var(--zx-color-text-muted);
42
+ }
43
+
44
+ .zx-tree__row[aria-expanded="true"] .zx-tree__twisty {
45
+ transform: rotate(90deg);
46
+ }
47
+
48
+ @media (prefers-reduced-motion: no-preference) {
49
+ .zx-tree__twisty {
50
+ transition: transform var(--zx-dur-fast) var(--zx-ease);
51
+ }
52
+ }
53
+
54
+ .zx-tree__row[data-loading] .zx-tree__twisty {
55
+ opacity: 0.5;
56
+ }
57
+
58
+ .zx-tree__checkbox {
59
+ flex: none;
60
+ margin: 0;
61
+ }
62
+
63
+ .zx-tree__icon {
64
+ display: inline-grid;
65
+ place-items: center;
66
+ flex: none;
67
+ color: var(--zx-color-text-muted);
68
+ }
69
+
70
+ .zx-tree__row[data-branch] .zx-tree__icon {
71
+ color: var(--zx-color-warning);
72
+ }
73
+
74
+ .zx-tree__row[aria-selected="true"] .zx-tree__icon {
75
+ color: inherit;
76
+ }
77
+
78
+ .zx-tree__label {
79
+ overflow: hidden;
80
+ text-overflow: ellipsis;
81
+ white-space: nowrap;
82
+ }
83
+
84
+ .zx-tree__badge {
85
+ margin-inline-start: auto;
86
+ padding-inline: var(--zx-space-2);
87
+ border-radius: var(--zx-radius-full);
88
+ background: var(--zx-color-bg-muted);
89
+ color: var(--zx-color-text-muted);
90
+ font-size: var(--zx-text-xs);
91
+ font-variant-numeric: tabular-nums;
92
+ }
93
+
94
+ .zx-tree__empty {
95
+ padding: var(--zx-space-5);
96
+ color: var(--zx-color-text-muted);
97
+ font-size: var(--zx-text-sm);
98
+ text-align: center;
99
+ }