@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,302 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+ import { icon as createIcon } from '../../core/icons.js';
4
+ import { Fieldset } from '../fieldset/fieldset.js';
5
+ import { coerceFloat, coerceInteger } from '../field/field.js';
6
+
7
+ /**
8
+ * @typedef {Object} FormAction
9
+ * @property {string} [id] Action identifier.
10
+ * @property {string} [label] Button text.
11
+ * @property {string|null} [icon=null] Kernel icon name.
12
+ * @property {'default'|'primary'|'danger'|'ghost'} [kind='default'] Visual intent.
13
+ * @property {'md'|'sm'} [size='md'] Button size.
14
+ * @property {'button'|'submit'|'reset'} [type='button'] Native button type.
15
+ * @property {boolean} [disabled=false] Disabled state.
16
+ * @property {string} [title] Native button title.
17
+ * @property {(event: MouseEvent, form: Form) => void} [onClick] Click callback.
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} FormOptions
22
+ * @property {Array<Fieldset|import('../fieldset/fieldset.js').FieldsetOptions>} [fieldsets=[]] Initial fieldsets.
23
+ * @property {FormAction[]} [actions=[]] Form action descriptors.
24
+ * @property {boolean} [novalidate=true] Native `novalidate` state.
25
+ */
26
+
27
+ /**
28
+ * Real form element coordinating fieldsets, validation, and actions.
29
+ * @fires Form#submit
30
+ * @fires Form#invalid
31
+ * @fires Form#change
32
+ * @extends {Component<FormOptions>}
33
+ */
34
+ export class Form extends Component {
35
+ static cssName = 'form';
36
+
37
+ /** @type {FormOptions & {msg: Record<string, string>}} */
38
+ static defaults = {
39
+ fieldsets: [],
40
+ actions: [],
41
+ novalidate: true,
42
+ msg: {
43
+ 'form.required': 'This field is required.',
44
+ 'form.int': 'Enter a whole number.',
45
+ 'form.float': 'Enter a valid number.'
46
+ }
47
+ };
48
+
49
+ /** @returns {HTMLElement} */
50
+ render() {
51
+ const root = /** @type {HTMLElement} */ (this.el ?? h('form'));
52
+ this._fieldsets = [];
53
+ this._ownedFieldsets = new Set();
54
+ this._origins = new Map();
55
+ this._ownedNodes = [];
56
+ this._novalidateAttribute = root.getAttribute('novalidate');
57
+ root.toggleAttribute('novalidate', Boolean(this.options.novalidate));
58
+
59
+ this._body = h('div', { class: 'zx-form__body' });
60
+ this._actions = h('div', { class: 'zx-form__actions' });
61
+ this._ownedNodes.push(this._body, this._actions);
62
+ root.append(this._body, this._actions);
63
+
64
+ this.listen(root, 'submit', (event) => {
65
+ event.preventDefault();
66
+ this.submit();
67
+ });
68
+ this.listen(root, 'reset', (event) => {
69
+ event.preventDefault();
70
+ this.reset();
71
+ });
72
+ this.listen(this._actions, 'click', (event) => this._handleAction(event));
73
+
74
+ const fieldsets = Array.isArray(this.options.fieldsets) ? this.options.fieldsets : [];
75
+ for (const fieldset of fieldsets) this.addFieldset(fieldset);
76
+ this.setActions(Array.isArray(this.options.actions) ? this.options.actions : []);
77
+ return root;
78
+ }
79
+
80
+ /**
81
+ * Adds an existing fieldset or constructs one from options.
82
+ * @param {Fieldset|import('../fieldset/fieldset.js').FieldsetOptions} fieldset Fieldset source.
83
+ * @returns {Fieldset}
84
+ */
85
+ addFieldset(fieldset) {
86
+ const owned = !(fieldset instanceof Fieldset);
87
+ const instance = owned ? new Fieldset(null, fieldset ?? {}) : fieldset;
88
+ if (this._fieldsets.includes(instance)) return instance;
89
+ if (owned) this._ownedFieldsets.add(instance);
90
+ else this._origins.set(instance, { parent: instance.el.parentNode, next: instance.el.nextSibling });
91
+ this._fieldsets.push(instance);
92
+ this._body.append(instance.el);
93
+ this.listen(instance.el, 'zx-change', (event) => {
94
+ const target = event.target;
95
+ const id = target instanceof Element ? target.dataset.fieldId : null;
96
+ if (id) this.emit('change', { id, value: instance.getValue(id) });
97
+ });
98
+ return instance;
99
+ }
100
+
101
+ /**
102
+ * Replaces the rendered action buttons.
103
+ * @param {FormAction[]} list Button descriptors.
104
+ * @returns {this}
105
+ */
106
+ setActions(list) {
107
+ this._actionDescriptors = Array.isArray(list) ? list.map((action) => ({ ...action })) : [];
108
+ this._actions.replaceChildren(...this._actionDescriptors.map((action, index) => {
109
+ const label = String(action.label ?? action.id ?? 'Action');
110
+ return h('button', {
111
+ class: 'zx-btn',
112
+ type: ['submit', 'reset'].includes(action.type) ? action.type : 'button',
113
+ disabled: Boolean(action.disabled),
114
+ title: action.title,
115
+ ariaLabel: !label && action.title ? action.title : undefined,
116
+ dataset: {
117
+ actionIndex: index,
118
+ kind: ['primary', 'danger', 'ghost'].includes(action.kind) ? action.kind : 'default',
119
+ size: action.size === 'sm' ? 'sm' : 'md'
120
+ }
121
+ }, action.icon ? createIcon(action.icon, { size: 16 }) : null,
122
+ label ? h('span', { class: 'zx-btn__label' }, label) : null);
123
+ }));
124
+ this._actions.hidden = this._actionDescriptors.length === 0;
125
+ return this;
126
+ }
127
+
128
+ /**
129
+ * Returns all values merged in fieldset order.
130
+ * @returns {Record<string, unknown>}
131
+ */
132
+ getValues() {
133
+ return Object.assign({}, ...this._fieldsets.map((fieldset) => fieldset.getValues()));
134
+ }
135
+
136
+ /**
137
+ * Sets matching values across all fieldsets.
138
+ * @param {Record<string, unknown>} values Values keyed by field identifier.
139
+ * @param {{silent?: boolean}} [options={}] Set `silent` to suppress field change events.
140
+ * @returns {this}
141
+ */
142
+ setValues(values, { silent = false } = {}) {
143
+ for (const fieldset of this._fieldsets) fieldset.setValues(values, { silent });
144
+ return this;
145
+ }
146
+
147
+ /**
148
+ * Returns the first matching field.
149
+ * @param {string} id Field identifier.
150
+ * @returns {import('../field/field.js').Field|null}
151
+ */
152
+ getField(id) {
153
+ for (const fieldset of this._fieldsets) {
154
+ const field = fieldset.getField(id);
155
+ if (field) return field;
156
+ }
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Sets one matching field value.
162
+ * @param {string} id Field identifier.
163
+ * @param {unknown} value New value.
164
+ * @returns {this}
165
+ */
166
+ setValue(id, value) {
167
+ this.getField(id)?.setValue(value);
168
+ return this;
169
+ }
170
+
171
+ /**
172
+ * Returns one matching field value.
173
+ * @param {string} id Field identifier.
174
+ * @returns {unknown}
175
+ */
176
+ getValue(id) {
177
+ return this.getField(id)?.getValue() ?? null;
178
+ }
179
+
180
+ /**
181
+ * Resets all fieldsets and highlights.
182
+ * @returns {this}
183
+ */
184
+ reset() {
185
+ for (const fieldset of this._fieldsets) fieldset.reset().clearHighlights();
186
+ return this;
187
+ }
188
+
189
+ /**
190
+ * Applies field highlights across all fieldsets.
191
+ * @param {Record<string, string>} highlights Messages keyed by field identifier.
192
+ * @param {'danger'|'warning'|'success'} [kind='danger'] Highlight kind.
193
+ * @returns {number}
194
+ */
195
+ setHighlights(highlights, kind = 'danger') {
196
+ return this._fieldsets.reduce((count, fieldset) => count + fieldset.setHighlights(highlights, kind), 0);
197
+ }
198
+
199
+ /**
200
+ * Clears all field highlights.
201
+ * @returns {this}
202
+ */
203
+ clearHighlights() {
204
+ for (const fieldset of this._fieldsets) fieldset.clearHighlights();
205
+ return this;
206
+ }
207
+
208
+ /**
209
+ * Validates and emits a preventable submit event.
210
+ * @returns {boolean} True when valid and not prevented.
211
+ * @fires Form#submit
212
+ * @fires Form#invalid
213
+ */
214
+ submit() {
215
+ const errors = {};
216
+ let firstInvalid = null;
217
+ for (const fieldset of this._fieldsets) {
218
+ for (const [id, field] of Object.entries(fieldset.getFields())) {
219
+ const value = field.getValue();
220
+ let message = '';
221
+ if (field.options.required && isEmpty(value)) {
222
+ message = this.msg('form.required');
223
+ } else if (!isEmpty(value) && field.options.type === 'int') {
224
+ const result = coerceInteger(value);
225
+ if (!result.valid) message = this.msg('form.int');
226
+ else field.setValue(result.value, { silent: true });
227
+ } else if (!isEmpty(value) && field.options.type === 'float') {
228
+ const result = coerceFloat(value);
229
+ if (!result.valid) message = this.msg('form.float');
230
+ else field.setValue(result.value, { silent: true });
231
+ }
232
+ if (message) {
233
+ errors[id] = message;
234
+ field.setHighlight(message, 'danger');
235
+ firstInvalid ??= field;
236
+ } else {
237
+ field.clearHighlight();
238
+ }
239
+ }
240
+ }
241
+
242
+ if (Object.keys(errors).length) {
243
+ this.emit('invalid', { errors });
244
+ firstInvalid?.focus();
245
+ return false;
246
+ }
247
+ const event = this.emit('submit', { values: this.getValues() });
248
+ return !event.defaultPrevented;
249
+ }
250
+
251
+ /** @param {Event} event @returns {void} */
252
+ _handleAction(event) {
253
+ const button = event.target instanceof Element ? event.target.closest('[data-action-index]') : null;
254
+ if (!button || !this._actions.contains(button)) return;
255
+ const action = this._actionDescriptors[Number(button.dataset.actionIndex)];
256
+ const callback = action?.onClick ?? action?.onclick ?? action?.handler;
257
+ if (typeof callback === 'function') callback(event, this);
258
+ }
259
+
260
+ /** @returns {void} */
261
+ destroy() {
262
+ for (const fieldset of this._fieldsets ?? []) {
263
+ if (this._ownedFieldsets.has(fieldset)) fieldset.destroy();
264
+ else {
265
+ const origin = this._origins.get(fieldset);
266
+ if (origin?.parent) origin.parent.insertBefore(
267
+ fieldset.el,
268
+ origin.next?.parentNode === origin.parent ? origin.next : null
269
+ );
270
+ else fieldset.el.remove();
271
+ }
272
+ }
273
+ this._fieldsets = [];
274
+ for (const node of this._ownedNodes ?? []) node.remove();
275
+ if (this._novalidateAttribute === null) this.el.removeAttribute('novalidate');
276
+ else this.el.setAttribute('novalidate', this._novalidateAttribute);
277
+ super.destroy();
278
+ }
279
+ }
280
+
281
+ /** @param {unknown} value @returns {boolean} */
282
+ function isEmpty(value) {
283
+ return value == null || value === '' || value === false || (Array.isArray(value) && value.length === 0);
284
+ }
285
+
286
+ /**
287
+ * Successful form submission event.
288
+ * @event Form#submit
289
+ * @type {CustomEvent<{values: Record<string, unknown>}>
290
+ */
291
+
292
+ /**
293
+ * Form validation failure event.
294
+ * @event Form#invalid
295
+ * @type {CustomEvent<{errors: Record<string, string>}>
296
+ */
297
+
298
+ /**
299
+ * Delegated field value change event.
300
+ * @event Form#change
301
+ * @type {CustomEvent<{id: string, value: unknown}>
302
+ */
@@ -0,0 +1,51 @@
1
+ .zx-groupbox {
2
+ overflow: hidden;
3
+ border: 1px solid var(--zx-color-border);
4
+ border-radius: var(--zx-radius-lg);
5
+ background: var(--zx-color-bg-surface);
6
+ }
7
+
8
+ .zx-groupbox__summary {
9
+ display: flex;
10
+ align-items: center;
11
+ gap: var(--zx-space-2);
12
+ min-block-size: var(--zx-control-height);
13
+ padding: var(--zx-space-2) var(--zx-space-4);
14
+ font-weight: 650;
15
+ cursor: pointer;
16
+ user-select: none;
17
+ }
18
+
19
+ .zx-groupbox__summary::-webkit-details-marker {
20
+ display: none;
21
+ }
22
+
23
+ .zx-groupbox__summary:hover {
24
+ background: var(--zx-color-bg-hover);
25
+ }
26
+
27
+ .zx-groupbox__summary:focus-visible {
28
+ box-shadow: var(--zx-focus-ring);
29
+ }
30
+
31
+ .zx-groupbox__chevron {
32
+ display: inline-grid;
33
+ flex: none;
34
+ place-items: center;
35
+ color: var(--zx-color-text-muted);
36
+ }
37
+
38
+ .zx-groupbox[open] .zx-groupbox__chevron {
39
+ transform: rotate(90deg);
40
+ }
41
+
42
+ .zx-groupbox__content {
43
+ border-block-start: 1px solid var(--zx-color-border);
44
+ padding: var(--zx-space-4);
45
+ }
46
+
47
+ @media (prefers-reduced-motion: no-preference) {
48
+ .zx-groupbox__chevron {
49
+ transition: transform var(--zx-dur-fast) var(--zx-ease);
50
+ }
51
+ }
@@ -0,0 +1,199 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h, resolveElement } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+
5
+ /**
6
+ * @typedef {Object} GroupboxOptions
7
+ * @property {string} [title=''] Section title.
8
+ * @property {boolean} [open=true] Initial expanded state.
9
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open callback.
10
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close callback.
11
+ */
12
+
13
+ /** @typedef {string|Node|Component} GroupboxContent */
14
+
15
+ /**
16
+ * Collapsible section backed by native details and summary elements.
17
+ * @fires Groupbox#open
18
+ * @fires Groupbox#close
19
+ * @extends {Component<GroupboxOptions>}
20
+ */
21
+ export class Groupbox extends Component {
22
+ static cssName = 'groupbox';
23
+
24
+ /** @type {Readonly<GroupboxOptions>} */
25
+ static defaults = { title: '', open: true };
26
+
27
+ /**
28
+ * Creates or adopts a native details section.
29
+ * @param {Element|string|null} target Existing details, plain container, selector, or null.
30
+ * @param {GroupboxOptions} [options={}] Groupbox options.
31
+ */
32
+ constructor(target, options = {}) {
33
+ let normalizedTarget = target;
34
+ let adoption = null;
35
+ if (target !== null) {
36
+ const resolved = resolveElement(target);
37
+ if (resolved && resolved.localName !== 'details') {
38
+ const details = document.createElement('details');
39
+ const children = Array.from(resolved.childNodes);
40
+ const parent = resolved.parentNode;
41
+ const next = resolved.nextSibling;
42
+ if (parent) parent.insertBefore(details, resolved);
43
+ details.append(...children);
44
+ resolved.remove();
45
+ adoption = { original: resolved, details, children, parent, next };
46
+ normalizedTarget = details;
47
+ }
48
+ }
49
+
50
+ try {
51
+ super(normalizedTarget, options);
52
+ } catch (error) {
53
+ if (adoption) restoreAdoption(adoption);
54
+ throw error;
55
+ }
56
+ this._adoption = adoption;
57
+ }
58
+
59
+ /** @returns {HTMLDetailsElement} */
60
+ render() {
61
+ const created = !this.el;
62
+ const details = /** @type {HTMLDetailsElement} */ (this.el ?? h('details'));
63
+ if (details.localName !== 'details') throw new TypeError('Groupbox requires a details element');
64
+
65
+ this._createdRoot = created;
66
+ this._original = created ? null : snapshot(details);
67
+ const contentNodes = Array.from(details.childNodes).filter((node) =>
68
+ !(node.nodeType === 1 && /** @type {Element} */ (node).localName === 'summary')
69
+ );
70
+ const summary = h('summary', { class: 'zx-groupbox__summary', ref: 'summary' },
71
+ h('span', { class: 'zx-groupbox__chevron', ariaHidden: 'true' },
72
+ icon('chevron-right', { size: 16 })
73
+ ),
74
+ h('span', { class: 'zx-groupbox__title', ref: 'title' }, String(this.options.title))
75
+ );
76
+ const content = h('div', { class: 'zx-groupbox__content', ref: 'content' }, contentNodes);
77
+ details.replaceChildren(summary, content);
78
+ details.open = Boolean(this.options.open);
79
+ this._lastOpen = details.open;
80
+ this.listen(details, 'toggle', () => {
81
+ const open = details.open;
82
+ if (open === this._lastOpen) return;
83
+ this._lastOpen = open;
84
+ this.emit(open ? 'open' : 'close');
85
+ });
86
+ return details;
87
+ }
88
+
89
+ /**
90
+ * Expands the section.
91
+ * @returns {this}
92
+ */
93
+ open() {
94
+ this.el.setAttribute('open', '');
95
+ return this;
96
+ }
97
+
98
+ /**
99
+ * Collapses the section.
100
+ * @returns {this}
101
+ */
102
+ close() {
103
+ this.el.removeAttribute('open');
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * Toggles the section.
109
+ * @returns {this}
110
+ */
111
+ toggle() {
112
+ return this.isOpen() ? this.close() : this.open();
113
+ }
114
+
115
+ /**
116
+ * Reports whether the section is expanded.
117
+ * @returns {boolean}
118
+ */
119
+ isOpen() {
120
+ return /** @type {HTMLDetailsElement} */ (this.el).open;
121
+ }
122
+
123
+ /**
124
+ * Replaces the visible title.
125
+ * @param {string} title Next title.
126
+ * @returns {this}
127
+ */
128
+ setTitle(title) {
129
+ this.refs.title.textContent = String(title);
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * Replaces the section body.
135
+ * @param {GroupboxContent} content Next text, node, or component.
136
+ * @returns {this}
137
+ */
138
+ setContent(content) {
139
+ const node = content instanceof Component ? content.toElement() : content;
140
+ this.refs.content.replaceChildren();
141
+ if (typeof node === 'string') this.refs.content.append(document.createTextNode(node));
142
+ else if (node && typeof node.nodeType === 'number') this.refs.content.append(node);
143
+ else throw new TypeError('Groupbox content must be a string, Node, or Component');
144
+ return this;
145
+ }
146
+
147
+ /** @returns {void} */
148
+ destroy() {
149
+ if (this._cleaned) return;
150
+ this._cleaned = true;
151
+ super.destroy();
152
+ if (this._adoption) {
153
+ restoreAdoption(this._adoption);
154
+ } else if (!this._createdRoot && this._original) {
155
+ restore(this.el, this._original);
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Emitted when the native details element opens.
162
+ * @event Groupbox#open
163
+ * @type {CustomEvent<Record<string, never>>}
164
+ */
165
+
166
+ /**
167
+ * Emitted when the native details element closes.
168
+ * @event Groupbox#close
169
+ * @type {CustomEvent<Record<string, never>>}
170
+ */
171
+
172
+ /** @param {Element} element @returns {{attributes: [string, string][], children: Node[]}} */
173
+ function snapshot(element) {
174
+ return {
175
+ attributes: Array.from(element.attributes, (attribute) => [attribute.name, attribute.value]),
176
+ children: Array.from(element.childNodes)
177
+ };
178
+ }
179
+
180
+ /** @param {Element} element @param {{attributes: [string, string][], children: Node[]}} state */
181
+ function restore(element, state) {
182
+ for (const attribute of Array.from(element.attributes)) element.removeAttribute(attribute.name);
183
+ for (const [name, value] of state.attributes) element.setAttribute(name, value);
184
+ element.replaceChildren(...state.children);
185
+ }
186
+
187
+ /**
188
+ * @param {{original: Element, details: Element, children: Node[], parent: Node|null, next: Node|null}} state
189
+ * @returns {void}
190
+ */
191
+ function restoreAdoption(state) {
192
+ state.original.replaceChildren(...state.children);
193
+ if (state.details.parentNode) {
194
+ state.details.replaceWith(state.original);
195
+ } else if (state.parent) {
196
+ const reference = state.next?.parentNode === state.parent ? state.next : null;
197
+ state.parent.insertBefore(state.original, reference);
198
+ }
199
+ }
@@ -0,0 +1,117 @@
1
+ .zx-stack {
2
+ --zx-stack-gap: var(--zx-space-4);
3
+
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: var(--zx-stack-gap);
7
+ min-inline-size: 0;
8
+ }
9
+
10
+ .zx-stack[data-inline] {
11
+ display: inline-flex;
12
+ }
13
+
14
+ .zx-stack[data-direction="row"] {
15
+ flex-direction: row;
16
+ align-items: center;
17
+ }
18
+
19
+ .zx-stack[data-wrap] {
20
+ flex-wrap: wrap;
21
+ }
22
+
23
+ .zx-stack[data-align="start"] {
24
+ align-items: flex-start;
25
+ }
26
+
27
+ .zx-stack[data-align="center"] {
28
+ align-items: center;
29
+ }
30
+
31
+ .zx-stack[data-align="end"] {
32
+ align-items: flex-end;
33
+ }
34
+
35
+ .zx-stack[data-align="stretch"] {
36
+ align-items: stretch;
37
+ }
38
+
39
+ .zx-stack[data-align="baseline"] {
40
+ align-items: baseline;
41
+ }
42
+
43
+ .zx-stack[data-justify="start"] {
44
+ justify-content: flex-start;
45
+ }
46
+
47
+ .zx-stack[data-justify="center"] {
48
+ justify-content: center;
49
+ }
50
+
51
+ .zx-stack[data-justify="end"] {
52
+ justify-content: flex-end;
53
+ }
54
+
55
+ .zx-stack[data-justify="between"] {
56
+ justify-content: space-between;
57
+ }
58
+
59
+ .zx-stack[data-justify="around"] {
60
+ justify-content: space-around;
61
+ }
62
+
63
+ .zx-grid {
64
+ --zx-grid-gap: var(--zx-space-4);
65
+ --zx-grid-min: 16rem;
66
+
67
+ display: grid;
68
+ gap: var(--zx-grid-gap);
69
+ /* `min(…, 100%)` keeps a track from overflowing a container narrower than the minimum itself. */
70
+ grid-template-columns: repeat(auto-fit, minmax(min(var(--zx-grid-min), 100%), 1fr));
71
+ min-inline-size: 0;
72
+ }
73
+
74
+ /*
75
+ * With a column count, each track asks for its share of the row but never less than
76
+ * `--zx-grid-min`, so `auto-fit` lays out the requested columns while there is room for them and
77
+ * drops one at a time as the container narrows — no media query, and the same behaviour inside a
78
+ * split pane as on a full-width page.
79
+ */
80
+ .zx-grid[data-columns] {
81
+ grid-template-columns: repeat(auto-fit, minmax(
82
+ max(
83
+ min(var(--zx-grid-min), 100%),
84
+ calc((100% - (var(--zx-grid-columns) - 1) * var(--zx-grid-gap)) / var(--zx-grid-columns))
85
+ ),
86
+ 1fr
87
+ ));
88
+ }
89
+
90
+ .zx-grid[data-align="start"] {
91
+ align-items: start;
92
+ }
93
+
94
+ .zx-grid[data-align="center"] {
95
+ align-items: center;
96
+ }
97
+
98
+ .zx-grid[data-align="end"] {
99
+ align-items: end;
100
+ }
101
+
102
+ .zx-grid[data-align="stretch"] {
103
+ align-items: stretch;
104
+ }
105
+
106
+ .zx-aspect {
107
+ display: block;
108
+ aspect-ratio: var(--zx-aspect-ratio, 16 / 9);
109
+ inline-size: 100%;
110
+ overflow: hidden;
111
+ }
112
+
113
+ .zx-aspect > * {
114
+ inline-size: 100%;
115
+ block-size: 100%;
116
+ object-fit: var(--zx-aspect-fit, cover);
117
+ }