@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,509 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+ import { typeahead } from '../../core/keyboard.js';
5
+ import { uid } from '../../core/util.js';
6
+ import { createReaders, isBranch, pathTo } from '../tree/hierarchy.js';
7
+
8
+ /** @typedef {import('../tree/hierarchy.js').TreeNode} TreeNode */
9
+ /**
10
+ * @typedef {Object} FinderOptions
11
+ * @property {TreeNode[]} [items=[]] Nested nodes.
12
+ * @property {string|((node: TreeNode) => unknown)} [valueKey='ID'] Node ID property or reader.
13
+ * @property {string|((node: TreeNode) => string)} [labelKey='name'] Node label property or reader.
14
+ * @property {string} [childrenKey='children'] Property holding child nodes.
15
+ * @property {unknown[]} [path=[]] Initially selected node IDs, from the root down.
16
+ * @property {((node: TreeNode) => Promise<TreeNode[]>|TreeNode[])|null} [load=null] Lazy child loader.
17
+ * @property {((node: TreeNode) => Node|string|null)|null} [preview=null] Renderer for the leaf preview pane.
18
+ * @property {string} [rootLabel=''] Accessible name of the first column.
19
+ * @property {number} [columnWidth=220] Column width in pixels.
20
+ * @property {number|string|null} [height=320] Component height in pixels or any CSS length.
21
+ * @property {boolean} [icons=true] Whether to render folder and file icons.
22
+ * @property {((node: TreeNode) => Node|string)|null} [renderItem=null] Row renderer.
23
+ * @property {string} [emptyText='Empty'] Text shown in an empty column.
24
+ * @property {Record<string, string>|Record<string, Record<string, string>>} [msg] Localized messages.
25
+ * @property {(event: CustomEvent<{path: unknown[], nodes: TreeNode[], node: TreeNode|null}>) => void} [onchange] Path listener.
26
+ * @property {(event: CustomEvent<{node: TreeNode, id: unknown}>) => void} [onactivate] Activation listener.
27
+ * @property {(event: CustomEvent<{error: Error, node: TreeNode, id: unknown}>) => void} [onerror] Loader-failure listener.
28
+ */
29
+
30
+ /**
31
+ * Miller-columns hierarchy browser: each column lists the children of the row selected in the
32
+ * column to its left, the way the macOS Finder's column view works.
33
+ *
34
+ * Columns are linked listboxes with a single roving tab stop across the whole component: Up and
35
+ * Down move inside a column, Right steps into the selection's children, Left steps back.
36
+ *
37
+ * @fires Finder#change
38
+ * @fires Finder#activate
39
+ * @fires Finder#error
40
+ * @extends {Component<FinderOptions>}
41
+ */
42
+ export class Finder extends Component {
43
+ static cssName = 'finder';
44
+
45
+ /** @type {FinderOptions} */
46
+ static defaults = {
47
+ items: [],
48
+ valueKey: 'ID',
49
+ labelKey: 'name',
50
+ childrenKey: 'children',
51
+ path: [],
52
+ load: null,
53
+ preview: null,
54
+ rootLabel: '',
55
+ columnWidth: 220,
56
+ height: 320,
57
+ icons: true,
58
+ renderItem: null,
59
+ emptyText: 'Empty'
60
+ };
61
+
62
+ /** @returns {HTMLElement} */
63
+ render() {
64
+ // render() runs inside the base constructor, before class-field initializers would run.
65
+ this._items = [];
66
+ this._path = [];
67
+ this._columns = [];
68
+ this._activeColumn = 0;
69
+ /** @type {Map<unknown, Promise<void>>} In-flight lazy loads, keyed by node ID. */
70
+ this._loading = new Map();
71
+ this._pathSequence = 0;
72
+ this._createdRoot = this.el === null;
73
+ this._snapshot = this._createdRoot ? null : snapshotTarget(this.el);
74
+
75
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
76
+ this.el = root;
77
+ this._readers = createReaders(this.options);
78
+ this._childrenKey = this.options.childrenKey ?? 'children';
79
+
80
+ root.style.setProperty('--zx-finder-column', `${Number(this.options.columnWidth) || 220}px`);
81
+ if (this.options.height !== null) {
82
+ root.style.blockSize = typeof this.options.height === 'number'
83
+ ? `${this.options.height}px`
84
+ : String(this.options.height);
85
+ }
86
+
87
+ this._items = Array.isArray(this.options.items) ? this.options.items.slice() : [];
88
+ this._handleTypeahead = typeahead(
89
+ () => this._columnNodes(this._activeColumn).map((node) => this._readers.label(node)),
90
+ (_label, index) => this._selectAt(this._activeColumn, index, { focus: true })
91
+ );
92
+
93
+ this.listen(root, 'click', (event) => this._onClick(/** @type {MouseEvent} */ (event)));
94
+ this.listen(root, 'dblclick', (event) => this._onDoubleClick(/** @type {MouseEvent} */ (event)));
95
+ this.listen(root, 'keydown', (event) => this._onKeydown(/** @type {KeyboardEvent} */ (event)));
96
+
97
+ // Paint the root column immediately: setPath() may await a loader, and the component must
98
+ // never be a blank box in the meantime.
99
+ this._render({});
100
+ void this.setPath(this.options.path, { silent: true });
101
+ return root;
102
+ }
103
+
104
+ /**
105
+ * Returns the selected IDs from the root down.
106
+ * @returns {unknown[]}
107
+ */
108
+ getPath() {
109
+ return this._path.slice();
110
+ }
111
+
112
+ /**
113
+ * Returns the selected nodes from the root down.
114
+ * @returns {TreeNode[]}
115
+ */
116
+ getNodes() {
117
+ let level = this._items;
118
+ const nodes = [];
119
+ for (const id of this._path) {
120
+ const node = level.find((candidate) => Object.is(this._readers.value(candidate), id));
121
+ if (!node) break;
122
+ nodes.push(node);
123
+ level = this._readers.children(node) ?? [];
124
+ }
125
+ return nodes;
126
+ }
127
+
128
+ /**
129
+ * Returns the deepest selected node.
130
+ * @returns {TreeNode|null}
131
+ */
132
+ getSelection() {
133
+ const nodes = this.getNodes();
134
+ return nodes.length > 0 ? nodes[nodes.length - 1] : null;
135
+ }
136
+
137
+ /**
138
+ * Selects a path, loading every branch along it.
139
+ * @param {unknown[]} ids Node IDs from the root down.
140
+ * @param {{silent?: boolean, focus?: boolean}} [options={}] Behavior.
141
+ * @returns {Promise<this>}
142
+ * @fires Finder#change
143
+ */
144
+ async setPath(ids, { silent = false, focus = false } = {}) {
145
+ const sequence = this._pathSequence + 1;
146
+ this._pathSequence = sequence;
147
+ const wanted = Array.isArray(ids) ? ids.slice() : [];
148
+ const path = [];
149
+ let level = this._items;
150
+
151
+ for (const id of wanted) {
152
+ const node = level.find((candidate) => Object.is(this._readers.value(candidate), id));
153
+ if (!node) break;
154
+ path.push(id);
155
+ if (!isBranch(node, this._readers)) break;
156
+ await this._ensureChildren(node);
157
+ // A slower earlier call must not overwrite a newer path once its loaders resolve.
158
+ if (sequence !== this._pathSequence) return this;
159
+ level = this._readers.children(node) ?? [];
160
+ }
161
+ this._path = path;
162
+ this._activeColumn = Math.max(0, path.length - 1);
163
+ this._render({ focus });
164
+ if (!silent) this._emitChange();
165
+ return this;
166
+ }
167
+
168
+ /**
169
+ * Reveals and selects a node anywhere in the tree.
170
+ * @param {unknown} id Node ID.
171
+ * @param {{silent?: boolean}} [options={}] Behavior.
172
+ * @returns {Promise<this>}
173
+ */
174
+ async reveal(id, { silent = false } = {}) {
175
+ const chain = pathTo(this._items, id, this._readers);
176
+ if (chain.length === 0) return this;
177
+ return this.setPath(chain.map((node) => this._readers.value(node)), { silent });
178
+ }
179
+
180
+ /**
181
+ * Replaces the whole hierarchy and clears the path.
182
+ * @param {TreeNode[]} items Nested nodes.
183
+ * @returns {this}
184
+ */
185
+ setItems(items) {
186
+ this._items = Array.isArray(items) ? items.slice() : [];
187
+ this._path = [];
188
+ this._render({});
189
+ return this;
190
+ }
191
+
192
+ /** Focuses the active column. @returns {this} */
193
+ focus() {
194
+ const row = this._activeRow(this._activeColumn) ?? this._rowAt(this._activeColumn, 0);
195
+ row?.focus();
196
+ return this;
197
+ }
198
+
199
+ /* ---------------------------------------------------------------- rendering -- */
200
+
201
+ /**
202
+ * Rebuilds every column from the current path.
203
+ * @param {{focus?: boolean}} options Behavior.
204
+ * @returns {void}
205
+ */
206
+ _render({ focus = false }) {
207
+ const nodes = this.getNodes();
208
+ // `selectedId` uses `undefined` for "nothing selected" so a node whose own ID is null still
209
+ // renders as selected.
210
+ /** @type {{nodes: TreeNode[], selectedId: unknown, owner: TreeNode|null}[]} */
211
+ const columns = [{ nodes: this._items, selectedId: this._path[0], owner: null }];
212
+
213
+ nodes.forEach((node, index) => {
214
+ if (!isBranch(node, this._readers)) return;
215
+ const children = this._readers.children(node) ?? [];
216
+ columns.push({ nodes: children, selectedId: this._path[index + 1], owner: node });
217
+ });
218
+
219
+ this._columns = columns;
220
+ this._activeColumn = Math.min(this._activeColumn, columns.length - 1);
221
+
222
+ const elements = columns.map((column, index) => this._renderColumn(column, index));
223
+ const leaf = nodes[nodes.length - 1] ?? null;
224
+ if (this.options.preview && leaf && !isBranch(leaf, this._readers)) {
225
+ const content = this.options.preview(leaf);
226
+ if (content) elements.push(h('div', { class: 'zx-finder__preview' }, content));
227
+ }
228
+ this.el.replaceChildren(...elements);
229
+ this._syncTabStop();
230
+
231
+ // Keep the deepest column in view as the path grows.
232
+ const last = this.el.lastElementChild;
233
+ last?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
234
+ if (focus) this.focus();
235
+ }
236
+
237
+ /**
238
+ * @param {{nodes: TreeNode[], selectedId: unknown, owner: TreeNode|null}} column Column spec.
239
+ * @param {number} index Column index.
240
+ * @returns {HTMLElement}
241
+ */
242
+ _renderColumn(column, index) {
243
+ const label = column.owner
244
+ ? this._readers.label(column.owner)
245
+ : String(this.options.rootLabel || this._message('finder.root', 'Root'));
246
+ const list = h('div', {
247
+ class: 'zx-finder__list',
248
+ role: 'listbox',
249
+ ariaLabel: label,
250
+ dataset: { column: String(index) }
251
+ });
252
+
253
+ if (column.nodes.length === 0) {
254
+ const loading = column.owner !== null && this._loading.has(this._readers.value(column.owner));
255
+ list.append(h('div', { class: 'zx-finder__empty' }, loading
256
+ ? this._message('finder.loading', 'Loading…')
257
+ : this._message('finder.empty', this.options.emptyText)));
258
+ }
259
+
260
+ for (const node of column.nodes) {
261
+ const id = this._readers.value(node);
262
+ const branch = isBranch(node, this._readers);
263
+ const selected = column.selectedId !== undefined && Object.is(id, column.selectedId);
264
+ const row = h('div', {
265
+ class: 'zx-finder__row',
266
+ id: uid('zx-finder-row'),
267
+ role: 'option',
268
+ tabIndex: -1,
269
+ ariaSelected: String(selected)
270
+ });
271
+ if (this.options.icons) {
272
+ row.append(h('span', { class: 'zx-finder__icon', ariaHidden: 'true' },
273
+ icon(branch ? 'folder' : 'file', { size: 14 })));
274
+ }
275
+ row.append(h('span', { class: 'zx-finder__label' },
276
+ this.options.renderItem ? this.options.renderItem(node) : this._readers.label(node)));
277
+ if (branch) {
278
+ row.append(h('span', { class: 'zx-finder__chevron', ariaHidden: 'true' },
279
+ icon('chevron-right', { size: 11 })));
280
+ }
281
+ row.toggleAttribute('data-branch', branch);
282
+ list.append(row);
283
+ }
284
+ return h('div', { class: 'zx-finder__column' }, list);
285
+ }
286
+
287
+ /** Keeps exactly one tab stop across all columns. @returns {void} */
288
+ _syncTabStop() {
289
+ const rows = this.el.querySelectorAll('.zx-finder__row');
290
+ rows.forEach((row) => { row.tabIndex = -1; });
291
+ const active = this._activeRow(this._activeColumn) ?? this._rowAt(this._activeColumn, 0) ?? rows[0];
292
+ if (active) active.tabIndex = 0;
293
+ }
294
+
295
+ /* -------------------------------------------------------------- interaction -- */
296
+
297
+ /** @param {MouseEvent} event @returns {void} */
298
+ _onClick(event) {
299
+ const row = /** @type {Element} */ (event.target).closest('.zx-finder__row');
300
+ if (!row) return;
301
+ const { column, index } = this._locate(row);
302
+ if (column < 0) return;
303
+ void this._selectAt(column, index, { focus: true });
304
+ }
305
+
306
+ /** @param {MouseEvent} event @returns {void} */
307
+ _onDoubleClick(event) {
308
+ const row = /** @type {Element} */ (event.target).closest('.zx-finder__row');
309
+ if (!row) return;
310
+ const { column, index } = this._locate(row);
311
+ const node = this._columnNodes(column)[index];
312
+ // Branches open on double-click; only leaves are "activated".
313
+ if (!node) return;
314
+ if (isBranch(node, this._readers)) void this._enter(column, index);
315
+ else this.emit('activate', { node, id: this._readers.value(node) });
316
+ }
317
+
318
+ /** @param {KeyboardEvent} event @returns {void} */
319
+ _onKeydown(event) {
320
+ // The preview pane holds arbitrary application markup; its own controls keep their keys.
321
+ if (/** @type {Element} */ (event.target).closest('.zx-finder__preview')) return;
322
+ const column = this._activeColumn;
323
+ const nodes = this._columnNodes(column);
324
+ if (nodes.length === 0 && !['ArrowLeft'].includes(event.key)) return;
325
+ const current = Math.max(0, nodes.findIndex(
326
+ (node) => Object.is(this._readers.value(node), this._columns[column]?.selectedId)));
327
+
328
+ switch (event.key) {
329
+ case 'ArrowDown':
330
+ event.preventDefault();
331
+ void this._selectAt(column, Math.min(nodes.length - 1, current + 1), { focus: true });
332
+ return;
333
+ case 'ArrowUp':
334
+ event.preventDefault();
335
+ void this._selectAt(column, Math.max(0, current - 1), { focus: true });
336
+ return;
337
+ case 'Home':
338
+ event.preventDefault();
339
+ void this._selectAt(column, 0, { focus: true });
340
+ return;
341
+ case 'End':
342
+ event.preventDefault();
343
+ void this._selectAt(column, nodes.length - 1, { focus: true });
344
+ return;
345
+ case 'ArrowRight': {
346
+ event.preventDefault();
347
+ const node = nodes[current];
348
+ if (!node || !isBranch(node, this._readers)) return;
349
+ void this._enter(column, current);
350
+ return;
351
+ }
352
+ case 'ArrowLeft':
353
+ event.preventDefault();
354
+ if (column === 0) return;
355
+ this._path = this._path.slice(0, column);
356
+ this._activeColumn = column - 1;
357
+ this._render({ focus: true });
358
+ this._emitChange();
359
+ return;
360
+ case 'Enter': {
361
+ event.preventDefault();
362
+ const node = nodes[current];
363
+ if (!node) return;
364
+ if (isBranch(node, this._readers)) void this._enter(column, current);
365
+ else this.emit('activate', { node, id: this._readers.value(node) });
366
+ return;
367
+ }
368
+ default:
369
+ this._handleTypeahead(event);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Selects a row in a column, truncating the path below it.
375
+ * @param {number} column Column index.
376
+ * @param {number} index Row index inside the column.
377
+ * @param {{focus?: boolean}} [options={}] Behavior.
378
+ * @returns {Promise<void>}
379
+ * @fires Finder#change
380
+ */
381
+ async _selectAt(column, index, { focus = false } = {}) {
382
+ const node = this._columnNodes(column)[index];
383
+ if (!node) return;
384
+ const id = this._readers.value(node);
385
+ // Re-selecting the row that is already selected would replace the element under the pointer
386
+ // and break the browser's own double-click detection.
387
+ if (this._path.length === column + 1 && Object.is(this._path[column], id)) {
388
+ this._activeColumn = column;
389
+ this._syncTabStop();
390
+ if (focus) this.focus();
391
+ return;
392
+ }
393
+ this._path = [...this._path.slice(0, column), id];
394
+ this._activeColumn = column;
395
+ if (isBranch(node, this._readers)) await this._ensureChildren(node);
396
+ this._render({ focus });
397
+ this._emitChange();
398
+ }
399
+
400
+ /**
401
+ * Moves into a branch's children column.
402
+ * @param {number} column Column index.
403
+ * @param {number} index Row index.
404
+ * @returns {Promise<void>}
405
+ */
406
+ async _enter(column, index) {
407
+ await this._selectAt(column, index);
408
+ const children = this._columnNodes(column + 1);
409
+ if (children.length === 0) {
410
+ this._activeColumn = column;
411
+ this._syncTabStop();
412
+ this.focus();
413
+ return;
414
+ }
415
+ this._activeColumn = column + 1;
416
+ // Entering an unvisited column lands on its first row.
417
+ if (this._path.length <= column + 1) await this._selectAt(column + 1, 0, { focus: true });
418
+ else {
419
+ this._syncTabStop();
420
+ this.focus();
421
+ }
422
+ }
423
+
424
+ /* ------------------------------------------------------------------ internals -- */
425
+
426
+ /**
427
+ * Loads a branch's children through the configured loader, once.
428
+ * @param {TreeNode} node Branch node.
429
+ * @returns {Promise<void>}
430
+ */
431
+ async _ensureChildren(node) {
432
+ const loaded = this._readers.children(node);
433
+ if (!this.options.load || (loaded && loaded.length > 0)) return;
434
+ const id = this._readers.value(node);
435
+ // One in-flight request per branch, so a double navigation never calls the loader twice.
436
+ const pending = this._loading.get(id);
437
+ if (pending) return pending;
438
+ const request = (async () => {
439
+ try {
440
+ const children = await this.options.load(node);
441
+ node[this._childrenKey] = Array.isArray(children) ? children : [];
442
+ if (node[this._childrenKey].length === 0) node.hasChildren = false;
443
+ } catch (error) {
444
+ node[this._childrenKey] = [];
445
+ this.emit('error', { error, node, id });
446
+ } finally {
447
+ this._loading.delete(id);
448
+ }
449
+ })();
450
+ this._loading.set(id, request);
451
+ return request;
452
+ }
453
+
454
+ /** @param {number} column @returns {TreeNode[]} */
455
+ _columnNodes(column) {
456
+ return this._columns[column]?.nodes ?? [];
457
+ }
458
+
459
+ /** @param {Element} row @returns {{column: number, index: number}} */
460
+ _locate(row) {
461
+ const list = row.closest('.zx-finder__list');
462
+ if (!list) return { column: -1, index: -1 };
463
+ return {
464
+ column: Number(list.dataset.column),
465
+ index: [...list.querySelectorAll('.zx-finder__row')].indexOf(row)
466
+ };
467
+ }
468
+
469
+ /** @param {number} column @param {number} index @returns {HTMLElement|null} */
470
+ _rowAt(column, index) {
471
+ const list = this.el.querySelector(`.zx-finder__list[data-column="${column}"]`);
472
+ return /** @type {HTMLElement|null} */ (list?.querySelectorAll('.zx-finder__row')[index] ?? null);
473
+ }
474
+
475
+ /** @param {number} column @returns {HTMLElement|null} */
476
+ _activeRow(column) {
477
+ const list = this.el.querySelector(`.zx-finder__list[data-column="${column}"]`);
478
+ return /** @type {HTMLElement|null} */ (list?.querySelector('[aria-selected="true"]') ?? null);
479
+ }
480
+
481
+ /** @returns {void} */
482
+ _emitChange() {
483
+ const nodes = this.getNodes();
484
+ this.emit('change', {
485
+ path: this.getPath(),
486
+ nodes,
487
+ node: nodes.length > 0 ? nodes[nodes.length - 1] : null
488
+ });
489
+ }
490
+
491
+ /** @param {string} key @param {string} fallback @returns {string} */
492
+ _message(key, fallback) {
493
+ const message = this.msg(key);
494
+ return message === key ? fallback : message;
495
+ }
496
+
497
+ /** Restores an enhanced target to the markup it had before the takeover. @returns {void} */
498
+ destroy() {
499
+ // Invalidate any in-flight path resolution so a late loader cannot re-render a dead component.
500
+ this._pathSequence += 1;
501
+ const root = this.el;
502
+ super.destroy();
503
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
504
+ }
505
+ }
506
+
507
+ /** Path change. @event Finder#change @type {CustomEvent<{path: unknown[], nodes: TreeNode[], node: TreeNode|null}>} */
508
+ /** Leaf activated by Enter or double-click. @event Finder#activate @type {CustomEvent<{node: TreeNode, id: unknown}>} */
509
+ /** A lazy loader rejected. @event Finder#error @type {CustomEvent<{error: Error, node: TreeNode, id: unknown}>} */
@@ -0,0 +1,32 @@
1
+ .zx-form,
2
+ .zx-form__body {
3
+ display: grid;
4
+ gap: var(--zx-space-5);
5
+ min-inline-size: 0;
6
+ }
7
+
8
+ .zx-form__actions {
9
+ display: flex;
10
+ flex-wrap: wrap;
11
+ justify-content: flex-end;
12
+ gap: var(--zx-space-2);
13
+ padding-block-start: var(--zx-space-2);
14
+ }
15
+
16
+ .zx-form__actions[hidden] {
17
+ display: none;
18
+ }
19
+
20
+ .zx-form__actions .zx-btn {
21
+ min-inline-size: 6rem;
22
+ }
23
+
24
+ .zx-form__actions .zx-btn[type="submit"] {
25
+ border-color: var(--zx-color-accent);
26
+ background: var(--zx-color-accent);
27
+ color: var(--zx-color-on-accent);
28
+ }
29
+
30
+ .zx-form__actions .zx-btn[type="submit"]:hover {
31
+ background: var(--zx-color-accent-hover);
32
+ }