@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,336 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+ import { isElement, uid } from '../../core/util.js';
5
+ import { button } from '../button/button.js';
6
+
7
+ /** @typedef {string|Node|Component|null} PanelContent */
8
+ /**
9
+ * @typedef {Object} PanelButtonDescriptor
10
+ * @property {string} [label=''] Visible button label.
11
+ * @property {string|null} [icon=null] Icon name from the kernel icon set.
12
+ * @property {'default'|'primary'|'danger'|'ghost'} [kind='default'] Visual intent.
13
+ * @property {'md'|'sm'} [size='md'] Control size.
14
+ * @property {boolean} [disabled=false] Whether the button is disabled.
15
+ * @property {string} [title] Native title text.
16
+ * @property {(event: MouseEvent) => void} [onclick] Click callback.
17
+ */
18
+ /** @typedef {Element|PanelButtonDescriptor} PanelButton */
19
+
20
+ /**
21
+ * @typedef {Object} PanelOptions
22
+ * @property {string} [title=''] Section title.
23
+ * @property {PanelContent} [content=null] Initial body content.
24
+ * @property {boolean} [open=true] Initial expanded state.
25
+ * @property {boolean} [collapsible=true] Whether the header toggles the body.
26
+ * @property {PanelButton[]} [buttons=[]] Header actions, right-aligned next to the title.
27
+ * @property {PanelContent} [footer=null] Optional footer content.
28
+ * @property {PanelButton[]} [footerButtons=[]] Footer actions, right-aligned in the footer bar.
29
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open callback.
30
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close callback.
31
+ */
32
+
33
+ /**
34
+ * Framed, optionally collapsible titled section.
35
+ *
36
+ * The header is a row rather than one big button: the title and chevron form the collapse control
37
+ * and the action area sits beside it, because a button may not contain other buttons.
38
+ * @fires Panel#open
39
+ * @fires Panel#close
40
+ * @extends {Component<PanelOptions>}
41
+ */
42
+ export class Panel extends Component {
43
+ static cssName = 'panel';
44
+
45
+ /** @type {Readonly<PanelOptions>} */
46
+ static defaults = {
47
+ title: '',
48
+ content: null,
49
+ open: true,
50
+ collapsible: true,
51
+ buttons: [],
52
+ footer: null,
53
+ footerButtons: []
54
+ };
55
+
56
+ /**
57
+ * Creates or enhances a panel.
58
+ * @param {Element|string|null} target Existing container, selector, or null.
59
+ * @param {PanelOptions} [options={}] Panel options.
60
+ */
61
+ constructor(target, options = {}) {
62
+ super(target, options);
63
+ }
64
+
65
+ /** @returns {HTMLElement} */
66
+ render() {
67
+ const created = !this.el;
68
+ const root = /** @type {HTMLElement} */ (this.el ?? h('section'));
69
+ this.el = root;
70
+ this._createdRoot = created;
71
+ this._original = created ? null : snapshot(root);
72
+ this._cleaned = false;
73
+ this._open = Boolean(this.options.open);
74
+ this._collapsible = Boolean(this.options.collapsible);
75
+
76
+ this._hasFooterContent = false;
77
+
78
+ const originalContent = created ? [] : Array.from(root.childNodes);
79
+ const bodyId = uid('zx-panel-body');
80
+ const title = h('span', { class: 'zx-panel__title', ref: 'title' });
81
+ const toggleChildren = [];
82
+ if (this._collapsible) {
83
+ toggleChildren.push(h('span', {
84
+ class: 'zx-panel__chevron',
85
+ ariaHidden: 'true'
86
+ }, icon('chevron-right', { size: 16 })));
87
+ }
88
+ toggleChildren.push(title);
89
+
90
+ const toggle = this._collapsible ? h('button', {
91
+ class: 'zx-panel__toggle',
92
+ ref: 'toggle',
93
+ type: 'button',
94
+ ariaControls: bodyId,
95
+ ariaExpanded: String(this._open)
96
+ }, toggleChildren) : h('div', {
97
+ class: 'zx-panel__toggle',
98
+ ref: 'toggle'
99
+ }, toggleChildren);
100
+ const header = h('div', { class: 'zx-panel__header', ref: 'header' },
101
+ toggle,
102
+ h('div', {
103
+ class: 'zx-panel__buttons',
104
+ ref: 'buttons',
105
+ role: 'group',
106
+ ariaLabel: 'Panel actions'
107
+ })
108
+ );
109
+ const content = h('div', {
110
+ class: 'zx-panel__content',
111
+ ref: 'content'
112
+ });
113
+ const body = h('div', {
114
+ class: 'zx-panel__body',
115
+ ref: 'body',
116
+ id: bodyId
117
+ }, content);
118
+ const footer = h('div', {
119
+ class: 'zx-panel__footer',
120
+ ref: 'footer',
121
+ hidden: true
122
+ },
123
+ h('div', { class: 'zx-panel__footer-content', ref: 'footerContent' }),
124
+ h('div', {
125
+ class: 'zx-panel__footer-buttons',
126
+ ref: 'footerButtons',
127
+ role: 'group',
128
+ ariaLabel: 'Panel footer actions'
129
+ }));
130
+ root.replaceChildren(header, body, footer);
131
+
132
+ this.setTitle(this.options.title);
133
+ if (this.options.content !== null) this.setContent(this.options.content);
134
+ else content.append(...originalContent);
135
+ this.setButtons(this.options.buttons);
136
+ this.setFooter(this.options.footer);
137
+ this.setFooterButtons(this.options.footerButtons);
138
+ this._syncOpenState();
139
+
140
+ if (this._collapsible) {
141
+ this.listen(toggle, 'click', () => this.toggle());
142
+ }
143
+ return root;
144
+ }
145
+
146
+ /**
147
+ * Replaces the visible title.
148
+ * @param {string} title Next title.
149
+ * @returns {this}
150
+ */
151
+ setTitle(title) {
152
+ this.refs.title.textContent = String(title ?? '');
153
+ return this;
154
+ }
155
+
156
+ /**
157
+ * Replaces the panel body.
158
+ * @param {PanelContent} content Next text, node, component, or null to clear.
159
+ * @returns {this}
160
+ */
161
+ setContent(content) {
162
+ replaceContent(this.refs.content, content, 'Panel content');
163
+ return this;
164
+ }
165
+
166
+ /**
167
+ * Replaces the header action area, right-aligned beside the title.
168
+ * @param {PanelButton[]} list Button descriptors or elements.
169
+ * @returns {this}
170
+ */
171
+ setButtons(list) {
172
+ this.refs.buttons.replaceChildren(...this._buildButtons(list, 'Panel buttons'));
173
+ return this;
174
+ }
175
+
176
+ /**
177
+ * Replaces or removes the footer content. The footer bar appears whenever it has content,
178
+ * footer buttons, or both.
179
+ * @param {PanelContent} content Next footer content, or null to remove it.
180
+ * @returns {this}
181
+ */
182
+ setFooter(content) {
183
+ replaceContent(this.refs.footerContent, content, 'Panel footer');
184
+ this._hasFooterContent = content !== null;
185
+ this._syncFooter();
186
+ return this;
187
+ }
188
+
189
+ /**
190
+ * Replaces the footer action area, right-aligned in the footer bar.
191
+ * @param {PanelButton[]} list Button descriptors or elements.
192
+ * @returns {this}
193
+ */
194
+ setFooterButtons(list) {
195
+ this.refs.footerButtons.replaceChildren(
196
+ ...this._buildButtons(list, 'Panel footer buttons'));
197
+ this._syncFooter();
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Expands the panel.
203
+ * @returns {this}
204
+ * @fires Panel#open
205
+ */
206
+ open() {
207
+ if (this._open) return this;
208
+ this._open = true;
209
+ this._syncOpenState();
210
+ this.emit('open');
211
+ return this;
212
+ }
213
+
214
+ /**
215
+ * Collapses the panel.
216
+ * @returns {this}
217
+ * @fires Panel#close
218
+ */
219
+ close() {
220
+ if (!this._open) return this;
221
+ this._open = false;
222
+ this._syncOpenState();
223
+ this.emit('close');
224
+ return this;
225
+ }
226
+
227
+ /**
228
+ * Toggles the panel's expanded state.
229
+ * @returns {this}
230
+ * @fires Panel#open
231
+ * @fires Panel#close
232
+ */
233
+ toggle() {
234
+ return this.isOpen() ? this.close() : this.open();
235
+ }
236
+
237
+ /**
238
+ * Reports whether the body is expanded.
239
+ * @returns {boolean}
240
+ */
241
+ isOpen() {
242
+ return this._open;
243
+ }
244
+
245
+ /** @returns {void} */
246
+ destroy() {
247
+ if (this._cleaned) return;
248
+ this._cleaned = true;
249
+ super.destroy();
250
+ if (!this._createdRoot && this._original) restore(this.el, this._original);
251
+ }
252
+
253
+ /** @returns {void} */
254
+ _syncOpenState() {
255
+ this.el.setAttribute('data-state', this._open ? 'open' : 'closed');
256
+ this.refs.body.hidden = !this._open;
257
+ if (this._collapsible) {
258
+ this.refs.toggle.setAttribute('aria-expanded', String(this._open));
259
+ }
260
+ }
261
+
262
+ /** @returns {void} */
263
+ _syncFooter() {
264
+ this.refs.footer.hidden = !this._hasFooterContent
265
+ && this.refs.footerButtons.childElementCount === 0;
266
+ }
267
+
268
+ /**
269
+ * Turns button descriptors into elements, passing through any Element unchanged.
270
+ * @param {PanelButton[]} list Button descriptors or elements.
271
+ * @param {string} label Error-message prefix.
272
+ * @returns {Element[]}
273
+ */
274
+ _buildButtons(list, label) {
275
+ if (!Array.isArray(list)) throw new TypeError(`${label} must be an array`);
276
+ return list.map((item) => {
277
+ if (isElement(item)) return item;
278
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
279
+ throw new TypeError(`${label} must be Elements or button descriptors`);
280
+ }
281
+ const descriptor = { ...item };
282
+ const onclick = descriptor.onclick;
283
+ delete descriptor.onclick;
284
+ const element = button(descriptor);
285
+ if (typeof onclick === 'function') this.listen(element, 'click', onclick);
286
+ return element;
287
+ });
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Emitted after the panel expands.
293
+ * @event Panel#open
294
+ * @type {CustomEvent<Record<string, never>>}
295
+ */
296
+
297
+ /**
298
+ * Emitted after the panel collapses.
299
+ * @event Panel#close
300
+ * @type {CustomEvent<Record<string, never>>}
301
+ */
302
+
303
+ /** @param {Element} element @returns {{attributes: Array<[string, string]>, children: Node[]}} */
304
+ function snapshot(element) {
305
+ return {
306
+ attributes: Array.from(element.attributes, (attribute) => [attribute.name, attribute.value]),
307
+ children: Array.from(element.childNodes)
308
+ };
309
+ }
310
+
311
+ /**
312
+ * @param {Element} element
313
+ * @param {{attributes: Array<[string, string]>, children: Node[]}} original
314
+ * @returns {void}
315
+ */
316
+ function restore(element, original) {
317
+ for (const attribute of Array.from(element.attributes)) element.removeAttribute(attribute.name);
318
+ for (const [name, value] of original.attributes) element.setAttribute(name, value);
319
+ element.replaceChildren(...original.children);
320
+ }
321
+
322
+ /** @param {Element} target @param {PanelContent} content @param {string} label @returns {void} */
323
+ function replaceContent(target, content, label) {
324
+ target.replaceChildren();
325
+ if (content === null) return;
326
+ if (typeof content === 'string') {
327
+ target.append(document.createTextNode(content));
328
+ return;
329
+ }
330
+ const node = content instanceof Component ? content.toElement() : content;
331
+ if (node && typeof node.nodeType === 'number') {
332
+ target.append(node);
333
+ return;
334
+ }
335
+ throw new TypeError(`${label} must be a string, Node, Component, or null`);
336
+ }
@@ -0,0 +1,47 @@
1
+ .zx-permission {
2
+ min-inline-size: 0;
3
+ color: var(--zx-color-text);
4
+ font-size: var(--zx-control-font-size);
5
+ }
6
+
7
+ .zx-permission__choices {
8
+ display: grid;
9
+ grid-template-columns: repeat(3, max-content) minmax(160px, 1fr);
10
+ align-items: center;
11
+ gap: var(--zx-space-2) var(--zx-space-4);
12
+ }
13
+
14
+ .zx-permission__choice {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ gap: var(--zx-space-2);
18
+ min-block-size: var(--zx-control-height);
19
+ cursor: pointer;
20
+ }
21
+
22
+ .zx-permission__choice input {
23
+ accent-color: var(--zx-color-accent);
24
+ }
25
+
26
+ .zx-permission__choice:has(input:focus-visible) {
27
+ border-radius: var(--zx-radius-sm);
28
+ box-shadow: var(--zx-focus-ring);
29
+ }
30
+
31
+ .zx-permission__groups {
32
+ min-inline-size: 160px;
33
+ }
34
+
35
+ .zx-permission:not([data-state="group"]) .zx-permission__groups {
36
+ color: var(--zx-color-text-muted);
37
+ }
38
+
39
+ @media (max-width: 640px) {
40
+ .zx-permission__choices {
41
+ grid-template-columns: 1fr;
42
+ }
43
+
44
+ .zx-permission__groups {
45
+ inline-size: 100%;
46
+ }
47
+ }
@@ -0,0 +1,198 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+ import { uid } from '../../core/util.js';
4
+ import { Select } from '../select/select.js';
5
+
6
+ /** @typedef {Record<string, any>|string|number|boolean|symbol|bigint|null|undefined} PermissionGroup */
7
+ /**
8
+ * @typedef {Object} PermissionOptions
9
+ * @property {boolean|string|number|symbol|bigint|null} [value=true] Public, private, or group ID.
10
+ * @property {PermissionGroup[]} [groups=[]] Available groups.
11
+ * @property {string|((item: PermissionGroup) => unknown)} [groupsValueKey='ID'] Group ID property or reader.
12
+ * @property {string|((item: PermissionGroup) => string)} [groupsLabelKey='name'] Group label property or reader.
13
+ * @property {Record<string, string>|Record<string, Record<string, string>>} [msg] Localized messages.
14
+ * @property {(event: CustomEvent<PermissionChangeDetail>) => void} [onchange] Change listener.
15
+ */
16
+ /** @typedef {{value: unknown}} PermissionChangeDetail */
17
+
18
+ /**
19
+ * Record-permission choice for private, public, and group access.
20
+ *
21
+ * @fires Permission#change
22
+ * @extends {Component<PermissionOptions>}
23
+ */
24
+ export class Permission extends Component {
25
+ static cssName = 'permission';
26
+
27
+ /** @type {PermissionOptions} */
28
+ static defaults = {
29
+ value: true,
30
+ groups: [],
31
+ groupsValueKey: 'ID',
32
+ groupsLabelKey: 'name'
33
+ };
34
+
35
+
36
+ /** @returns {HTMLElement} */
37
+ render() {
38
+ // Instance state initialized here because render() runs inside the base
39
+ // constructor, before class-field initializers would run (and clobber it).
40
+ this._mode = 'public'; this._groupValue = null; this._groupSelect = null; this._createdRoot = false; this._originalChildren = []; this._root = null; this._originalState = null;
41
+ this._createdRoot = this.el === null;
42
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
43
+ this.el = root;
44
+ this._root = root;
45
+ if (!this._createdRoot) {
46
+ this._originalChildren = Array.from(root.childNodes);
47
+ this._originalState = root.getAttribute('data-state');
48
+ }
49
+ const name = uid('zx-permission');
50
+ const choices = h('div', {
51
+ class: 'zx-permission__choices',
52
+ role: 'radiogroup',
53
+ ariaLabel: this._message('permission.label', 'Permission')
54
+ });
55
+ const privateChoice = this._choice(name, 'private', this._message('permission.private', 'Private'));
56
+ const publicChoice = this._choice(name, 'public', this._message('permission.public', 'Public'));
57
+ const groupChoice = this._choice(name, 'group', this._message('permission.group', 'Group'));
58
+ const groupHost = h('div', { ref: 'groupHost', class: 'zx-permission__groups' });
59
+ choices.append(privateChoice.label, publicChoice.label, groupChoice.label, groupHost);
60
+ root.replaceChildren(choices);
61
+ this.refs.private = privateChoice.input;
62
+ this.refs.public = publicChoice.input;
63
+ this.refs.group = groupChoice.input;
64
+
65
+ this._groupSelect = new Select(groupHost, {
66
+ items: Array.isArray(this.options.groups) ? this.options.groups : [],
67
+ valueKey: this.options.groupsValueKey,
68
+ labelKey: this.options.groupsLabelKey,
69
+ placeholder: this._message('permission.group', 'Group')
70
+ });
71
+ this._groupSelect.refs.input.setAttribute(
72
+ 'aria-label',
73
+ this._message('permission.group', 'Group')
74
+ );
75
+ for (const input of [privateChoice.input, publicChoice.input, groupChoice.input]) {
76
+ this.listen(input, 'change', () => {
77
+ if (!input.checked) return;
78
+ if (input.value === 'group') this._selectGroupMode();
79
+ else this.set(input.value);
80
+ });
81
+ }
82
+ this.listen(groupHost, 'zx-change', (event) => {
83
+ event.stopPropagation();
84
+ if (this._mode !== 'group') return;
85
+ const previous = this.get();
86
+ this._groupValue = /** @type {CustomEvent} */ (event).detail.value;
87
+ const value = this.get();
88
+ if (!Object.is(previous, value)) this.emit('change', { value });
89
+ });
90
+
91
+ this.set(this.options.value, { silent: true });
92
+ return root;
93
+ }
94
+
95
+ /** Returns the normalized permission value. @returns {'private'|'public'|unknown} */
96
+ get() {
97
+ if (this._mode === 'private') return 'private';
98
+ if (this._mode === 'public') return 'public';
99
+ return this._groupValue;
100
+ }
101
+
102
+ /**
103
+ * Sets a public, private, or group permission.
104
+ * @param {boolean|string|number|symbol|bigint|null} value Permission value.
105
+ * @param {{silent?: boolean}} [options={}] Update behavior.
106
+ * @returns {this}
107
+ * @fires Permission#change
108
+ */
109
+ set(value, { silent = false } = {}) {
110
+ const previous = this.get();
111
+ if (value === true || value === 'public') {
112
+ this._mode = 'public';
113
+ } else if (value === false || value === 'private') {
114
+ this._mode = 'private';
115
+ } else {
116
+ this._mode = 'group';
117
+ this._groupValue = value;
118
+ this._groupSelect?.set(value, { silent: true });
119
+ }
120
+ this._sync();
121
+ const next = this.get();
122
+ if (!silent && !Object.is(previous, next)) this.emit('change', { value: next });
123
+ return this;
124
+ }
125
+
126
+ /** Destroys the embedded Select and restores an enhanced target. @returns {void} */
127
+ destroy() {
128
+ this._groupSelect?.destroy();
129
+ this._groupSelect = null;
130
+ if (!this._createdRoot && this.el) {
131
+ this.el.replaceChildren(...this._originalChildren);
132
+ if (this._originalState === null) this.el.removeAttribute('data-state');
133
+ else this.el.setAttribute('data-state', this._originalState);
134
+ }
135
+ super.destroy();
136
+ }
137
+
138
+ /** @returns {void} */
139
+ _selectGroupMode() {
140
+ const previous = this.get();
141
+ this._mode = 'group';
142
+ if (this._groupValue == null) {
143
+ const first = Array.isArray(this.options.groups) ? this.options.groups[0] : null;
144
+ this._groupValue = first == null ? null : read(first, this.options.groupsValueKey);
145
+ }
146
+ this._groupSelect?.set(this._groupValue, { silent: true });
147
+ this._sync();
148
+ const next = this.get();
149
+ if (!Object.is(previous, next)) this.emit('change', { value: next });
150
+ }
151
+
152
+ /** @returns {void} */
153
+ _sync() {
154
+ /** @type {HTMLInputElement} */ (this.refs.private).checked = this._mode === 'private';
155
+ /** @type {HTMLInputElement} */ (this.refs.public).checked = this._mode === 'public';
156
+ /** @type {HTMLInputElement} */ (this.refs.group).checked = this._mode === 'group';
157
+ if (this._mode === 'group') this._groupSelect?.enable();
158
+ else this._groupSelect?.disable();
159
+ this._root?.setAttribute('data-state', this._mode);
160
+ }
161
+
162
+ /**
163
+ * @param {string} name
164
+ * @param {'private'|'public'|'group'} value
165
+ * @param {string} text
166
+ * @returns {{label: HTMLLabelElement, input: HTMLInputElement}}
167
+ */
168
+ _choice(name, value, text) {
169
+ const input = /** @type {HTMLInputElement} */ (h('input', {
170
+ type: 'radio',
171
+ name,
172
+ value
173
+ }));
174
+ const label = /** @type {HTMLLabelElement} */ (h('label', {
175
+ class: 'zx-permission__choice'
176
+ }, input, h('span', {}, text)));
177
+ return { label, input };
178
+ }
179
+
180
+ /** @param {string} key @param {string} fallback @returns {string} */
181
+ _message(key, fallback) {
182
+ const message = this.msg(key);
183
+ return message === key ? fallback : message;
184
+ }
185
+ }
186
+
187
+ /** Permission-value change. @event Permission#change @type {CustomEvent<PermissionChangeDetail>} */
188
+
189
+ /**
190
+ * @param {PermissionGroup} item
191
+ * @param {string|((item: PermissionGroup) => unknown)} reader
192
+ * @returns {unknown}
193
+ */
194
+ function read(item, reader) {
195
+ if (typeof reader === 'function') return reader(item);
196
+ if (item === null || typeof item !== 'object') return item;
197
+ return item[reader];
198
+ }
@@ -0,0 +1,34 @@
1
+ import { Field } from '../field/field.js';
2
+ import { Rating } from './rating.js';
3
+
4
+ /**
5
+ * Registers the Rating-backed `rating` field type.
6
+ * @returns {void}
7
+ */
8
+ export function registerRatingFieldAdapter() {
9
+ Field.register('rating', (field, options) => {
10
+ const rating = field.own(new Rating(null, {
11
+ label: options.label ? String(options.label) : undefined,
12
+ ...(options.props ?? {})
13
+ }));
14
+ let setting = false;
15
+ rating.on('change', (event) => {
16
+ if (!setting) field.emit('change', { value: event.detail.value });
17
+ });
18
+ return {
19
+ el: rating.el,
20
+ // Unrated reads as null rather than 0, so Form's required check treats it as empty.
21
+ get: () => rating.get() || null,
22
+ set: (value) => {
23
+ setting = true;
24
+ try {
25
+ rating.set(Number(value) || 0, { silent: true });
26
+ } finally {
27
+ setting = false;
28
+ }
29
+ },
30
+ focus: () => rating.focus(),
31
+ setDisabled: (value) => (value ? rating.disable() : rating.enable())
32
+ };
33
+ });
34
+ }