@alepha/ui 0.13.6 → 0.13.7

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 (157) hide show
  1. package/dist/admin/AdminAudits-CwvH8e8c.js +215 -0
  2. package/dist/admin/AdminAudits-CwvH8e8c.js.map +1 -0
  3. package/dist/admin/AdminAudits-Dv8Vk_6r.js +3 -0
  4. package/dist/admin/AdminFiles-5CPA3lQk.js +3 -0
  5. package/dist/admin/{AdminFiles-B_jfB_Py.js → AdminFiles-C_w1tb_x.js} +4 -3
  6. package/dist/admin/AdminFiles-C_w1tb_x.js.map +1 -0
  7. package/dist/admin/AdminLayout-BnSmtA4x.js +3 -0
  8. package/dist/admin/AdminLayout-XiSivwWH.js +39 -0
  9. package/dist/admin/AdminLayout-XiSivwWH.js.map +1 -0
  10. package/dist/admin/AdminNotifications-DLjmZWtf.js +3 -0
  11. package/dist/admin/{AdminNotifications-BFEjqpqx.js → AdminNotifications-DuYy74AN.js} +3 -3
  12. package/dist/admin/AdminNotifications-DuYy74AN.js.map +1 -0
  13. package/dist/admin/AdminParameters-DYg48Jwe.js +3 -0
  14. package/dist/admin/AdminParameters-YagqWTG3.js +575 -0
  15. package/dist/admin/AdminParameters-YagqWTG3.js.map +1 -0
  16. package/dist/admin/{AdminSessions-D7DESfWK.js → AdminSessions-BCjgJ-93.js} +4 -4
  17. package/dist/admin/AdminSessions-BCjgJ-93.js.map +1 -0
  18. package/dist/admin/AdminSessions-DEh2uN-4.js +3 -0
  19. package/dist/admin/AdminUserAudits-B_PUXCKC.js +177 -0
  20. package/dist/admin/AdminUserAudits-B_PUXCKC.js.map +1 -0
  21. package/dist/admin/AdminUserAudits-D7cTcElL.js +3 -0
  22. package/dist/admin/{AdminUserCreate-Bhxsn92l.js → AdminUserCreate-DzfRbGZ4.js} +4 -4
  23. package/dist/admin/AdminUserCreate-DzfRbGZ4.js.map +1 -0
  24. package/dist/admin/{AdminUserCreate-CYI_xW5T.js → AdminUserCreate-oUA1KDIl.js} +1 -1
  25. package/dist/admin/{AdminUserDetails-C2y1Ig4n.js → AdminUserDetails-DeTrJm-t.js} +5 -5
  26. package/dist/admin/AdminUserDetails-DeTrJm-t.js.map +1 -0
  27. package/dist/admin/{AdminUserDetails-Cmzx9HxH.js → AdminUserDetails-y1H5DW8Y.js} +1 -1
  28. package/dist/admin/{AdminUserLayout-sW6cjZL0.js → AdminUserLayout-CsfrrZkD.js} +4 -7
  29. package/dist/admin/AdminUserLayout-CsfrrZkD.js.map +1 -0
  30. package/dist/admin/{AdminUserLayout-DGSf612u.js → AdminUserLayout-Dejnz13m.js} +1 -1
  31. package/dist/admin/AdminUserSessions-Bbhcpz4k.js +3 -0
  32. package/dist/admin/{AdminUserSessions-CvN15wPe.js → AdminUserSessions-DO9H85O-.js} +4 -4
  33. package/dist/admin/AdminUserSessions-DO9H85O-.js.map +1 -0
  34. package/dist/admin/{AdminUserSettings-DvaaxgcV.js → AdminUserSettings-B3jA8g3p.js} +4 -4
  35. package/dist/admin/AdminUserSettings-B3jA8g3p.js.map +1 -0
  36. package/dist/admin/AdminUserSettings-CE0xpbQc.js +3 -0
  37. package/dist/admin/AdminUsers-CegGZDhW.js +3 -0
  38. package/dist/admin/{AdminUsers-BR3C-jrg.js → AdminUsers-ebbrJBT0.js} +13 -17
  39. package/dist/admin/AdminUsers-ebbrJBT0.js.map +1 -0
  40. package/dist/admin/index.d.ts +2044 -1044
  41. package/dist/admin/index.js +65 -62
  42. package/dist/admin/index.js.map +1 -1
  43. package/dist/auth/AuthLayout-BAZJHzDG.js +23 -0
  44. package/dist/auth/AuthLayout-BAZJHzDG.js.map +1 -0
  45. package/dist/auth/{Login-7HlBjDeV.js → Login-CeNZZjrr.js} +80 -44
  46. package/dist/auth/Login-CeNZZjrr.js.map +1 -0
  47. package/dist/auth/Login-hQcu1nlu.js +4 -0
  48. package/dist/auth/Register-B6HBNVHS.js +4 -0
  49. package/dist/auth/{Register-CuQr3kgi.js → Register-s4ENeyiE.js} +131 -91
  50. package/dist/auth/Register-s4ENeyiE.js.map +1 -0
  51. package/dist/auth/ResetPassword-Cjd-W-Nu.js +3 -0
  52. package/dist/auth/ResetPassword-GLIFkJT7.js +278 -0
  53. package/dist/auth/ResetPassword-GLIFkJT7.js.map +1 -0
  54. package/dist/auth/index.d.ts +471 -426
  55. package/dist/auth/index.js +26 -18
  56. package/dist/auth/index.js.map +1 -1
  57. package/dist/core/index.d.ts +400 -130
  58. package/dist/core/index.js +1751 -1369
  59. package/dist/core/index.js.map +1 -1
  60. package/package.json +15 -11
  61. package/src/admin/AdminRouter.ts +70 -16
  62. package/src/admin/components/AdminLayout.tsx +41 -61
  63. package/src/admin/components/audits/AdminAudits.tsx +240 -0
  64. package/src/admin/components/{AdminFiles.tsx → files/AdminFiles.tsx} +1 -1
  65. package/src/admin/components/{AdminJobs.tsx → jobs/AdminJobs.tsx} +1 -1
  66. package/src/admin/components/parameters/AdminParameters.tsx +137 -0
  67. package/src/admin/components/parameters/ParameterDetails.tsx +228 -0
  68. package/src/admin/components/parameters/ParameterHistory.tsx +146 -0
  69. package/src/admin/components/parameters/ParameterTree.tsx +146 -0
  70. package/src/admin/components/parameters/types.ts +35 -0
  71. package/src/admin/components/{AdminSessions.tsx → sessions/AdminSessions.tsx} +1 -1
  72. package/src/admin/components/users/AdminUserAudits.tsx +183 -0
  73. package/src/admin/components/{AdminUserCreate.tsx → users/AdminUserCreate.tsx} +1 -1
  74. package/src/admin/components/{AdminUserLayout.tsx → users/AdminUserLayout.tsx} +1 -4
  75. package/src/admin/components/{AdminUserSettings.tsx → users/AdminUserSettings.tsx} +1 -1
  76. package/src/admin/components/{AdminUsers.tsx → users/AdminUsers.tsx} +10 -12
  77. package/src/admin/index.ts +24 -16
  78. package/src/auth/AuthRouter.ts +23 -17
  79. package/src/auth/components/AuthLayout.tsx +6 -3
  80. package/src/auth/components/Login.tsx +109 -47
  81. package/src/auth/components/Register.tsx +158 -94
  82. package/src/auth/components/ResetPassword.tsx +51 -5
  83. package/src/auth/components/buttons/UserButton.tsx +2 -0
  84. package/src/core/atoms/alephaThemeAtom.ts +13 -0
  85. package/src/core/atoms/alephaThemeListAtom.ts +10 -0
  86. package/src/core/atoms/themes/default.ts +6 -0
  87. package/src/core/{themes → atoms/themes}/midnight.ts +3 -5
  88. package/src/core/components/buttons/ActionButton.tsx +33 -26
  89. package/src/core/components/buttons/DarkModeButton.tsx +0 -1
  90. package/src/core/components/buttons/ThemeButton.tsx +10 -7
  91. package/src/core/components/buttons/ToggleSidebarButton.tsx +19 -16
  92. package/src/core/components/data/ErrorViewer.tsx +171 -0
  93. package/src/core/components/data/JsonViewer.tsx +147 -138
  94. package/src/core/components/form/Control.tsx +95 -18
  95. package/src/core/components/form/ControlArray.tsx +377 -0
  96. package/src/core/components/form/ControlObject.tsx +127 -0
  97. package/src/core/components/form/TypeForm.tsx +99 -37
  98. package/src/core/components/layout/AdminShell.tsx +14 -1
  99. package/src/core/components/layout/AlephaMantineProvider.tsx +7 -3
  100. package/src/core/components/layout/Omnibar.tsx +1 -1
  101. package/src/core/components/layout/Sidebar.tsx +47 -14
  102. package/src/core/components/table/ColumnPicker.tsx +126 -0
  103. package/src/core/components/table/DataTable.tsx +354 -181
  104. package/src/core/components/table/DataTableFilters.tsx +64 -0
  105. package/src/core/components/table/DataTablePagination.tsx +59 -0
  106. package/src/core/components/table/DataTableToolbar.tsx +126 -0
  107. package/src/core/components/table/FilterPicker.tsx +138 -0
  108. package/src/core/components/table/types.ts +199 -0
  109. package/src/core/helpers/isComponentType.ts +9 -0
  110. package/src/core/helpers/renderIcon.tsx +13 -0
  111. package/src/core/hooks/useTheme.ts +24 -18
  112. package/src/core/index.ts +24 -3
  113. package/src/core/interfaces/AlephaTheme.ts +8 -0
  114. package/src/core/providers/ThemeProvider.ts +44 -62
  115. package/src/core/services/DialogService.tsx +24 -0
  116. package/src/core/utils/parseInput.ts +2 -2
  117. package/styles.css +1 -1
  118. package/dist/admin/AdminFiles-B-0UcHVV.js +0 -3
  119. package/dist/admin/AdminFiles-B_jfB_Py.js.map +0 -1
  120. package/dist/admin/AdminLayout-BMtiXAzS.js +0 -396
  121. package/dist/admin/AdminLayout-BMtiXAzS.js.map +0 -1
  122. package/dist/admin/AdminLayout-BNo3GoHR.js +0 -3
  123. package/dist/admin/AdminNotifications-BFEjqpqx.js.map +0 -1
  124. package/dist/admin/AdminNotifications-DJs2ZjNj.js +0 -3
  125. package/dist/admin/AdminSessions-D7DESfWK.js.map +0 -1
  126. package/dist/admin/AdminSessions-PS2M8iXi.js +0 -3
  127. package/dist/admin/AdminUserCreate-Bhxsn92l.js.map +0 -1
  128. package/dist/admin/AdminUserDetails-C2y1Ig4n.js.map +0 -1
  129. package/dist/admin/AdminUserLayout-sW6cjZL0.js.map +0 -1
  130. package/dist/admin/AdminUserSessions-CvN15wPe.js.map +0 -1
  131. package/dist/admin/AdminUserSessions-D-aOcZgV.js +0 -3
  132. package/dist/admin/AdminUserSettings-CEMhIYrI.js +0 -3
  133. package/dist/admin/AdminUserSettings-DvaaxgcV.js.map +0 -1
  134. package/dist/admin/AdminUsers-BR3C-jrg.js.map +0 -1
  135. package/dist/admin/AdminUsers-CMW9vN09.js +0 -3
  136. package/dist/auth/AuthLayout-CzwUKD9y.js +0 -19
  137. package/dist/auth/AuthLayout-CzwUKD9y.js.map +0 -1
  138. package/dist/auth/Login-7HlBjDeV.js.map +0 -1
  139. package/dist/auth/Login-C-e27DGb.js +0 -4
  140. package/dist/auth/Register-CuQr3kgi.js.map +0 -1
  141. package/dist/auth/Register-DbvXwgbG.js +0 -4
  142. package/dist/auth/ResetPassword-BzU-cdd4.js +0 -243
  143. package/dist/auth/ResetPassword-BzU-cdd4.js.map +0 -1
  144. package/dist/auth/ResetPassword-DSvrdpaA.js +0 -3
  145. package/src/admin/AdminSidebar.ts +0 -31
  146. package/src/admin/components/AdminParameters.tsx +0 -24
  147. package/src/core/themes/aurora.ts +0 -107
  148. package/src/core/themes/crystal.ts +0 -107
  149. package/src/core/themes/default.ts +0 -7
  150. package/src/core/themes/ember.ts +0 -107
  151. package/src/core/themes/index.ts +0 -7
  152. package/src/core/themes/remoraid.ts +0 -278
  153. package/src/core/themes/slate.ts +0 -81
  154. /package/src/admin/components/{AdminNotifications.tsx → notifications/AdminNotifications.tsx} +0 -0
  155. /package/src/admin/components/{AdminUserDetails.tsx → users/AdminUserDetails.tsx} +0 -0
  156. /package/src/admin/components/{AdminUserSessions.tsx → users/AdminUserSessions.tsx} +0 -0
  157. /package/src/admin/components/{AdminVerifications.tsx → verifications/AdminVerifications.tsx} +0 -0
@@ -25,8 +25,10 @@ import {
25
25
  type GenericControlProps,
26
26
  parseInput,
27
27
  } from "../../utils/parseInput.ts";
28
+ import ControlArray, { type ControlArrayProps } from "./ControlArray.tsx";
28
29
  import ControlDate from "./ControlDate.tsx";
29
30
  import ControlNumber, { type ControlNumberProps } from "./ControlNumber.tsx";
31
+ import ControlObject, { type ControlObjectProps } from "./ControlObject.tsx";
30
32
  import ControlQueryBuilder from "./ControlQueryBuilder.tsx";
31
33
  import ControlSelect, { type ControlSelectProps } from "./ControlSelect.tsx";
32
34
 
@@ -43,6 +45,8 @@ export interface ControlProps extends GenericControlProps {
43
45
  datetime?: boolean | DateTimePickerProps;
44
46
  time?: boolean | TimeInputProps;
45
47
  query?: any; // Enable query builder mode with schema-aware autocomplete
48
+ object?: boolean | Partial<Omit<ControlObjectProps, "input">>; // Nested object editing
49
+ array?: boolean | Partial<Omit<ControlArrayProps, "input">>; // Array of items editing
46
50
  custom?: ComponentType<CustomControlProps>;
47
51
  }
48
52
 
@@ -64,17 +68,22 @@ export interface ControlProps extends GenericControlProps {
64
68
  * - DateTimePicker (for date-time format)
65
69
  * - TimeInput (for time format)
66
70
  * - QueryBuilder (for building type-safe queries with autocomplete)
71
+ * - ControlObject (for nested object schemas)
72
+ * - ControlArray (for arrays of objects)
67
73
  * - Custom component
68
74
  *
69
75
  * Automatically handles labels, descriptions, error messages, required state, and default icons.
70
76
  */
71
77
  const Control = (_props: ControlProps) => {
72
78
  const form = useFormState(_props.input, ["error"]);
73
- const { inputProps, id, icon, format, schema } = parseInput(_props, form);
79
+
80
+ // Early return if input is not properly initialized
74
81
  if (!_props.input?.props) {
75
82
  return null;
76
83
  }
77
84
 
85
+ const { inputProps, id, icon, format, schema } = parseInput(_props, form);
86
+
78
87
  const props = {
79
88
  ..._props,
80
89
  ...schema.$control,
@@ -114,6 +123,56 @@ const Control = (_props: ControlProps) => {
114
123
  }
115
124
  //endregion
116
125
 
126
+ //region <ControlObject/>
127
+ // Handle nested objects with properties
128
+ const isObject =
129
+ props.input.schema &&
130
+ "type" in props.input.schema &&
131
+ props.input.schema.type === "object" &&
132
+ "properties" in props.input.schema;
133
+
134
+ if (props.object || isObject) {
135
+ const controlObjectProps =
136
+ typeof props.object === "object" ? props.object : {};
137
+ return (
138
+ <ControlObject
139
+ input={props.input}
140
+ title={props.title}
141
+ description={props.description}
142
+ {...controlObjectProps}
143
+ />
144
+ );
145
+ }
146
+ //endregion
147
+
148
+ //region <ControlArray/>
149
+ // Handle arrays of objects (arrays of primitives are handled by ControlSelect)
150
+ const isArray =
151
+ props.input.schema &&
152
+ "type" in props.input.schema &&
153
+ props.input.schema.type === "array";
154
+
155
+ const isArrayOfObjects =
156
+ isArray &&
157
+ "items" in props.input.schema &&
158
+ props.input.schema.items &&
159
+ typeof props.input.schema.items === "object" &&
160
+ "properties" in props.input.schema.items;
161
+
162
+ if (props.array || isArrayOfObjects) {
163
+ const controlArrayProps =
164
+ typeof props.array === "object" ? props.array : {};
165
+ return (
166
+ <ControlArray
167
+ input={props.input}
168
+ title={props.title}
169
+ description={props.description}
170
+ {...controlArrayProps}
171
+ />
172
+ );
173
+ }
174
+ //endregion
175
+
117
176
  //region <NumberInput/>
118
177
  if (
119
178
  props.number ||
@@ -170,16 +229,13 @@ const Control = (_props: ControlProps) => {
170
229
 
171
230
  //region <ControlSelect/>
172
231
  // Handle: single enum, array of enum, array of strings, or explicit select/multi/tags props
232
+ // Note: arrays of objects are handled by ControlArray above, this handles primitive arrays
173
233
  const isEnum =
174
234
  props.input.schema &&
175
235
  "enum" in props.input.schema &&
176
236
  props.input.schema.enum;
177
- const isArray =
178
- props.input.schema &&
179
- "type" in props.input.schema &&
180
- props.input.schema.type === "array";
181
237
 
182
- if (isEnum || isArray || props.select) {
238
+ if (isEnum || (isArray && !isArrayOfObjects) || props.select) {
183
239
  const opts = typeof props.select === "object" ? props.select : {};
184
240
  return (
185
241
  <ControlSelect
@@ -195,21 +251,42 @@ const Control = (_props: ControlProps) => {
195
251
 
196
252
  //region <Switch/>
197
253
  if (
198
- (props.input.schema &&
199
- "type" in props.input.schema &&
200
- props.input.schema.type === "boolean") ||
201
- props.switch
254
+ props.input.schema &&
255
+ "type" in props.input.schema &&
256
+ props.input.schema.type === "boolean"
202
257
  ) {
203
- const switchProps = typeof props.switch === "object" ? props.switch : {};
258
+ if (props.switch) {
259
+ const switchProps = typeof props.switch === "object" ? props.switch : {};
260
+
261
+ return (
262
+ <Switch
263
+ {...inputProps}
264
+ id={id}
265
+ color={"blue"}
266
+ defaultChecked={props.input.props.defaultValue}
267
+ {...props.input.props}
268
+ {...switchProps}
269
+ />
270
+ );
271
+ }
272
+
273
+ // by default, render as <Select/> with Yes/No/Empty options
274
+ const selectProps: Partial<ControlSelectProps> = {
275
+ loader: async () => [
276
+ { value: "true", label: "Yes" },
277
+ { value: "false", label: "No" },
278
+ { value: "", label: "" },
279
+ ],
280
+ ...props.input.props,
281
+ };
204
282
 
205
283
  return (
206
- <Switch
207
- {...inputProps}
208
- id={id}
209
- color={"blue"}
210
- defaultChecked={props.input.props.defaultValue}
211
- {...props.input.props}
212
- {...switchProps}
284
+ <ControlSelect
285
+ input={props.input}
286
+ title={props.title}
287
+ description={props.description}
288
+ icon={icon}
289
+ {...selectProps}
213
290
  />
214
291
  );
215
292
  }
@@ -0,0 +1,377 @@
1
+ import { useEvents } from "@alepha/react";
2
+ import type { BaseInputField } from "@alepha/react/form";
3
+ import {
4
+ ActionIcon,
5
+ Fieldset,
6
+ Flex,
7
+ Grid,
8
+ Stack,
9
+ Text,
10
+ UnstyledButton,
11
+ } from "@mantine/core";
12
+ import { IconGripVertical, IconPlus, IconTrash } from "@tabler/icons-react";
13
+ import type { TObject, TSchema } from "alepha";
14
+ import { useRef, useState } from "react";
15
+ import { ui } from "../../constants/ui.ts";
16
+ import {
17
+ type GenericControlProps,
18
+ parseInput,
19
+ } from "../../utils/parseInput.ts";
20
+ import Control, { type ControlProps } from "./Control.tsx";
21
+
22
+ export interface ControlArrayProps extends GenericControlProps {
23
+ /**
24
+ * Minimum number of items allowed.
25
+ * @default 0
26
+ */
27
+ min?: number;
28
+
29
+ /**
30
+ * Maximum number of items allowed.
31
+ * @default Infinity
32
+ */
33
+ max?: number;
34
+
35
+ /**
36
+ * Label for the add button.
37
+ * @default "Add item"
38
+ */
39
+ addLabel?: string;
40
+
41
+ /**
42
+ * Number of columns for object item fields.
43
+ * @default 1
44
+ */
45
+ columns?: number;
46
+
47
+ /**
48
+ * Variant for the container.
49
+ * - "fieldset": Uses Mantine Fieldset with legend
50
+ * - "plain": No container, just renders items
51
+ * @default "fieldset"
52
+ */
53
+ variant?: "fieldset" | "plain";
54
+
55
+ /**
56
+ * Per-field control props override for object items.
57
+ * Keys are field names from the item schema.
58
+ */
59
+ controlProps?: Record<string, Partial<Omit<ControlProps, "input">>>;
60
+
61
+ /**
62
+ * Control props for primitive items.
63
+ */
64
+ itemControlProps?: Partial<Omit<ControlProps, "input">>;
65
+
66
+ /**
67
+ * Show drag handle for reordering.
68
+ * @default false
69
+ */
70
+ sortable?: boolean;
71
+ }
72
+
73
+ /**
74
+ * ControlArray component for editing arrays of schema items.
75
+ *
76
+ * Features:
77
+ * - Dynamic add/remove of items
78
+ * - Supports arrays of objects with nested fields
79
+ * - Supports arrays of primitives
80
+ * - Grid layout for object items
81
+ * - Min/max constraints
82
+ *
83
+ * @example
84
+ * ```tsx
85
+ * // For a schema like:
86
+ * // t.object({
87
+ * // contacts: t.array(t.object({
88
+ * // name: t.text(),
89
+ * // email: t.text({ format: "email" }),
90
+ * // }))
91
+ * // })
92
+ *
93
+ * <ControlArray
94
+ * input={form.input.contacts}
95
+ * columns={2}
96
+ * addLabel="Add contact"
97
+ * controlProps={{
98
+ * email: { text: { placeholder: "email@example.com" } }
99
+ * }}
100
+ * />
101
+ * ```
102
+ */
103
+ const ControlArray = (props: ControlArrayProps) => {
104
+ 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
+ );
141
+
142
+ if (!props.input?.props) {
143
+ return null;
144
+ }
145
+
146
+ const schema = props.input.schema;
147
+ if (!schema || !("items" in schema)) {
148
+ return null;
149
+ }
150
+
151
+ const itemSchema = (schema as any).items as TSchema;
152
+ const isObjectItem = itemSchema && "properties" in itemSchema;
153
+ const { min = 0, max = Number.POSITIVE_INFINITY, columns = 1 } = props;
154
+
155
+ const updateFormValue = (newItems: Array<{ key: number; value: any }>) => {
156
+ props.input.set(newItems.map((item) => item.value));
157
+ };
158
+
159
+ const handleAdd = () => {
160
+ if (items.length >= max) return;
161
+
162
+ // Create default value based on item schema
163
+ let newValue: any;
164
+ if (isObjectItem) {
165
+ newValue = {};
166
+ // Initialize with default values from schema if available
167
+ const objSchema = itemSchema as TObject;
168
+ for (const [key, propSchema] of Object.entries(objSchema.properties)) {
169
+ if ("default" in propSchema) {
170
+ newValue[key] = propSchema.default;
171
+ }
172
+ }
173
+ } else {
174
+ newValue = "";
175
+ }
176
+
177
+ const newItems = [...items, { key: idCounter.current++, value: newValue }];
178
+ setItems(newItems);
179
+ updateFormValue(newItems);
180
+ };
181
+
182
+ const handleRemove = (index: number) => {
183
+ if (items.length <= min) return;
184
+ const newItems = items.filter((_, i) => i !== index);
185
+ setItems(newItems);
186
+ updateFormValue(newItems);
187
+ };
188
+
189
+ const handleItemChange = (index: number, value: any) => {
190
+ const newItems = [...items];
191
+ newItems[index] = { ...newItems[index], value };
192
+ setItems(newItems);
193
+ updateFormValue(newItems);
194
+ };
195
+
196
+ const handleFieldChange = (index: number, field: string, value: any) => {
197
+ const newItems = [...items];
198
+ newItems[index] = {
199
+ ...newItems[index],
200
+ value: { ...newItems[index].value, [field]: value },
201
+ };
202
+ setItems(newItems);
203
+ updateFormValue(newItems);
204
+ };
205
+
206
+ const colSpan = 12 / columns;
207
+ const fieldNames = isObjectItem
208
+ ? Object.keys((itemSchema as TObject).properties)
209
+ : [];
210
+
211
+ const renderItems = () => (
212
+ <Stack gap="sm">
213
+ {items.map((item, index) => (
214
+ <Flex
215
+ key={item.key}
216
+ gap="sm"
217
+ align="flex-start"
218
+ p="xs"
219
+ bg={ui.colors.surface}
220
+ style={{ borderRadius: "var(--mantine-radius-sm)" }}
221
+ >
222
+ {props.sortable && (
223
+ <ActionIcon
224
+ variant="subtle"
225
+ color="gray"
226
+ style={{ cursor: "grab" }}
227
+ >
228
+ <IconGripVertical size={16} />
229
+ </ActionIcon>
230
+ )}
231
+
232
+ {isObjectItem ? (
233
+ <Grid style={{ flex: 1 }} gutter="sm">
234
+ {fieldNames.map((fieldName) => {
235
+ const fieldSchema = (itemSchema as TObject).properties[
236
+ fieldName
237
+ ];
238
+ 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
+ };
256
+
257
+ return (
258
+ <Grid.Col key={fieldName} span={colSpan}>
259
+ <Control input={virtualInput} {...fieldControlProps} />
260
+ </Grid.Col>
261
+ );
262
+ })}
263
+ </Grid>
264
+ ) : (
265
+ <Flex style={{ flex: 1 }}>
266
+ <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
+ }
281
+ {...props.itemControlProps}
282
+ />
283
+ </Flex>
284
+ )}
285
+
286
+ <ActionIcon
287
+ variant="subtle"
288
+ color="red"
289
+ onClick={() => handleRemove(index)}
290
+ disabled={items.length <= min}
291
+ >
292
+ <IconTrash size={16} />
293
+ </ActionIcon>
294
+ </Flex>
295
+ ))}
296
+
297
+ <UnstyledButton
298
+ onClick={handleAdd}
299
+ disabled={items.length >= max}
300
+ style={{
301
+ display: "flex",
302
+ alignItems: "center",
303
+ justifyContent: "center",
304
+ gap: 6,
305
+ padding: "8px 12px",
306
+ borderRadius: "var(--mantine-radius-sm)",
307
+ border: "1px dashed var(--mantine-color-dimmed)",
308
+ color: "var(--mantine-color-dimmed)",
309
+ fontSize: "var(--mantine-font-size-sm)",
310
+ cursor: items.length >= max ? "not-allowed" : "pointer",
311
+ opacity: items.length >= max ? 0.5 : 1,
312
+ transition: "all 150ms ease",
313
+ }}
314
+ onMouseEnter={(e) => {
315
+ if (items.length < max) {
316
+ e.currentTarget.style.borderColor =
317
+ "var(--mantine-color-blue-filled)";
318
+ e.currentTarget.style.color = "var(--mantine-color-blue-filled)";
319
+ e.currentTarget.style.background =
320
+ "var(--mantine-color-blue-light)";
321
+ }
322
+ }}
323
+ onMouseLeave={(e) => {
324
+ e.currentTarget.style.borderColor = "var(--mantine-color-dimmed)";
325
+ e.currentTarget.style.color = "var(--mantine-color-dimmed)";
326
+ e.currentTarget.style.background = "transparent";
327
+ }}
328
+ >
329
+ <IconPlus size={14} />
330
+ {props.addLabel ?? "Add"}
331
+ </UnstyledButton>
332
+ </Stack>
333
+ );
334
+
335
+ if (props.variant === "plain") {
336
+ return (
337
+ <Stack gap="xs">
338
+ {inputProps.label && (
339
+ <Text size="sm" fw={500}>
340
+ {inputProps.label}
341
+ </Text>
342
+ )}
343
+ {inputProps.description && (
344
+ <Text size="sm" c="dimmed">
345
+ {inputProps.description}
346
+ </Text>
347
+ )}
348
+ {renderItems()}
349
+ {inputProps.error && (
350
+ <Text size="sm" c="red">
351
+ {inputProps.error}
352
+ </Text>
353
+ )}
354
+ </Stack>
355
+ );
356
+ }
357
+
358
+ return (
359
+ <Fieldset legend={inputProps.label}>
360
+ <Stack gap="xs">
361
+ {inputProps.description && (
362
+ <Text size="sm" c="dimmed">
363
+ {inputProps.description}
364
+ </Text>
365
+ )}
366
+ {renderItems()}
367
+ {inputProps.error && (
368
+ <Text size="sm" c="red">
369
+ {inputProps.error}
370
+ </Text>
371
+ )}
372
+ </Stack>
373
+ </Fieldset>
374
+ );
375
+ };
376
+
377
+ export default ControlArray;
@@ -0,0 +1,127 @@
1
+ import type { BaseInputField, ObjectInputField } from "@alepha/react/form";
2
+ import { Fieldset, Grid, Stack, Text } from "@mantine/core";
3
+ import type { TObject } from "alepha";
4
+ import {
5
+ type GenericControlProps,
6
+ parseInput,
7
+ } from "../../utils/parseInput.ts";
8
+ import Control, { type ControlProps } from "./Control.tsx";
9
+
10
+ export interface ControlObjectProps extends GenericControlProps {
11
+ /**
12
+ * Number of columns for the grid layout.
13
+ * @default 1
14
+ */
15
+ columns?: number;
16
+
17
+ /**
18
+ * Variant for the container.
19
+ * - "fieldset": Uses Mantine Fieldset with legend
20
+ * - "plain": No container, just renders fields
21
+ * @default "fieldset"
22
+ */
23
+ variant?: "fieldset" | "plain";
24
+
25
+ /**
26
+ * Per-field control props override.
27
+ * Keys are field names from the schema.
28
+ */
29
+ controlProps?: Record<string, Partial<Omit<ControlProps, "input">>>;
30
+ }
31
+
32
+ /**
33
+ * ControlObject component for editing nested object schemas.
34
+ *
35
+ * Features:
36
+ * - Renders all properties of an object schema
37
+ * - Supports grid layout with configurable columns
38
+ * - Per-field customization via controlProps
39
+ * - Recursive support for deeply nested objects
40
+ *
41
+ * The form system provides nested InputFields under the `.items` property.
42
+ * For example: form.input.address.items.street
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * // For a schema like:
47
+ * // t.object({
48
+ * // address: t.object({
49
+ * // street: t.text(),
50
+ * // city: t.text(),
51
+ * // zip: t.text(),
52
+ * // })
53
+ * // })
54
+ *
55
+ * <ControlObject
56
+ * input={form.input.address}
57
+ * columns={2}
58
+ * controlProps={{
59
+ * zip: { text: { maxLength: 10 } }
60
+ * }}
61
+ * />
62
+ * ```
63
+ */
64
+ const ControlObject = (props: ControlObjectProps) => {
65
+ const { inputProps } = parseInput(props, {});
66
+
67
+ if (!props.input?.props) {
68
+ return null;
69
+ }
70
+
71
+ const schema = props.input.schema as TObject;
72
+ if (!schema?.properties) {
73
+ return null;
74
+ }
75
+
76
+ const fieldNames = Object.keys(schema.properties);
77
+ const columns = props.columns ?? 1;
78
+ const colSpan = 12 / columns;
79
+
80
+ // The form system provides nested InputFields under .items
81
+ const objectInput = props.input as ObjectInputField<TObject>;
82
+ const nestedItems = objectInput.items as Record<string, BaseInputField>;
83
+
84
+ const renderFields = () => (
85
+ <Grid>
86
+ {fieldNames.map((fieldName) => {
87
+ const fieldControlProps = props.controlProps?.[fieldName] ?? {};
88
+
89
+ // Access nested InputField from .items
90
+ const field = nestedItems?.[fieldName];
91
+ if (!field) {
92
+ return null;
93
+ }
94
+
95
+ return (
96
+ <Grid.Col key={fieldName} span={colSpan}>
97
+ <Control input={field} {...fieldControlProps} />
98
+ </Grid.Col>
99
+ );
100
+ })}
101
+ </Grid>
102
+ );
103
+
104
+ if (props.variant === "plain") {
105
+ return renderFields();
106
+ }
107
+
108
+ return (
109
+ <Fieldset legend={inputProps.label}>
110
+ <Stack gap="xs">
111
+ {inputProps.description && (
112
+ <Text size="sm" c="dimmed">
113
+ {inputProps.description}
114
+ </Text>
115
+ )}
116
+ {renderFields()}
117
+ {inputProps.error && (
118
+ <Text size="sm" c="red">
119
+ {inputProps.error}
120
+ </Text>
121
+ )}
122
+ </Stack>
123
+ </Fieldset>
124
+ );
125
+ };
126
+
127
+ export default ControlObject;