@alepha/ui 0.16.1 → 0.17.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 (218) hide show
  1. package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CF_qOO3u.js} +20 -19
  2. package/dist/admin/AdminApiKeys-CF_qOO3u.js.map +1 -0
  3. package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BQno3hZG.js} +7 -7
  4. package/dist/admin/AdminAudits-BQno3hZG.js.map +1 -0
  5. package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-kvuUaASF.js} +3 -4
  6. package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-kvuUaASF.js.map} +1 -1
  7. package/dist/admin/AdminJobDashboard-CrPxp0W1.js +485 -0
  8. package/dist/admin/AdminJobDashboard-CrPxp0W1.js.map +1 -0
  9. package/dist/admin/AdminJobExecutions-D-b4Zt7W.js +678 -0
  10. package/dist/admin/AdminJobExecutions-D-b4Zt7W.js.map +1 -0
  11. package/dist/admin/AdminJobRegistry-CNX5cpDx.js +301 -0
  12. package/dist/admin/AdminJobRegistry-CNX5cpDx.js.map +1 -0
  13. package/dist/admin/{AdminLayout-BqZiXx4H.js → AdminLayout-e-ZP5nWw.js} +6 -9
  14. package/dist/admin/AdminLayout-e-ZP5nWw.js.map +1 -0
  15. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-DeHJFf6W.js} +3 -4
  16. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-DeHJFf6W.js.map} +1 -1
  17. package/dist/admin/AdminParameters-iQE8o7a7.js +774 -0
  18. package/dist/admin/AdminParameters-iQE8o7a7.js.map +1 -0
  19. package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-oKJCbd7w.js} +5 -6
  20. package/dist/admin/AdminSessions-oKJCbd7w.js.map +1 -0
  21. package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-BNCEle_E.js} +6 -7
  22. package/dist/admin/AdminUserAudits-BNCEle_E.js.map +1 -0
  23. package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CgqeFwCt.js} +6 -6
  24. package/dist/admin/AdminUserCreate-CgqeFwCt.js.map +1 -0
  25. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-DDe1A1GP.js} +30 -28
  26. package/dist/admin/AdminUserDetails-DDe1A1GP.js.map +1 -0
  27. package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-HAlobhWf.js} +20 -19
  28. package/dist/admin/AdminUserLayout-HAlobhWf.js.map +1 -0
  29. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Bq1LnVLf.js} +5 -6
  30. package/dist/admin/AdminUserSessions-Bq1LnVLf.js.map +1 -0
  31. package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-BRsBZoxV.js} +10 -9
  32. package/dist/admin/AdminUserSettings-BRsBZoxV.js.map +1 -0
  33. package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-D71kIOSn.js} +6 -7
  34. package/dist/admin/AdminUsers-D71kIOSn.js.map +1 -0
  35. package/dist/admin/index.d.ts +21 -85
  36. package/dist/admin/index.d.ts.map +1 -1
  37. package/dist/admin/index.js +66 -88
  38. package/dist/admin/index.js.map +1 -1
  39. package/dist/auth/{AuthLayout-Dj5K4SIN.js → AuthLayout-CdJcrPs4.js} +2 -3
  40. package/dist/auth/{AuthLayout-Dj5K4SIN.js.map → AuthLayout-CdJcrPs4.js.map} +1 -1
  41. package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
  42. package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
  43. package/dist/auth/{Login-BBqTosqZ.js → Login-BS_FYTy0.js} +19 -13
  44. package/dist/auth/Login-BS_FYTy0.js.map +1 -0
  45. package/dist/auth/{Profile-Bxj8Nwom.js → Profile-CjDsW378.js} +17 -12
  46. package/dist/auth/Profile-CjDsW378.js.map +1 -0
  47. package/dist/auth/{Register-Ce675Crg.js → Register-C5eqzAaD.js} +27 -17
  48. package/dist/auth/Register-C5eqzAaD.js.map +1 -0
  49. package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-XifinVao.js} +17 -10
  50. package/dist/auth/ResetPassword-XifinVao.js.map +1 -0
  51. package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DTgbeJOO.js} +9 -6
  52. package/dist/auth/VerifyEmail-DTgbeJOO.js.map +1 -0
  53. package/dist/auth/index.d.ts +18 -14
  54. package/dist/auth/index.d.ts.map +1 -1
  55. package/dist/auth/index.js +19 -18
  56. package/dist/auth/index.js.map +1 -1
  57. package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
  58. package/dist/core/index.d.ts +182 -92
  59. package/dist/core/index.d.ts.map +1 -1
  60. package/dist/core/index.js +789 -476
  61. package/dist/core/index.js.map +1 -1
  62. package/dist/demo/DemoDataTable-lnBKWBf8.js +362 -0
  63. package/dist/demo/DemoDataTable-lnBKWBf8.js.map +1 -0
  64. package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-CUMZsYaH.js} +6 -6
  65. package/dist/demo/DemoHome-CUMZsYaH.js.map +1 -0
  66. package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-_uokbGaW.js} +18 -19
  67. package/dist/demo/DemoJsonViewer-_uokbGaW.js.map +1 -0
  68. package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-DHVoacE6.js} +4 -5
  69. package/dist/demo/DemoLayout-DHVoacE6.js.map +1 -0
  70. package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DjJ9314c.js} +27 -24
  71. package/dist/demo/DemoLogin-DjJ9314c.js.map +1 -0
  72. package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DzkJ5M83.js} +39 -32
  73. package/dist/demo/DemoRegister-DzkJ5M83.js.map +1 -0
  74. package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-DWh4_BpQ.js} +30 -26
  75. package/dist/demo/DemoResetPassword-DWh4_BpQ.js.map +1 -0
  76. package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-C1csnGhX.js} +4 -5
  77. package/dist/demo/DemoSidebar-C1csnGhX.js.map +1 -0
  78. package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-CWz6fJrJ.js} +4 -5
  79. package/dist/demo/DemoTypeForm-CWz6fJrJ.js.map +1 -0
  80. package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-DbU_tCj8.js} +16 -16
  81. package/dist/demo/DemoVerifyEmail-DbU_tCj8.js.map +1 -0
  82. package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-Ch1m3Uzl.js} +2 -4
  83. package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-Ch1m3Uzl.js.map} +1 -1
  84. package/dist/demo/{Showcase-CQrMWars.js → Showcase-BzoXNlCn.js} +11 -13
  85. package/dist/demo/Showcase-BzoXNlCn.js.map +1 -0
  86. package/dist/demo/index.d.ts +3 -70
  87. package/dist/demo/index.d.ts.map +1 -1
  88. package/dist/demo/index.js +11 -15
  89. package/dist/demo/index.js.map +1 -1
  90. package/dist/json/index.js +2 -2
  91. package/dist/json/index.js.map +1 -1
  92. package/package.json +11 -5
  93. package/src/admin/AdminRouter.ts +51 -29
  94. package/src/admin/components/AdminLayout.tsx +6 -9
  95. package/src/admin/components/audits/AdminAudits.tsx +5 -5
  96. package/src/admin/components/jobs/AdminJobDashboard.tsx +455 -0
  97. package/src/admin/components/jobs/AdminJobExecutions.tsx +693 -0
  98. package/src/admin/components/jobs/AdminJobRegistry.tsx +325 -0
  99. package/src/admin/components/keys/AdminApiKeys.tsx +28 -31
  100. package/src/admin/components/parameters/AdminParameters.tsx +156 -78
  101. package/src/admin/components/parameters/ParameterDetails.tsx +173 -108
  102. package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
  103. package/src/admin/components/parameters/ParameterHistory.tsx +22 -35
  104. package/src/admin/components/parameters/ParameterTree.tsx +283 -109
  105. package/src/admin/components/parameters/types.ts +3 -3
  106. package/src/admin/components/sessions/AdminSessions.tsx +3 -3
  107. package/src/admin/components/shared/AdminResourceHeader.tsx +20 -16
  108. package/src/admin/components/users/AdminUserAudits.tsx +5 -5
  109. package/src/admin/components/users/AdminUserCreate.tsx +3 -3
  110. package/src/admin/components/users/AdminUserDetails.tsx +51 -53
  111. package/src/admin/components/users/AdminUserLayout.tsx +7 -7
  112. package/src/admin/components/users/AdminUserSessions.tsx +3 -3
  113. package/src/admin/components/users/AdminUserSettings.tsx +9 -9
  114. package/src/admin/components/users/AdminUsers.tsx +5 -5
  115. package/src/admin/components/verifications/AdminVerifications.tsx +3 -3
  116. package/src/admin/index.ts +0 -24
  117. package/src/admin/primitives/$uiAdmin.ts +2 -2
  118. package/src/auth/AuthRouter.ts +1 -0
  119. package/src/auth/components/Login.tsx +13 -13
  120. package/src/auth/components/Profile.tsx +17 -26
  121. package/src/auth/components/Register.tsx +21 -31
  122. package/src/auth/components/ResetPassword.tsx +13 -22
  123. package/src/auth/components/VerifyEmail.tsx +5 -5
  124. package/src/auth/components/buttons/UserButton.tsx +14 -4
  125. package/src/core/components/buttons/ActionButton.tsx +13 -17
  126. package/src/core/components/buttons/DarkModeButton.tsx +8 -4
  127. package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
  128. package/src/core/components/data/ErrorViewer.tsx +15 -15
  129. package/src/core/components/dialogs/AlertDialog.tsx +3 -3
  130. package/src/core/components/dialogs/ConfirmDialog.tsx +3 -3
  131. package/src/core/components/dialogs/PromptDialog.tsx +3 -3
  132. package/src/core/components/form/Control.tsx +19 -32
  133. package/src/core/components/form/ControlArray.tsx +206 -96
  134. package/src/core/components/form/ControlObject.tsx +3 -3
  135. package/src/core/components/form/ControlQueryBuilder.tsx +20 -22
  136. package/src/core/components/form/ControlSelect.tsx +4 -0
  137. package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
  138. package/src/core/components/form/TypeForm.tsx +7 -0
  139. package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
  140. package/src/core/components/layout/Breadcrumb.tsx +91 -0
  141. package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
  142. package/src/core/components/layout/Omnibar.tsx +2 -1
  143. package/src/core/components/layout/Sidebar.tsx +63 -19
  144. package/src/core/components/table/ColumnPicker.tsx +47 -31
  145. package/src/core/components/table/DataTable.tsx +277 -201
  146. package/src/core/components/table/DataTableFilters.tsx +8 -0
  147. package/src/core/components/table/DataTableToolbar.tsx +98 -5
  148. package/src/core/components/table/FilterPicker.tsx +28 -26
  149. package/src/core/components/table/types.ts +52 -37
  150. package/src/core/components/table/useTableSelection.ts +83 -0
  151. package/src/core/constants/ui.ts +1 -1
  152. package/src/core/helpers/renderIcon.tsx +5 -2
  153. package/src/core/index.ts +9 -5
  154. package/src/core/styles.css +8 -7
  155. package/src/core/utils/parseInput.ts +1 -0
  156. package/src/core/utils/string.ts +28 -4
  157. package/src/demo/components/DemoHome.tsx +5 -5
  158. package/src/demo/components/DemoLayout.tsx +6 -2
  159. package/src/demo/components/core/DemoDataTable.tsx +209 -5
  160. package/src/demo/components/json/DemoJsonViewer.tsx +1 -1
  161. package/src/demo/components/shared/MacWindow.tsx +7 -7
  162. package/src/demo/components/shared/Showcase.tsx +3 -3
  163. package/src/demo/index.ts +0 -11
  164. package/src/json/components/JsonViewer.tsx +3 -3
  165. package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
  166. package/dist/admin/AdminApiKeys-GMORg-1l.js.map +0 -1
  167. package/dist/admin/AdminAudits-8SM96viT.js +0 -3
  168. package/dist/admin/AdminAudits-pkWrjq1Z.js.map +0 -1
  169. package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
  170. package/dist/admin/AdminJobs-B-q9iGO3.js +0 -697
  171. package/dist/admin/AdminJobs-B-q9iGO3.js.map +0 -1
  172. package/dist/admin/AdminJobs-CED1syCn.js +0 -3
  173. package/dist/admin/AdminLayout-BqZiXx4H.js.map +0 -1
  174. package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
  175. package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
  176. package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
  177. package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
  178. package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
  179. package/dist/admin/AdminSessions-DzIOxM3b.js.map +0 -1
  180. package/dist/admin/AdminUserAudits-CiUPN2BC.js.map +0 -1
  181. package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
  182. package/dist/admin/AdminUserCreate-BwQKr4xE.js.map +0 -1
  183. package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
  184. package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
  185. package/dist/admin/AdminUserDetails-uqtC5aJ1.js.map +0 -1
  186. package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
  187. package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
  188. package/dist/admin/AdminUserSessions-DAE8Nf1F.js.map +0 -1
  189. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
  190. package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
  191. package/dist/admin/AdminUserSettings-EbahaV2a.js.map +0 -1
  192. package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
  193. package/dist/admin/AdminUsers-Dcjh0KNW.js.map +0 -1
  194. package/dist/auth/Login-BBqTosqZ.js.map +0 -1
  195. package/dist/auth/Login-CoU63mMR.js +0 -4
  196. package/dist/auth/Profile-Bxj8Nwom.js.map +0 -1
  197. package/dist/auth/Register-BV_oa_AK.js +0 -4
  198. package/dist/auth/Register-Ce675Crg.js.map +0 -1
  199. package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
  200. package/dist/auth/ResetPassword-DWdt7c40.js.map +0 -1
  201. package/dist/auth/VerifyEmail-CI4JwByV.js.map +0 -1
  202. package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
  203. package/dist/demo/DemoDataTable-CguplbR7.js +0 -150
  204. package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
  205. package/dist/demo/DemoHome-Cce2bWmg.js.map +0 -1
  206. package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
  207. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
  208. package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
  209. package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
  210. package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
  211. package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
  212. package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
  213. package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
  214. package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
  215. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
  216. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
  217. package/dist/demo/Showcase-CQrMWars.js.map +0 -1
  218. package/src/admin/components/jobs/AdminJobs.tsx +0 -772
@@ -3,15 +3,14 @@ import {
3
3
  Fieldset,
4
4
  Flex,
5
5
  Grid,
6
- Stack,
7
6
  Text,
8
7
  UnstyledButton,
9
8
  } from "@mantine/core";
10
9
  import { IconGripVertical, IconPlus, IconTrash } from "@tabler/icons-react";
11
10
  import type { TObject, TSchema } from "alepha";
12
- import { useEvents } from "alepha/react";
11
+ import { useAlepha } from "alepha/react";
13
12
  import type { BaseInputField } from "alepha/react/form";
14
- import { useRef, useState } from "react";
13
+ import { useCallback, useEffect, useRef, useState } from "react";
15
14
  import { ui } from "../../constants/ui.ts";
16
15
  import {
17
16
  type GenericControlProps,
@@ -19,6 +18,14 @@ import {
19
18
  } from "../../utils/parseInput.ts";
20
19
  import Control, { type ControlProps } from "./Control.tsx";
21
20
 
21
+ /**
22
+ * Represents an array item with a stable key for React reconciliation.
23
+ */
24
+ interface ArrayItem {
25
+ key: number;
26
+ value: any;
27
+ }
28
+
22
29
  export interface ControlArrayProps extends GenericControlProps {
23
30
  /**
24
31
  * Minimum number of items allowed.
@@ -70,6 +77,169 @@ export interface ControlArrayProps extends GenericControlProps {
70
77
  sortable?: boolean;
71
78
  }
72
79
 
80
+ /**
81
+ * Custom hook to sync array items with form state.
82
+ * Uses form events as the source of truth, eliminating dual-state issues.
83
+ */
84
+ const useArrayItems = (
85
+ input: BaseInputField | undefined,
86
+ ): {
87
+ items: ArrayItem[];
88
+ setItems: (items: ArrayItem[]) => void;
89
+ nextKey: () => number;
90
+ } => {
91
+ const alepha = useAlepha();
92
+ const keyCounter = useRef(0);
93
+
94
+ // Initialize from defaultValue
95
+ const [items, setItemsState] = useState<ArrayItem[]>(() => {
96
+ const defaultValue = input?.props?.defaultValue;
97
+ if (Array.isArray(defaultValue)) {
98
+ return defaultValue.map((value) => ({
99
+ key: keyCounter.current++,
100
+ value,
101
+ }));
102
+ }
103
+ return [];
104
+ });
105
+
106
+ // Sync form value to local state
107
+ const syncFromFormValue = useCallback((formValue: any[] | undefined) => {
108
+ if (!Array.isArray(formValue)) {
109
+ setItemsState([]);
110
+ return;
111
+ }
112
+
113
+ // Preserve keys for existing items where possible
114
+ setItemsState((prevItems) => {
115
+ // If lengths match and values are same references, keep existing keys
116
+ if (prevItems.length === formValue.length) {
117
+ const allSame = prevItems.every(
118
+ (item, i) => item.value === formValue[i],
119
+ );
120
+ if (allSame) return prevItems;
121
+ }
122
+
123
+ // Otherwise, create new items with fresh keys
124
+ keyCounter.current = 0;
125
+ return formValue.map((value) => ({
126
+ key: keyCounter.current++,
127
+ value,
128
+ }));
129
+ });
130
+ }, []);
131
+
132
+ // Listen for form changes and reset events
133
+ useEffect(() => {
134
+ if (!input?.form) return;
135
+
136
+ const formId = input.form.id;
137
+ const fieldPath = input.path;
138
+
139
+ const listeners = [
140
+ // Handle form reset
141
+ alepha.events.on("form:reset", (event) => {
142
+ if (event.id === formId) {
143
+ const defaultValue = input.props?.defaultValue;
144
+ keyCounter.current = 0;
145
+ if (Array.isArray(defaultValue)) {
146
+ setItemsState(
147
+ defaultValue.map((value) => ({
148
+ key: keyCounter.current++,
149
+ value,
150
+ })),
151
+ );
152
+ } else {
153
+ setItemsState([]);
154
+ }
155
+ }
156
+ }),
157
+
158
+ // Handle external value changes (e.g., programmatic updates)
159
+ alepha.events.on("form:change", (event) => {
160
+ if (event.id === formId && event.path === fieldPath) {
161
+ // Value was changed externally, sync our state
162
+ syncFromFormValue(event.value);
163
+ }
164
+ }),
165
+ ];
166
+
167
+ return () => {
168
+ for (const unsub of listeners) {
169
+ unsub();
170
+ }
171
+ };
172
+ }, [alepha, input, syncFromFormValue]);
173
+
174
+ // Update form when items change
175
+ const setItems = useCallback(
176
+ (newItems: ArrayItem[]) => {
177
+ setItemsState(newItems);
178
+ // Update form value - this will trigger form:change but we'll detect it's from us
179
+ input?.set(newItems.map((item) => item.value));
180
+ },
181
+ [input],
182
+ );
183
+
184
+ const nextKey = useCallback(() => keyCounter.current++, []);
185
+
186
+ return { items, setItems, nextKey };
187
+ };
188
+
189
+ /**
190
+ * Creates a proper InputField for an array item that integrates with the form system.
191
+ * Uses array index for test IDs to ensure predictable, testable element identifiers.
192
+ */
193
+ const createArrayItemInput = (
194
+ parentInput: BaseInputField,
195
+ itemSchema: TSchema,
196
+ index: number,
197
+ _itemKey: number,
198
+ value: any,
199
+ onValueChange: (value: any) => void,
200
+ ): BaseInputField => {
201
+ return {
202
+ schema: itemSchema,
203
+ path: `${parentInput.path}/${index}`,
204
+ required: false,
205
+ form: parentInput.form,
206
+ props: {
207
+ id: `${parentInput.props.id}-${index}`,
208
+ name: `${parentInput.props.name}[${index}]`,
209
+ defaultValue: value,
210
+ },
211
+ set: onValueChange,
212
+ };
213
+ };
214
+
215
+ /**
216
+ * Creates a proper InputField for a nested object field within an array item.
217
+ * Uses array index for test IDs to ensure predictable, testable element identifiers.
218
+ */
219
+ const createArrayItemFieldInput = (
220
+ parentInput: BaseInputField,
221
+ itemSchema: TObject,
222
+ fieldName: string,
223
+ index: number,
224
+ _itemKey: number,
225
+ itemValue: any,
226
+ onFieldChange: (field: string, value: any) => void,
227
+ ): BaseInputField => {
228
+ const fieldSchema = itemSchema.properties[fieldName];
229
+ return {
230
+ schema: fieldSchema,
231
+ path: `${parentInput.path}/${index}/${fieldName}`,
232
+ required: itemSchema.required?.includes(fieldName) ?? false,
233
+ form: parentInput.form,
234
+ props: {
235
+ id: `${parentInput.props.id}-${index}-${fieldName}`,
236
+ name: `${parentInput.props.name}[${index}].${fieldName}`,
237
+ defaultValue: itemValue?.[fieldName],
238
+ },
239
+ set: (value: any) => onFieldChange(fieldName, value),
240
+ };
241
+ };
242
+
73
243
  /**
74
244
  * ControlArray component for editing arrays of schema items.
75
245
  *
@@ -79,6 +249,7 @@ export interface ControlArrayProps extends GenericControlProps {
79
249
  * - Supports arrays of primitives
80
250
  * - Grid layout for object items
81
251
  * - Min/max constraints
252
+ * - Syncs with form state (handles external updates and resets)
82
253
  *
83
254
  * @example
84
255
  * ```tsx
@@ -102,42 +273,7 @@ export interface ControlArrayProps extends GenericControlProps {
102
273
  */
103
274
  const ControlArray = (props: ControlArrayProps) => {
104
275
  const { inputProps } = parseInput(props, {});
105
- const idCounter = useRef(0);
106
-
107
- // Initialize items with unique keys for React
108
- const [items, setItems] = useState<Array<{ key: number; value: any }>>(() => {
109
- const defaultValue = props.input?.props?.defaultValue;
110
- if (Array.isArray(defaultValue)) {
111
- return defaultValue.map((value) => ({
112
- key: idCounter.current++,
113
- value,
114
- }));
115
- }
116
- return [];
117
- });
118
-
119
- // Listen for form reset events
120
- useEvents(
121
- {
122
- "form:reset": (event) => {
123
- if (event.id === props.input?.form?.id) {
124
- const defaultValue = props.input?.props?.defaultValue;
125
- if (Array.isArray(defaultValue)) {
126
- idCounter.current = 0;
127
- setItems(
128
- defaultValue.map((value) => ({
129
- key: idCounter.current++,
130
- value,
131
- })),
132
- );
133
- } else {
134
- setItems([]);
135
- }
136
- }
137
- },
138
- },
139
- [props.input],
140
- );
276
+ const { items, setItems, nextKey } = useArrayItems(props.input);
141
277
 
142
278
  if (!props.input?.props) {
143
279
  return null;
@@ -148,14 +284,10 @@ const ControlArray = (props: ControlArrayProps) => {
148
284
  return null;
149
285
  }
150
286
 
151
- const itemSchema = (schema as any).items as TSchema;
287
+ const itemSchema = (schema as { items: TSchema }).items;
152
288
  const isObjectItem = itemSchema && "properties" in itemSchema;
153
289
  const { min = 0, max = Number.POSITIVE_INFINITY, columns = 1 } = props;
154
290
 
155
- const updateFormValue = (newItems: Array<{ key: number; value: any }>) => {
156
- props.input.set(newItems.map((item) => item.value));
157
- };
158
-
159
291
  const handleAdd = () => {
160
292
  if (items.length >= max) return;
161
293
 
@@ -174,23 +306,18 @@ const ControlArray = (props: ControlArrayProps) => {
174
306
  newValue = "";
175
307
  }
176
308
 
177
- const newItems = [...items, { key: idCounter.current++, value: newValue }];
178
- setItems(newItems);
179
- updateFormValue(newItems);
309
+ setItems([...items, { key: nextKey(), value: newValue }]);
180
310
  };
181
311
 
182
312
  const handleRemove = (index: number) => {
183
313
  if (items.length <= min) return;
184
- const newItems = items.filter((_, i) => i !== index);
185
- setItems(newItems);
186
- updateFormValue(newItems);
314
+ setItems(items.filter((_, i) => i !== index));
187
315
  };
188
316
 
189
317
  const handleItemChange = (index: number, value: any) => {
190
318
  const newItems = [...items];
191
319
  newItems[index] = { ...newItems[index], value };
192
320
  setItems(newItems);
193
- updateFormValue(newItems);
194
321
  };
195
322
 
196
323
  const handleFieldChange = (index: number, field: string, value: any) => {
@@ -200,16 +327,16 @@ const ControlArray = (props: ControlArrayProps) => {
200
327
  value: { ...newItems[index].value, [field]: value },
201
328
  };
202
329
  setItems(newItems);
203
- updateFormValue(newItems);
204
330
  };
205
331
 
206
332
  const colSpan = 12 / columns;
207
- const fieldNames = isObjectItem
208
- ? Object.keys((itemSchema as TObject).properties)
333
+ const objectItemSchema = isObjectItem ? (itemSchema as TObject) : null;
334
+ const fieldNames = objectItemSchema
335
+ ? Object.keys(objectItemSchema.properties)
209
336
  : [];
210
337
 
211
338
  const renderItems = () => (
212
- <Stack gap="sm">
339
+ <Flex direction="column" gap="sm">
213
340
  {items.map((item, index) => (
214
341
  <Flex
215
342
  key={item.key}
@@ -229,34 +356,23 @@ const ControlArray = (props: ControlArrayProps) => {
229
356
  </ActionIcon>
230
357
  )}
231
358
 
232
- {isObjectItem ? (
359
+ {objectItemSchema ? (
233
360
  <Grid style={{ flex: 1 }} gutter="sm">
234
361
  {fieldNames.map((fieldName) => {
235
- const fieldSchema = (itemSchema as TObject).properties[
236
- fieldName
237
- ];
238
362
  const fieldControlProps = props.controlProps?.[fieldName] ?? {};
239
-
240
- // Create a virtual InputField for the nested property
241
- const virtualInput: BaseInputField = {
242
- schema: fieldSchema,
243
- props: {
244
- id: `${props.input.props.id}-${item.key}-${fieldName}`,
245
- name: `${props.input.props.name}[${index}].${fieldName}`,
246
- defaultValue: item.value?.[fieldName],
247
- },
248
- path: `${props.input.path}/${index}/${fieldName}`,
249
- required:
250
- (itemSchema as TObject).required?.includes(fieldName) ??
251
- false,
252
- form: props.input.form,
253
- set: (value: any) =>
254
- handleFieldChange(index, fieldName, value),
255
- };
363
+ const fieldInput = createArrayItemFieldInput(
364
+ props.input,
365
+ objectItemSchema,
366
+ fieldName,
367
+ index,
368
+ item.key,
369
+ item.value,
370
+ (field, value) => handleFieldChange(index, field, value),
371
+ );
256
372
 
257
373
  return (
258
374
  <Grid.Col key={fieldName} span={colSpan}>
259
- <Control input={virtualInput} {...fieldControlProps} />
375
+ <Control input={fieldInput} {...fieldControlProps} />
260
376
  </Grid.Col>
261
377
  );
262
378
  })}
@@ -264,20 +380,14 @@ const ControlArray = (props: ControlArrayProps) => {
264
380
  ) : (
265
381
  <Flex style={{ flex: 1 }}>
266
382
  <Control
267
- input={
268
- {
269
- schema: itemSchema,
270
- props: {
271
- id: `${props.input.props.id}-${item.key}`,
272
- name: `${props.input.props.name}[${index}]`,
273
- defaultValue: item.value,
274
- },
275
- path: `${props.input.path}/${index}`,
276
- required: false,
277
- form: props.input.form,
278
- set: (value: any) => handleItemChange(index, value),
279
- } as BaseInputField
280
- }
383
+ input={createArrayItemInput(
384
+ props.input,
385
+ itemSchema,
386
+ index,
387
+ item.key,
388
+ item.value,
389
+ (value) => handleItemChange(index, value),
390
+ )}
281
391
  {...props.itemControlProps}
282
392
  />
283
393
  </Flex>
@@ -329,12 +439,12 @@ const ControlArray = (props: ControlArrayProps) => {
329
439
  <IconPlus size={14} />
330
440
  {props.addLabel ?? "Add"}
331
441
  </UnstyledButton>
332
- </Stack>
442
+ </Flex>
333
443
  );
334
444
 
335
445
  if (props.variant === "plain") {
336
446
  return (
337
- <Stack gap="xs">
447
+ <Flex direction="column" gap="xs">
338
448
  {inputProps.label && (
339
449
  <Text size="sm" fw={500}>
340
450
  {inputProps.label}
@@ -351,13 +461,13 @@ const ControlArray = (props: ControlArrayProps) => {
351
461
  {inputProps.error}
352
462
  </Text>
353
463
  )}
354
- </Stack>
464
+ </Flex>
355
465
  );
356
466
  }
357
467
 
358
468
  return (
359
469
  <Fieldset legend={inputProps.label}>
360
- <Stack gap="xs">
470
+ <Flex direction="column" gap="xs">
361
471
  {inputProps.description && (
362
472
  <Text size="sm" c="dimmed">
363
473
  {inputProps.description}
@@ -369,7 +479,7 @@ const ControlArray = (props: ControlArrayProps) => {
369
479
  {inputProps.error}
370
480
  </Text>
371
481
  )}
372
- </Stack>
482
+ </Flex>
373
483
  </Fieldset>
374
484
  );
375
485
  };
@@ -1,4 +1,4 @@
1
- import { Fieldset, Grid, Stack, Text } from "@mantine/core";
1
+ import { Fieldset, Flex, Grid, Text } from "@mantine/core";
2
2
  import type { TObject } from "alepha";
3
3
  import type { BaseInputField, ObjectInputField } from "alepha/react/form";
4
4
  import {
@@ -107,7 +107,7 @@ const ControlObject = (props: ControlObjectProps) => {
107
107
 
108
108
  return (
109
109
  <Fieldset legend={inputProps.label}>
110
- <Stack gap="xs">
110
+ <Flex direction="column" gap="xs">
111
111
  {inputProps.description && (
112
112
  <Text size="sm" c="dimmed">
113
113
  {inputProps.description}
@@ -119,7 +119,7 @@ const ControlObject = (props: ControlObjectProps) => {
119
119
  {inputProps.error}
120
120
  </Text>
121
121
  )}
122
- </Stack>
122
+ </Flex>
123
123
  </Fieldset>
124
124
  );
125
125
  };
@@ -3,9 +3,7 @@ import {
3
3
  Badge,
4
4
  Divider,
5
5
  Flex,
6
- Group,
7
6
  Popover,
8
- Stack,
9
7
  Text,
10
8
  TextInput,
11
9
  type TextInputProps,
@@ -165,7 +163,7 @@ interface QueryHelpProps {
165
163
 
166
164
  function QueryHelp({ fields, onInsert }: QueryHelpProps) {
167
165
  return (
168
- <Group
166
+ <Flex
169
167
  gap="md"
170
168
  align="flex-start"
171
169
  wrap="nowrap"
@@ -174,15 +172,15 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
174
172
  bdrs={"sm"}
175
173
  >
176
174
  {/* Left Column: Operators */}
177
- <Stack gap="md" style={{ flex: 1 }}>
175
+ <Flex direction="column" gap="md" style={{ flex: 1 }}>
178
176
  {/* Available Operators */}
179
- <Stack gap="xs">
177
+ <Flex direction="column" gap="xs">
180
178
  <Text size="sm" fw={600}>
181
179
  Operators
182
180
  </Text>
183
- <Stack gap={4}>
181
+ <Flex direction="column" gap={4}>
184
182
  {Object.entries(OPERATOR_INFO).map(([key, info]) => (
185
- <Group key={key} gap="xs" wrap="nowrap">
183
+ <Flex key={key} gap="xs" wrap="nowrap">
186
184
  <ActionButton
187
185
  px={"xs"}
188
186
  size={"xs"}
@@ -197,20 +195,20 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
197
195
  <Text size="xs" c="dimmed" style={{ flex: 1 }}>
198
196
  {info.label}
199
197
  </Text>
200
- </Group>
198
+ </Flex>
201
199
  ))}
202
- </Stack>
203
- </Stack>
200
+ </Flex>
201
+ </Flex>
204
202
 
205
203
  <Divider />
206
204
 
207
205
  {/* Logic Operators */}
208
- <Stack gap="xs">
206
+ <Flex direction="column" gap="xs">
209
207
  <Text size="sm" fw={600}>
210
208
  Logic
211
209
  </Text>
212
- <Stack gap={4}>
213
- <Group gap="xs" wrap="nowrap">
210
+ <Flex direction="column" gap={4}>
211
+ <Flex gap="xs" wrap="nowrap">
214
212
  <ActionButton
215
213
  px={"xs"}
216
214
  size={"xs"}
@@ -225,8 +223,8 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
225
223
  <Text size="xs" c="dimmed">
226
224
  AND
227
225
  </Text>
228
- </Group>
229
- <Group gap="xs" wrap="nowrap">
226
+ </Flex>
227
+ <Flex gap="xs" wrap="nowrap">
230
228
  <ActionButton
231
229
  px={"xs"}
232
230
  size={"xs"}
@@ -241,10 +239,10 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
241
239
  <Text size="xs" c="dimmed">
242
240
  OR
243
241
  </Text>
244
- </Group>
245
- </Stack>
246
- </Stack>
247
- </Stack>
242
+ </Flex>
243
+ </Flex>
244
+ </Flex>
245
+ </Flex>
248
246
 
249
247
  {/* Divider */}
250
248
  {fields.length > 0 && <Divider orientation="vertical" />}
@@ -283,7 +281,7 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
283
281
  {field.description || field.type}
284
282
  </Text>
285
283
  {field.enum && (
286
- <Group gap={0} wrap="wrap">
284
+ <Flex gap={0} wrap="wrap">
287
285
  {field.enum.map((enumValue) => (
288
286
  <ActionButton
289
287
  px={"xs"}
@@ -295,7 +293,7 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
295
293
  {enumValue}
296
294
  </ActionButton>
297
295
  ))}
298
- </Group>
296
+ </Flex>
299
297
  )}
300
298
  </Flex>
301
299
  <Badge size="xs" variant="light" style={{ flexShrink: 0 }}>
@@ -306,7 +304,7 @@ function QueryHelp({ fields, onInsert }: QueryHelpProps) {
306
304
  </Flex>
307
305
  </Flex>
308
306
  )}
309
- </Group>
307
+ </Flex>
310
308
  );
311
309
  }
312
310
 
@@ -119,6 +119,7 @@ const ControlSelect = (props: ControlSelectProps) => {
119
119
  return (
120
120
  <Autocomplete
121
121
  {...inputProps}
122
+ size={props.size}
122
123
  id={id}
123
124
  leftSection={icon}
124
125
  data={data}
@@ -134,6 +135,7 @@ const ControlSelect = (props: ControlSelectProps) => {
134
135
  return (
135
136
  <TagsInput
136
137
  {...inputProps}
138
+ size={props.size}
137
139
  id={id}
138
140
  leftSection={icon}
139
141
  defaultValue={
@@ -163,6 +165,7 @@ const ControlSelect = (props: ControlSelectProps) => {
163
165
  return (
164
166
  <MultiSelect
165
167
  {...inputProps}
168
+ size={props.size}
166
169
  id={id}
167
170
  leftSection={icon}
168
171
  data={data}
@@ -186,6 +189,7 @@ const ControlSelect = (props: ControlSelectProps) => {
186
189
  return (
187
190
  <Select
188
191
  {...inputProps}
192
+ size={props.size}
189
193
  id={id}
190
194
  leftSection={icon}
191
195
  data={data}