@zeyos/zx 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +15 -0
  3. package/dist/types/compat/base.d.ts +156 -0
  4. package/dist/types/compat/globals.d.ts +20 -0
  5. package/dist/types/compat/index.d.ts +128 -0
  6. package/dist/types/compat/map/basic.d.ts +175 -0
  7. package/dist/types/compat/map/data.d.ts +102 -0
  8. package/dist/types/compat/map/date-time.d.ts +80 -0
  9. package/dist/types/compat/map/factory.d.ts +9 -0
  10. package/dist/types/compat/map/form.d.ts +98 -0
  11. package/dist/types/compat/map/helpers.d.ts +12 -0
  12. package/dist/types/compat/map/http.d.ts +36 -0
  13. package/dist/types/compat/map/layout.d.ts +106 -0
  14. package/dist/types/compat/map/options.d.ts +80 -0
  15. package/dist/types/compat/map/select.d.ts +95 -0
  16. package/dist/types/compat/map/stubs.d.ts +32 -0
  17. package/dist/types/compat/map/utilities.d.ts +37 -0
  18. package/dist/types/compat-entry.d.ts +5 -0
  19. package/dist/types/components/badge/badge.d.ts +46 -0
  20. package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
  21. package/dist/types/components/button/button.d.ts +42 -0
  22. package/dist/types/components/check-button/check-button.d.ts +98 -0
  23. package/dist/types/components/checklist/checklist.d.ts +143 -0
  24. package/dist/types/components/checklist/field-adapter.d.ts +5 -0
  25. package/dist/types/components/context-menu/context-menu.d.ts +130 -0
  26. package/dist/types/components/copy/copy-button.d.ts +48 -0
  27. package/dist/types/components/copy/copy-input.d.ts +91 -0
  28. package/dist/types/components/data-filter/data-filter.d.ts +250 -0
  29. package/dist/types/components/data-filter/filter-core.d.ts +52 -0
  30. package/dist/types/components/date-picker/date-picker.d.ts +137 -0
  31. package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
  32. package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
  33. package/dist/types/components/date-picker/month-picker.d.ts +75 -0
  34. package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
  35. package/dist/types/components/date-picker/time-picker.d.ts +127 -0
  36. package/dist/types/components/datebox/date-range-box.d.ts +218 -0
  37. package/dist/types/components/datebox/datebox.d.ts +160 -0
  38. package/dist/types/components/datebox/field-adapter.d.ts +5 -0
  39. package/dist/types/components/dialog/dialog.d.ts +284 -0
  40. package/dist/types/components/dropdown/dropdown.d.ts +118 -0
  41. package/dist/types/components/empty-state/empty-state.d.ts +38 -0
  42. package/dist/types/components/field/field.d.ts +231 -0
  43. package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
  44. package/dist/types/components/field-upload/field-upload.d.ts +213 -0
  45. package/dist/types/components/fieldset/fieldset.d.ts +125 -0
  46. package/dist/types/components/finder/finder.d.ts +252 -0
  47. package/dist/types/components/form/form.d.ts +194 -0
  48. package/dist/types/components/groupbox/groupbox.d.ts +89 -0
  49. package/dist/types/components/layout/layout.d.ts +132 -0
  50. package/dist/types/components/loading/inline-loading.d.ts +86 -0
  51. package/dist/types/components/loading/progress-bar.d.ts +168 -0
  52. package/dist/types/components/loading/spinner.d.ts +34 -0
  53. package/dist/types/components/master-panel/master-panel.d.ts +112 -0
  54. package/dist/types/components/menu-button/menu-button.d.ts +142 -0
  55. package/dist/types/components/message/message.d.ts +212 -0
  56. package/dist/types/components/modal/modal.d.ts +105 -0
  57. package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
  58. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
  59. package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
  60. package/dist/types/components/number-field/field-adapter.d.ts +5 -0
  61. package/dist/types/components/number-field/number-field.d.ts +292 -0
  62. package/dist/types/components/pagination/pagination.d.ts +267 -0
  63. package/dist/types/components/panel/panel.d.ts +194 -0
  64. package/dist/types/components/permission/permission.d.ts +93 -0
  65. package/dist/types/components/rating/field-adapter.d.ts +5 -0
  66. package/dist/types/components/rating/rating.d.ts +213 -0
  67. package/dist/types/components/search/search.d.ts +98 -0
  68. package/dist/types/components/select/field-adapter.d.ts +5 -0
  69. package/dist/types/components/select/filter.d.ts +11 -0
  70. package/dist/types/components/select/select.d.ts +235 -0
  71. package/dist/types/components/skeleton/skeleton.d.ts +97 -0
  72. package/dist/types/components/slider/field-adapter.d.ts +5 -0
  73. package/dist/types/components/slider/slider.d.ts +216 -0
  74. package/dist/types/components/split-view/split-view.d.ts +335 -0
  75. package/dist/types/components/stepper/stepper.d.ts +314 -0
  76. package/dist/types/components/stepper/steps.d.ts +131 -0
  77. package/dist/types/components/tabbox/tabbox.d.ts +230 -0
  78. package/dist/types/components/table/sort.d.ts +49 -0
  79. package/dist/types/components/table/table.d.ts +872 -0
  80. package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
  81. package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
  82. package/dist/types/components/timebox/field-adapter.d.ts +5 -0
  83. package/dist/types/components/timebox/timebox.d.ts +121 -0
  84. package/dist/types/components/toggle/field-adapter.d.ts +5 -0
  85. package/dist/types/components/toggle/toggle.d.ts +94 -0
  86. package/dist/types/components/toolbar/toolbar.d.ts +230 -0
  87. package/dist/types/components/tooltip/tooltip.d.ts +183 -0
  88. package/dist/types/components/tree/hierarchy.d.ts +113 -0
  89. package/dist/types/components/tree/tree.d.ts +383 -0
  90. package/dist/types/components/truncate/truncate.d.ts +61 -0
  91. package/dist/types/components/value-list/field-adapter.d.ts +5 -0
  92. package/dist/types/components/value-list/value-list.d.ts +143 -0
  93. package/dist/types/core/breakpoint.d.ts +68 -0
  94. package/dist/types/core/component.d.ts +115 -0
  95. package/dist/types/core/date.d.ts +50 -0
  96. package/dist/types/core/dom.d.ts +86 -0
  97. package/dist/types/core/export.d.ts +70 -0
  98. package/dist/types/core/fontawesome.d.ts +107 -0
  99. package/dist/types/core/format.d.ts +107 -0
  100. package/dist/types/core/http.d.ts +122 -0
  101. package/dist/types/core/i18n.d.ts +35 -0
  102. package/dist/types/core/icons.d.ts +137 -0
  103. package/dist/types/core/keyboard.d.ts +72 -0
  104. package/dist/types/core/position.d.ts +45 -0
  105. package/dist/types/core/storage.d.ts +46 -0
  106. package/dist/types/core/util.d.ts +117 -0
  107. package/dist/types/elements/define.d.ts +73 -0
  108. package/dist/types/elements/reflect.d.ts +27 -0
  109. package/dist/types/index.d.ts +79 -0
  110. package/dist/types/zeyos/connect.d.ts +65 -0
  111. package/dist/types/zeyos/form.d.ts +140 -0
  112. package/dist/types/zeyos/icons.d.ts +104 -0
  113. package/dist/types/zeyos/index.d.ts +12 -0
  114. package/dist/types/zeyos/modules.d.ts +110 -0
  115. package/dist/types/zeyos/query.d.ts +157 -0
  116. package/dist/types/zeyos/schema.d.ts +79 -0
  117. package/dist/types/zeyos/select.d.ts +167 -0
  118. package/dist/types/zeyos/table.d.ts +205 -0
  119. package/dist/zx-compat.esm.js +362 -33
  120. package/dist/zx-compat.esm.min.js +1 -1
  121. package/dist/zx-compat.global.js +359 -67
  122. package/dist/zx-compat.global.min.js +3 -3
  123. package/dist/zx-zeyos.esm.js +342 -43
  124. package/dist/zx-zeyos.esm.min.js +1 -1
  125. package/dist/zx.css +68 -6
  126. package/dist/zx.esm.js +346 -67
  127. package/dist/zx.esm.min.js +3 -3
  128. package/dist/zx.global.js +346 -67
  129. package/dist/zx.global.min.js +3 -3
  130. package/dist/zx.min.css +1 -1
  131. package/docs/api.json +48 -2
  132. package/docs/llms.md +28 -0
  133. package/package.json +37 -9
  134. package/src/compat/base.js +291 -0
  135. package/src/compat/globals.js +165 -0
  136. package/src/compat/index.js +84 -0
  137. package/src/compat/map/basic.js +464 -0
  138. package/src/compat/map/data.js +232 -0
  139. package/src/compat/map/date-time.js +177 -0
  140. package/src/compat/map/factory.js +45 -0
  141. package/src/compat/map/form.js +330 -0
  142. package/src/compat/map/helpers.js +46 -0
  143. package/src/compat/map/http.js +117 -0
  144. package/src/compat/map/layout.js +256 -0
  145. package/src/compat/map/options.js +267 -0
  146. package/src/compat/map/select.js +198 -0
  147. package/src/compat/map/stubs.js +26 -0
  148. package/src/compat/map/utilities.js +113 -0
  149. package/src/compat-entry.js +11 -0
  150. package/src/components/badge/badge.css +147 -0
  151. package/src/components/badge/badge.js +92 -0
  152. package/src/components/breadcrumb/breadcrumb.css +105 -0
  153. package/src/components/breadcrumb/breadcrumb.js +331 -0
  154. package/src/components/button/button.css +104 -0
  155. package/src/components/button/button.js +76 -0
  156. package/src/components/check-button/check-button.css +66 -0
  157. package/src/components/check-button/check-button.js +167 -0
  158. package/src/components/checklist/checklist.css +65 -0
  159. package/src/components/checklist/checklist.js +282 -0
  160. package/src/components/checklist/field-adapter.js +46 -0
  161. package/src/components/context-menu/context-menu.css +54 -0
  162. package/src/components/context-menu/context-menu.js +334 -0
  163. package/src/components/copy/copy-button.css +27 -0
  164. package/src/components/copy/copy-button.js +78 -0
  165. package/src/components/copy/copy-input.css +59 -0
  166. package/src/components/copy/copy-input.js +123 -0
  167. package/src/components/data-filter/data-filter.css +53 -0
  168. package/src/components/data-filter/data-filter.js +400 -0
  169. package/src/components/data-filter/filter-core.js +128 -0
  170. package/src/components/date-picker/date-picker.css +181 -0
  171. package/src/components/date-picker/date-picker.js +500 -0
  172. package/src/components/date-picker/date-range-picker.css +264 -0
  173. package/src/components/date-picker/date-range-picker.js +990 -0
  174. package/src/components/date-picker/field-adapter.js +36 -0
  175. package/src/components/date-picker/month-picker.js +223 -0
  176. package/src/components/date-picker/range-field-adapter.js +31 -0
  177. package/src/components/date-picker/time-picker.css +191 -0
  178. package/src/components/date-picker/time-picker.js +406 -0
  179. package/src/components/datebox/date-range-box.css +128 -0
  180. package/src/components/datebox/date-range-box.js +492 -0
  181. package/src/components/datebox/datebox.css +108 -0
  182. package/src/components/datebox/datebox.js +358 -0
  183. package/src/components/datebox/field-adapter.js +34 -0
  184. package/src/components/dialog/dialog.css +74 -0
  185. package/src/components/dialog/dialog.js +450 -0
  186. package/src/components/dropdown/dropdown.css +24 -0
  187. package/src/components/dropdown/dropdown.js +288 -0
  188. package/src/components/empty-state/empty-state.css +80 -0
  189. package/src/components/empty-state/empty-state.js +92 -0
  190. package/src/components/field/field.css +127 -0
  191. package/src/components/field/field.js +605 -0
  192. package/src/components/field-upload/field-adapter.js +21 -0
  193. package/src/components/field-upload/field-upload.css +108 -0
  194. package/src/components/field-upload/field-upload.js +475 -0
  195. package/src/components/fieldset/fieldset.css +37 -0
  196. package/src/components/fieldset/fieldset.js +307 -0
  197. package/src/components/finder/finder.css +98 -0
  198. package/src/components/finder/finder.js +509 -0
  199. package/src/components/form/form.css +32 -0
  200. package/src/components/form/form.js +302 -0
  201. package/src/components/groupbox/groupbox.css +51 -0
  202. package/src/components/groupbox/groupbox.js +199 -0
  203. package/src/components/layout/layout.css +117 -0
  204. package/src/components/layout/layout.js +136 -0
  205. package/src/components/loading/inline-loading.css +34 -0
  206. package/src/components/loading/inline-loading.js +140 -0
  207. package/src/components/loading/progress-bar.css +115 -0
  208. package/src/components/loading/progress-bar.js +259 -0
  209. package/src/components/loading/spinner.css +58 -0
  210. package/src/components/loading/spinner.js +57 -0
  211. package/src/components/master-panel/master-panel.css +51 -0
  212. package/src/components/master-panel/master-panel.js +203 -0
  213. package/src/components/menu-button/menu-button.css +49 -0
  214. package/src/components/menu-button/menu-button.js +294 -0
  215. package/src/components/message/message.css +122 -0
  216. package/src/components/message/message.js +499 -0
  217. package/src/components/modal/modal.css +47 -0
  218. package/src/components/modal/modal.js +203 -0
  219. package/src/components/multi-value-editor/field-adapter.js +44 -0
  220. package/src/components/multi-value-editor/multi-value-editor.css +56 -0
  221. package/src/components/multi-value-editor/multi-value-editor.js +170 -0
  222. package/src/components/navigation-bar/navigation-bar.css +130 -0
  223. package/src/components/navigation-bar/navigation-bar.js +450 -0
  224. package/src/components/number-field/field-adapter.js +40 -0
  225. package/src/components/number-field/number-field.css +77 -0
  226. package/src/components/number-field/number-field.js +545 -0
  227. package/src/components/pagination/pagination.css +95 -0
  228. package/src/components/pagination/pagination.js +481 -0
  229. package/src/components/panel/panel.css +123 -0
  230. package/src/components/panel/panel.js +336 -0
  231. package/src/components/permission/permission.css +47 -0
  232. package/src/components/permission/permission.js +198 -0
  233. package/src/components/rating/field-adapter.js +34 -0
  234. package/src/components/rating/rating.css +97 -0
  235. package/src/components/rating/rating.js +395 -0
  236. package/src/components/search/search.css +70 -0
  237. package/src/components/search/search.js +193 -0
  238. package/src/components/select/field-adapter.js +24 -0
  239. package/src/components/select/filter.js +39 -0
  240. package/src/components/select/select.css +180 -0
  241. package/src/components/select/select.js +663 -0
  242. package/src/components/skeleton/skeleton.css +76 -0
  243. package/src/components/skeleton/skeleton.js +139 -0
  244. package/src/components/slider/field-adapter.js +36 -0
  245. package/src/components/slider/slider.css +181 -0
  246. package/src/components/slider/slider.js +399 -0
  247. package/src/components/split-view/split-view.css +139 -0
  248. package/src/components/split-view/split-view.js +806 -0
  249. package/src/components/stepper/stepper.css +234 -0
  250. package/src/components/stepper/stepper.js +459 -0
  251. package/src/components/stepper/steps.js +131 -0
  252. package/src/components/tabbox/tabbox.css +281 -0
  253. package/src/components/tabbox/tabbox.js +484 -0
  254. package/src/components/table/sort.js +85 -0
  255. package/src/components/table/table.css +364 -0
  256. package/src/components/table/table.js +1693 -0
  257. package/src/components/tag-picker/field-adapter.js +35 -0
  258. package/src/components/tag-picker/tag-picker.css +169 -0
  259. package/src/components/tag-picker/tag-picker.js +837 -0
  260. package/src/components/timebox/field-adapter.js +24 -0
  261. package/src/components/timebox/timebox.css +51 -0
  262. package/src/components/timebox/timebox.js +276 -0
  263. package/src/components/toggle/field-adapter.js +24 -0
  264. package/src/components/toggle/toggle.css +66 -0
  265. package/src/components/toggle/toggle.js +150 -0
  266. package/src/components/toolbar/toolbar.css +81 -0
  267. package/src/components/toolbar/toolbar.js +511 -0
  268. package/src/components/tooltip/tooltip.css +36 -0
  269. package/src/components/tooltip/tooltip.js +420 -0
  270. package/src/components/tree/hierarchy.js +151 -0
  271. package/src/components/tree/tree.css +99 -0
  272. package/src/components/tree/tree.js +784 -0
  273. package/src/components/truncate/truncate.css +20 -0
  274. package/src/components/truncate/truncate.js +92 -0
  275. package/src/components/value-list/field-adapter.js +24 -0
  276. package/src/components/value-list/value-list.css +108 -0
  277. package/src/components/value-list/value-list.js +336 -0
  278. package/src/core/breakpoint.js +150 -0
  279. package/src/core/component.js +292 -0
  280. package/src/core/date.js +216 -0
  281. package/src/core/dom.js +260 -0
  282. package/src/core/export.js +204 -0
  283. package/src/core/fontawesome.js +177 -0
  284. package/src/core/format.js +283 -0
  285. package/src/core/http.js +246 -0
  286. package/src/core/i18n.js +83 -0
  287. package/src/core/icons.js +283 -0
  288. package/src/core/keyboard.js +240 -0
  289. package/src/core/position.js +177 -0
  290. package/src/core/storage.js +175 -0
  291. package/src/core/util.js +336 -0
  292. package/src/elements/define.js +715 -0
  293. package/src/elements/reflect.js +79 -0
  294. package/src/index.js +131 -0
  295. package/src/zeyos/connect.js +142 -0
  296. package/src/zeyos/form.js +254 -0
  297. package/src/zeyos/icons.js +92 -0
  298. package/src/zeyos/index.js +21 -0
  299. package/src/zeyos/modules.js +269 -0
  300. package/src/zeyos/query.js +199 -0
  301. package/src/zeyos/schema.js +474 -0
  302. package/src/zeyos/select.js +175 -0
  303. package/src/zeyos/table.js +272 -0
  304. package/styles/base.css +124 -0
  305. package/styles/icons.css +34 -0
  306. package/styles/tokens/dark.css +84 -0
  307. package/styles/tokens/density.css +9 -0
  308. package/styles/tokens/global.css +80 -0
  309. package/styles/tokens/modules.css +109 -0
  310. package/styles/tokens/semantic.css +49 -0
  311. package/styles/zx.css +61 -0
@@ -0,0 +1,605 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+ import { uid } from '../../core/util.js';
4
+
5
+ /**
6
+ * @typedef {Object} FieldOptions
7
+ * @property {string|null} [id=null] Control ID and logical field identifier.
8
+ * @property {string} [type='text'] Registered field type.
9
+ * @property {string} [label=''] Visible label.
10
+ * @property {string} [description=''] Supporting description.
11
+ * @property {unknown} [value=undefined] Initial value.
12
+ * @property {string} [placeholder=''] Control placeholder.
13
+ * @property {boolean} [required=false] Whether a value is required.
14
+ * @property {boolean} [disabled=false] Whether the control is disabled.
15
+ * @property {string[]|Record<string, string>|null} [options=null] Select or option-list values.
16
+ * @property {'stack'|'inline'} [layout='stack'] Label/control layout.
17
+ * @property {Record<string, unknown>} [props={}] Adapter-specific control properties.
18
+ * @property {FieldAdapterFactory} [adapter] Adapter used by the `custom` type.
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} FieldAdapter
23
+ * @property {Element} el Control root.
24
+ * @property {() => unknown} get Read the current value.
25
+ * @property {(value: unknown, options?: {silent?: boolean}) => void} set Set the current value.
26
+ * @property {() => void} focus Focus the control.
27
+ * @property {(disabled: boolean) => void} setDisabled Set disabled state.
28
+ * @property {() => void} [destroy] Release adapter-owned resources.
29
+ */
30
+
31
+ /** @typedef {(field: Field, options: Readonly<FieldOptions>) => FieldAdapter} FieldAdapterFactory */
32
+ /** @typedef {{valid: boolean, value: number|string}} NumericCoercion */
33
+
34
+ /**
35
+ * One labelled form control backed by a registered adapter.
36
+ * @fires Field#change
37
+ * @fires Field#invalid
38
+ * @extends {Component<FieldOptions>}
39
+ */
40
+ export class Field extends Component {
41
+ static cssName = 'field';
42
+
43
+ /** @type {FieldOptions & {msg: Record<string, string>}} */
44
+ static defaults = {
45
+ id: null,
46
+ type: 'text',
47
+ label: '',
48
+ description: '',
49
+ value: undefined,
50
+ placeholder: '',
51
+ required: false,
52
+ disabled: false,
53
+ options: null,
54
+ layout: 'stack',
55
+ props: {},
56
+ msg: {
57
+ 'field.int': 'Enter a whole number.',
58
+ 'field.float': 'Enter a valid number.',
59
+ 'field.unknown': 'Unknown field type: %1'
60
+ }
61
+ };
62
+
63
+ /** @type {Map<string, FieldAdapterFactory>} */
64
+ static #adapters = new Map();
65
+
66
+ /**
67
+ * Registers or replaces a field adapter.
68
+ * @param {string} type Adapter type name.
69
+ * @param {FieldAdapterFactory} adapter Adapter factory.
70
+ * @returns {typeof Field}
71
+ */
72
+ static register(type, adapter) {
73
+ const name = String(type).trim().toLowerCase();
74
+ if (!name) throw new TypeError('Field adapter type must not be empty');
75
+ if (typeof adapter !== 'function') throw new TypeError('Field adapter must be a function');
76
+ Field.#adapters.set(name, adapter);
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * Reports whether a field adapter is registered.
82
+ * @param {string} type Adapter type name.
83
+ * @returns {boolean}
84
+ */
85
+ static has(type) {
86
+ return Field.#adapters.has(String(type).trim().toLowerCase());
87
+ }
88
+
89
+ // `adapter` (FieldAdapter), `initialValue`, and `controlId` are assigned in render();
90
+ // declaring them as class fields would re-initialize them to undefined after the base
91
+ // constructor (which calls render()) returns — see AGENTS.md.
92
+
93
+ /** @returns {HTMLElement} */
94
+ render() {
95
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
96
+ this.el = root;
97
+ const type = String(this.options.type || 'text').toLowerCase();
98
+ const layout = this.options.layout === 'inline' ? 'inline' : 'stack';
99
+ this._ownedComponents = new Set();
100
+ this._ownedNodes = [];
101
+ this._rootAttributes = rememberAttributes(root, ['data-layout', 'data-field-type', 'data-disabled', 'data-state']);
102
+ root.dataset.layout = layout;
103
+ root.dataset.fieldType = type;
104
+
105
+ this.controlId = this.options.id ? String(this.options.id) : uid('zx-field');
106
+ const labelId = `${this.controlId}-label`;
107
+ const descriptionId = `${this.controlId}-description`;
108
+ const highlightId = `${this.controlId}-highlight`;
109
+ const label = h('label', {
110
+ class: 'zx-field__label',
111
+ id: labelId,
112
+ htmlFor: this.controlId
113
+ }, String(this.options.label ?? ''), this.options.required ? h('span', {
114
+ class: 'zx-field__required',
115
+ ariaHidden: 'true'
116
+ }, ' *') : null);
117
+ const controlSlot = h('div', { class: 'zx-field__control' });
118
+ const description = h('div', {
119
+ class: 'zx-field__description',
120
+ id: descriptionId
121
+ }, String(this.options.description ?? ''));
122
+ const highlight = h('div', {
123
+ class: 'zx-field__highlight',
124
+ id: highlightId,
125
+ ariaLive: 'polite',
126
+ hidden: true
127
+ });
128
+ const body = h('div', { class: 'zx-field__body' }, controlSlot, description, highlight);
129
+ this._ownedNodes.push(label, body);
130
+ root.append(label, body);
131
+
132
+ const factory = Field.#adapters.get(type);
133
+ if (!factory) {
134
+ const message = this.msg('field.unknown', type);
135
+ console.warn(`[zx.Field] ${message}`);
136
+ this.adapter = errorAdapter(message, this.controlId);
137
+ } else {
138
+ try {
139
+ this.adapter = validateAdapter(factory(this, /** @type {Readonly<FieldOptions>} */ (this.options)));
140
+ } catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ console.warn(`[zx.Field] Adapter "${type}" failed: ${message}`);
143
+ this.adapter = errorAdapter(message, this.controlId);
144
+ }
145
+ }
146
+
147
+ const control = this.adapter.el;
148
+ if (!control.id) control.id = this.controlId;
149
+ label.htmlFor = control.id;
150
+ if (!isLabelable(control)) control.setAttribute('aria-labelledby', labelId);
151
+ this._baseDescribedBy = control.getAttribute('aria-describedby') ?? '';
152
+ controlSlot.append(control);
153
+ this._descriptionId = String(this.options.description ?? '') ? descriptionId : '';
154
+ this._highlightId = highlightId;
155
+ this._highlight = highlight;
156
+ this._syncDescription();
157
+ this.initialValue = initialValue(type, this.options.value);
158
+ this.adapter.set(this.initialValue, { silent: true });
159
+ this.setDisabled(Boolean(this.options.disabled));
160
+ this.listen(control, 'change', () => {
161
+ this.emit('change', { value: this.getValue() });
162
+ });
163
+ return root;
164
+ }
165
+
166
+ /**
167
+ * Returns the adapter value.
168
+ * @returns {unknown}
169
+ */
170
+ getValue() {
171
+ return this.adapter.get();
172
+ }
173
+
174
+ /**
175
+ * Sets the adapter value.
176
+ * @param {unknown} value New value.
177
+ * @param {{silent?: boolean}} [options={}] Set `silent` to suppress `change`.
178
+ * @returns {this}
179
+ * @fires Field#change
180
+ */
181
+ setValue(value, { silent = false } = {}) {
182
+ this.adapter.set(value, { silent });
183
+ if (!silent) this.emit('change', { value: this.getValue() });
184
+ return this;
185
+ }
186
+
187
+ /**
188
+ * Focuses the adapter control.
189
+ * @returns {this}
190
+ */
191
+ focus() {
192
+ this.adapter.focus();
193
+ return this;
194
+ }
195
+
196
+ /**
197
+ * Restores the initial value without emitting `change`.
198
+ * @returns {this}
199
+ */
200
+ reset() {
201
+ this.setValue(this.initialValue, { silent: true });
202
+ this.clearHighlight();
203
+ return this;
204
+ }
205
+
206
+ /**
207
+ * Enables or disables the adapter control.
208
+ * @param {boolean} disabled Disabled state.
209
+ * @returns {this}
210
+ */
211
+ setDisabled(disabled) {
212
+ const value = Boolean(disabled);
213
+ this.adapter.setDisabled(value);
214
+ this.el.dataset.disabled = String(value);
215
+ return this;
216
+ }
217
+
218
+ /**
219
+ * Displays a validation or status message.
220
+ * @param {string} message Message text.
221
+ * @param {'danger'|'warning'|'success'} [kind='danger'] Message kind.
222
+ * @returns {this}
223
+ */
224
+ setHighlight(message, kind = 'danger') {
225
+ if (!message) return this.clearHighlight();
226
+ const normalizedKind = ['danger', 'warning', 'success'].includes(kind) ? kind : 'danger';
227
+ this.el.dataset.state = normalizedKind;
228
+ this._highlight.textContent = String(message ?? '');
229
+ this._highlight.hidden = !message;
230
+ if (normalizedKind === 'danger' && message) {
231
+ this._highlight.setAttribute('role', 'alert');
232
+ this.adapter.el.setAttribute('aria-invalid', 'true');
233
+ this.emit('invalid', { message: String(message) });
234
+ } else {
235
+ this._highlight.removeAttribute('role');
236
+ this.adapter.el.removeAttribute('aria-invalid');
237
+ }
238
+ this._syncDescription(Boolean(message));
239
+ return this;
240
+ }
241
+
242
+ /**
243
+ * Clears the current validation or status message.
244
+ * @returns {this}
245
+ */
246
+ clearHighlight() {
247
+ delete this.el.dataset.state;
248
+ this._highlight.textContent = '';
249
+ this._highlight.hidden = true;
250
+ this._highlight.removeAttribute('role');
251
+ this.adapter.el.removeAttribute('aria-invalid');
252
+ this._syncDescription(false);
253
+ return this;
254
+ }
255
+
256
+ /**
257
+ * Returns the adapter's control root.
258
+ * @returns {Element}
259
+ */
260
+ getInput() {
261
+ return this.adapter.el;
262
+ }
263
+
264
+ /**
265
+ * Registers a child component for destruction with this field.
266
+ * @template {Component} T
267
+ * @param {T} component Child component.
268
+ * @returns {T} The registered component.
269
+ */
270
+ own(component) {
271
+ if (!(component instanceof Component)) throw new TypeError('Field.own() requires a Component');
272
+ this._ownedComponents.add(component);
273
+ return component;
274
+ }
275
+
276
+ /** @param {boolean} [highlighted=false] @returns {void} */
277
+ _syncDescription(highlighted = false) {
278
+ const ids = [this._baseDescribedBy, this._descriptionId, highlighted ? this._highlightId : '']
279
+ .flatMap((value) => value.split(/\s+/)).filter(Boolean);
280
+ if (ids.length) this.adapter.el.setAttribute('aria-describedby', [...new Set(ids)].join(' '));
281
+ else this.adapter.el.removeAttribute('aria-describedby');
282
+ }
283
+
284
+ /** @returns {void} */
285
+ destroy() {
286
+ this.adapter?.destroy?.();
287
+ for (const component of this._ownedComponents ?? []) component.destroy();
288
+ this._ownedComponents?.clear();
289
+ for (const node of this._ownedNodes ?? []) node.remove();
290
+ restoreAttributes(this.el, this._rootAttributes ?? new Map());
291
+ super.destroy();
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Coerces a localized integer string. Empty strings remain empty and valid.
297
+ * @param {unknown} input Input value.
298
+ * @returns {NumericCoercion}
299
+ */
300
+ export function coerceInteger(input) {
301
+ if (typeof input === 'number') {
302
+ return Number.isInteger(input) ? { valid: true, value: input } : { valid: false, value: String(input) };
303
+ }
304
+ const source = String(input ?? '').trim();
305
+ if (source === '') return { valid: true, value: '' };
306
+ const parsed = parseLocalizedNumber(source);
307
+ if (parsed !== null && Number.isInteger(parsed)) return { valid: true, value: parsed };
308
+ return { valid: false, value: source };
309
+ }
310
+
311
+ /**
312
+ * Coerces a localized floating-point string. The rightmost comma or dot is the decimal separator
313
+ * when both occur, so values such as `1.234,56` are accepted.
314
+ * @param {unknown} input Input value.
315
+ * @returns {NumericCoercion}
316
+ */
317
+ export function coerceFloat(input) {
318
+ if (typeof input === 'number') {
319
+ return Number.isFinite(input) ? { valid: true, value: input } : { valid: false, value: String(input) };
320
+ }
321
+ const source = String(input ?? '').trim();
322
+ if (source === '') return { valid: true, value: '' };
323
+ const parsed = parseLocalizedNumber(source);
324
+ return parsed !== null ? { valid: true, value: parsed } : { valid: false, value: source };
325
+ }
326
+
327
+ /** @param {string} source @returns {number|null} */
328
+ function parseLocalizedNumber(source) {
329
+ const compact = source.replace(/[\s\u00a0]/g, '');
330
+ if (!/^[+-]?[\d.,]+$/.test(compact)) return null;
331
+ const sign = /^[+-]/.test(compact) ? compact[0] : '';
332
+ const unsigned = sign ? compact.slice(1) : compact;
333
+ if (!/\d/.test(unsigned)) return null;
334
+ const dots = [...unsigned.matchAll(/\./g)].map((match) => match.index);
335
+ const commas = [...unsigned.matchAll(/,/g)].map((match) => match.index);
336
+ let normalized;
337
+
338
+ if (dots.length && commas.length) {
339
+ const decimal = dots.at(-1) > commas.at(-1) ? '.' : ',';
340
+ const thousands = decimal === '.' ? ',' : '.';
341
+ const decimalIndex = unsigned.lastIndexOf(decimal);
342
+ const whole = unsigned.slice(0, decimalIndex);
343
+ const fraction = unsigned.slice(decimalIndex + 1);
344
+ if (!/^\d+$/.test(fraction) || !validGroupedWhole(whole, thousands)) return null;
345
+ normalized = whole.split(thousands).join('') + '.' + fraction;
346
+ } else if (commas.length > 1 || dots.length > 1) {
347
+ const separator = commas.length ? ',' : '.';
348
+ if (!validGroupedWhole(unsigned, separator)) return null;
349
+ normalized = unsigned.split(separator).join('');
350
+ } else if (commas.length === 1) {
351
+ normalized = unsigned.replace(',', '.');
352
+ } else {
353
+ normalized = unsigned;
354
+ }
355
+
356
+ if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(normalized)) return null;
357
+ const result = Number(sign + normalized);
358
+ return Number.isFinite(result) ? result : null;
359
+ }
360
+
361
+ /** @param {string} value @param {string} separator @returns {boolean} */
362
+ function validGroupedWhole(value, separator) {
363
+ const groups = value.split(separator);
364
+ return groups.length > 0 && /^\d{1,3}$/.test(groups[0]) && groups.slice(1).every((part) => /^\d{3}$/.test(part));
365
+ }
366
+
367
+ /** @param {string} type @param {unknown} value @returns {unknown} */
368
+ function initialValue(type, value) {
369
+ if (value !== undefined) return value;
370
+ if (type === 'checkbox') return false;
371
+ return '';
372
+ }
373
+
374
+ /** @param {unknown} adapter @returns {FieldAdapter} */
375
+ function validateAdapter(adapter) {
376
+ if (!adapter || typeof adapter !== 'object') throw new TypeError('Adapter must return an object');
377
+ if (!(adapter.el && adapter.el.nodeType === 1)) throw new TypeError('Adapter must provide an Element');
378
+ for (const method of ['get', 'set', 'focus', 'setDisabled']) {
379
+ if (typeof adapter[method] !== 'function') throw new TypeError(`Adapter is missing ${method}()`);
380
+ }
381
+ return /** @type {FieldAdapter} */ (adapter);
382
+ }
383
+
384
+ /** @param {string} message @param {string} id @returns {FieldAdapter} */
385
+ function errorAdapter(message, id) {
386
+ const el = h('div', { class: 'zx-field__error-control', id, role: 'alert', tabIndex: -1 }, message);
387
+ return {
388
+ el,
389
+ get: () => null,
390
+ set: () => {},
391
+ focus: () => el.focus(),
392
+ setDisabled: () => {}
393
+ };
394
+ }
395
+
396
+ /** @param {Element} element @returns {boolean} */
397
+ function isLabelable(element) {
398
+ return ['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA'].includes(element.tagName);
399
+ }
400
+
401
+ /** @param {HTMLElement} element @param {string[]} names @returns {Map<string, string|null>} */
402
+ function rememberAttributes(element, names) {
403
+ return new Map(names.map((name) => [name, element.getAttribute(name)]));
404
+ }
405
+
406
+ /** @param {Element} element @param {Map<string, string|null>} attributes @returns {void} */
407
+ function restoreAttributes(element, attributes) {
408
+ for (const [name, value] of attributes) {
409
+ if (value === null) element.removeAttribute(name);
410
+ else element.setAttribute(name, value);
411
+ }
412
+ }
413
+
414
+ /** @param {Readonly<FieldOptions>} options @param {Record<string, unknown>} base @returns {Record<string, unknown>} */
415
+ function controlProps(options, base) {
416
+ const props = { ...(options.props ?? {}) };
417
+ for (const key of Object.keys(props)) {
418
+ if (/^on[a-z]/i.test(key) || key === 'content') delete props[key];
419
+ }
420
+ return { ...props, ...base };
421
+ }
422
+
423
+ /** @param {'input'|'textarea'} tag @param {string} type @returns {FieldAdapterFactory} */
424
+ function textAdapter(tag, type) {
425
+ return (_field, options) => {
426
+ const el = h(tag, controlProps(options, {
427
+ type: tag === 'input' ? type : undefined,
428
+ placeholder: options.placeholder,
429
+ required: options.required,
430
+ disabled: options.disabled
431
+ }));
432
+ return elementValueAdapter(el);
433
+ };
434
+ }
435
+
436
+ /** @param {HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement} el @returns {FieldAdapter} */
437
+ function elementValueAdapter(el) {
438
+ return {
439
+ el,
440
+ get: () => el.value,
441
+ set: (value) => { el.value = value == null ? '' : String(value); },
442
+ focus: () => el.focus(),
443
+ setDisabled: (disabled) => { el.disabled = disabled; }
444
+ };
445
+ }
446
+
447
+ /** @param {'int'|'float'} kind @returns {FieldAdapterFactory} */
448
+ function numericAdapter(kind) {
449
+ return (field, options) => {
450
+ const coerce = kind === 'int' ? coerceInteger : coerceFloat;
451
+ const input = h('input', controlProps(options, {
452
+ type: 'text',
453
+ inputMode: kind === 'int' ? 'numeric' : 'decimal',
454
+ placeholder: options.placeholder,
455
+ required: options.required,
456
+ disabled: options.disabled
457
+ }));
458
+ field.listen(input, 'blur', () => {
459
+ const result = coerce(input.value);
460
+ if (!result.valid) {
461
+ field.setHighlight(field.msg(`field.${kind}`), 'danger');
462
+ return;
463
+ }
464
+ if (result.value !== '') input.value = String(result.value);
465
+ field.clearHighlight();
466
+ });
467
+ return {
468
+ ...elementValueAdapter(input),
469
+ get: () => coerce(input.value).value
470
+ };
471
+ };
472
+ }
473
+
474
+ /** @type {FieldAdapterFactory} */
475
+ const checkboxAdapter = (_field, options) => {
476
+ const input = h('input', controlProps(options, {
477
+ type: 'checkbox',
478
+ required: options.required,
479
+ disabled: options.disabled
480
+ }));
481
+ return {
482
+ el: input,
483
+ get: () => input.checked,
484
+ set: (value) => { input.checked = Boolean(value); },
485
+ focus: () => input.focus(),
486
+ setDisabled: (disabled) => { input.disabled = disabled; }
487
+ };
488
+ };
489
+
490
+ /** @type {FieldAdapterFactory} */
491
+ const selectAdapter = (_field, options) => {
492
+ const select = h('select', controlProps(options, {
493
+ required: options.required,
494
+ disabled: options.disabled
495
+ }));
496
+ for (const [value, label] of normalizeOptions(options.options)) {
497
+ select.append(h('option', { value }, label));
498
+ }
499
+ return elementValueAdapter(select);
500
+ };
501
+
502
+ /** @type {FieldAdapterFactory} */
503
+ const optionListAdapter = (_field, options) => {
504
+ const root = h('div', {
505
+ class: 'zx-field__option-list',
506
+ role: 'radiogroup'
507
+ });
508
+ const name = options.id ? String(options.id) : uid('zx-option-list');
509
+ const radios = normalizeOptions(options.options).map(([value, label], index) => {
510
+ const input = h('input', {
511
+ type: 'radio',
512
+ name,
513
+ value,
514
+ required: Boolean(options.required) && index === 0,
515
+ disabled: options.disabled
516
+ });
517
+ root.append(h('label', { class: 'zx-field__option' }, input, h('span', {}, label)));
518
+ return input;
519
+ });
520
+ return {
521
+ el: root,
522
+ get: () => radios.find((radio) => radio.checked)?.value ?? '',
523
+ set: (value) => {
524
+ for (const radio of radios) radio.checked = String(value ?? '') === radio.value;
525
+ },
526
+ focus: () => (radios.find((radio) => radio.checked) ?? radios[0])?.focus(),
527
+ setDisabled: (disabled) => {
528
+ for (const radio of radios) radio.disabled = disabled;
529
+ root.setAttribute('aria-disabled', String(disabled));
530
+ }
531
+ };
532
+ };
533
+
534
+ /** @type {FieldAdapterFactory} */
535
+ const htmlAdapter = (_field, options) => {
536
+ const el = h('div', controlProps(options, { class: 'zx-field__static' }));
537
+ let adopted = null;
538
+ const set = (value) => {
539
+ restoreAdopted(adopted);
540
+ adopted = null;
541
+ if (value && typeof value === 'object' && typeof value.nodeType === 'number') {
542
+ adopted = { node: value, parent: value.parentNode, next: value.nextSibling };
543
+ el.replaceChildren(value);
544
+ } else {
545
+ el.textContent = value == null ? '' : String(value);
546
+ }
547
+ };
548
+ return {
549
+ el,
550
+ get: () => el.textContent,
551
+ set,
552
+ focus: () => {},
553
+ setDisabled: (disabled) => { el.setAttribute('aria-disabled', String(disabled)); },
554
+ destroy: () => restoreAdopted(adopted)
555
+ };
556
+ };
557
+
558
+ /** @param {{node: Node, parent: Node|null, next: Node|null}|null} adopted @returns {void} */
559
+ function restoreAdopted(adopted) {
560
+ if (!adopted) return;
561
+ if (adopted.parent) adopted.parent.insertBefore(
562
+ adopted.node,
563
+ adopted.next?.parentNode === adopted.parent ? adopted.next : null
564
+ );
565
+ else adopted.node.remove();
566
+ }
567
+
568
+ /** @type {FieldAdapterFactory} */
569
+ const customAdapter = (field, options) => {
570
+ if (typeof options.adapter !== 'function') throw new TypeError('Custom fields require options.adapter');
571
+ return options.adapter(field, options);
572
+ };
573
+
574
+ /** @param {FieldOptions['options']} options @returns {Array<[string, string]>} */
575
+ function normalizeOptions(options) {
576
+ if (Array.isArray(options)) return options.map((value) => [String(value), String(value)]);
577
+ if (options && typeof options === 'object') {
578
+ return Object.entries(options).map(([value, label]) => [value, String(label)]);
579
+ }
580
+ return [];
581
+ }
582
+
583
+ Field.register('text', textAdapter('input', 'text'));
584
+ Field.register('password', textAdapter('input', 'password'));
585
+ Field.register('textarea', textAdapter('textarea', 'text'));
586
+ Field.register('checkbox', checkboxAdapter);
587
+ Field.register('int', numericAdapter('int'));
588
+ Field.register('float', numericAdapter('float'));
589
+ Field.register('select', selectAdapter);
590
+ Field.register('optionlist', optionListAdapter);
591
+ Field.register('hidden', textAdapter('input', 'hidden'));
592
+ Field.register('html', htmlAdapter);
593
+ Field.register('custom', customAdapter);
594
+
595
+ /**
596
+ * Field value change event.
597
+ * @event Field#change
598
+ * @type {CustomEvent<{value: unknown}>}
599
+ */
600
+
601
+ /**
602
+ * Field validation failure event.
603
+ * @event Field#invalid
604
+ * @type {CustomEvent<{message: string}>}
605
+ */
@@ -0,0 +1,21 @@
1
+ import { Field } from '../field/field.js';
2
+ import { FieldUpload } from './field-upload.js';
3
+
4
+ /**
5
+ * Registers the `upload` field type. Upload state remains server-owned, so its form value is null.
6
+ * @returns {void}
7
+ */
8
+ export function registerFieldUploadAdapter() {
9
+ Field.register('upload', (field, options) => {
10
+ const upload = field.own(new FieldUpload(null, { ...(options.props ?? {}) }));
11
+ field.listen(upload.el, 'change', (event) => event.stopImmediatePropagation());
12
+ upload.on('select', () => field.emit('change', { value: null }));
13
+ return {
14
+ el: upload.el,
15
+ get: () => null,
16
+ set: () => upload.clear(),
17
+ focus: () => upload.el.querySelector('.zx-field-upload__dropzone')?.focus(),
18
+ setDisabled: (disabled) => upload.setDisabled(disabled)
19
+ };
20
+ });
21
+ }