@dxos/react-ui-form 0.8.4-main.7ace549 → 0.8.4-main.937b3ca

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 (167) hide show
  1. package/dist/lib/browser/index.mjs +2191 -2283
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +2191 -2283
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/ControlSection/ControlSection.d.ts +2 -2
  8. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  9. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +7 -4
  10. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +6 -5
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +9 -3
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form-old.stories.d.ts +131 -0
  16. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Form/Form.d.ts +81 -27
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +53 -92
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +37 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  26. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
  28. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  30. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  32. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  34. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
  36. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  38. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
  40. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  42. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  44. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/{custom/SelectOptionsInput.stories.d.ts → fields/SelectOptionField.stories.d.ts} +6 -3
  46. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  48. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  50. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  52. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  54. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/index.d.ts +2 -4
  56. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  57. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +14 -9
  58. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  59. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +5 -2
  60. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  61. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -8
  62. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  63. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -2
  64. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/testing.d.ts +0 -2
  66. package/dist/types/src/components/testing.d.ts.map +1 -1
  67. package/dist/types/src/hooks/index.d.ts +2 -3
  68. package/dist/types/src/hooks/index.d.ts.map +1 -1
  69. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  70. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  71. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  72. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  73. package/dist/types/src/translations.d.ts +6 -3
  74. package/dist/types/src/translations.d.ts.map +1 -1
  75. package/dist/types/src/util/index.d.ts +1 -1
  76. package/dist/types/src/util/index.d.ts.map +1 -1
  77. package/dist/types/src/util/properties.d.ts +7 -0
  78. package/dist/types/src/util/properties.d.ts.map +1 -0
  79. package/dist/types/src/util/refs.d.ts +1 -2
  80. package/dist/types/src/util/refs.d.ts.map +1 -1
  81. package/dist/types/tsconfig.tsbuildinfo +1 -1
  82. package/package.json +49 -44
  83. package/src/components/ControlSection/ControlSection.tsx +8 -8
  84. package/src/components/FieldEditor/FieldEditor.stories.tsx +42 -21
  85. package/src/components/FieldEditor/FieldEditor.tsx +72 -75
  86. package/src/components/Form/Form-old.stories.tsx +314 -0
  87. package/src/components/Form/Form.stories.tsx +146 -246
  88. package/src/components/Form/Form.tsx +319 -110
  89. package/src/components/Form/FormField.tsx +286 -0
  90. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  91. package/src/components/Form/FormFieldComponent.tsx +174 -0
  92. package/src/components/Form/FormFieldSet.tsx +125 -0
  93. package/src/components/Form/README.md +3 -0
  94. package/src/components/Form/fields/ArrayField.tsx +151 -0
  95. package/src/components/Form/fields/BooleanField.tsx +22 -0
  96. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  97. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  98. package/src/components/Form/fields/NumberField.tsx +39 -0
  99. package/src/components/Form/fields/RefField.tsx +181 -0
  100. package/src/components/Form/fields/SelectField.tsx +65 -0
  101. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +17 -14
  102. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +20 -14
  103. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  104. package/src/components/Form/fields/TextField.tsx +38 -0
  105. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +15 -8
  106. package/src/components/Form/fields/index.ts +15 -0
  107. package/src/components/Form/index.ts +3 -4
  108. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +15 -8
  109. package/src/components/ObjectPicker/ObjectPicker.tsx +76 -62
  110. package/src/components/ViewEditor/ViewEditor.stories.tsx +29 -25
  111. package/src/components/ViewEditor/ViewEditor.test.tsx +6 -2
  112. package/src/components/ViewEditor/ViewEditor.tsx +220 -154
  113. package/src/components/testing.tsx +9 -7
  114. package/src/hooks/index.ts +2 -3
  115. package/src/hooks/useFormHandler.ts +356 -0
  116. package/src/hooks/useKeyHandler.tsx +37 -0
  117. package/src/translations.ts +8 -6
  118. package/src/util/index.ts +1 -1
  119. package/src/util/properties.ts +18 -0
  120. package/src/util/refs.ts +7 -13
  121. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  122. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  123. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  124. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  125. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  126. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  127. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  128. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  129. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  130. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  131. package/dist/types/src/components/Form/Input.d.ts +0 -29
  132. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  133. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  134. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  135. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  136. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  137. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  138. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  139. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  140. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  141. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  142. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  143. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  144. package/dist/types/src/components/Form/factory.d.ts +0 -8
  145. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  146. package/dist/types/src/hooks/useForm.d.ts +0 -67
  147. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  148. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  149. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  150. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  151. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  152. package/dist/types/src/util/schema.d.ts +0 -3
  153. package/dist/types/src/util/schema.d.ts.map +0 -1
  154. package/src/components/Form/ArrayField.tsx +0 -127
  155. package/src/components/Form/Defaults.tsx +0 -205
  156. package/src/components/Form/FormActions.tsx +0 -59
  157. package/src/components/Form/FormContent.tsx +0 -362
  158. package/src/components/Form/FormContext.tsx +0 -110
  159. package/src/components/Form/Input.tsx +0 -54
  160. package/src/components/Form/RefField.tsx +0 -283
  161. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  162. package/src/components/Form/custom/index.ts +0 -7
  163. package/src/components/Form/factory.tsx +0 -34
  164. package/src/hooks/useForm.ts +0 -296
  165. package/src/hooks/useQueryRefOptions.ts +0 -62
  166. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  167. package/src/util/schema.ts +0 -12
@@ -0,0 +1,356 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
6
+ import type * as Schema from 'effect/Schema';
7
+ import type * as SchemaAST from 'effect/SchemaAST';
8
+ import { useCallback, useEffect, useMemo, useState } from 'react';
9
+
10
+ import { type AnyProperties } from '@dxos/echo/internal';
11
+ import {
12
+ type JsonPath,
13
+ createJsonPath,
14
+ fromEffectValidationPath,
15
+ getValue as getValue$,
16
+ setValue as setValue$,
17
+ } from '@dxos/effect';
18
+ import { log } from '@dxos/log';
19
+ import { useDefaultValue } from '@dxos/react-ui';
20
+ import { type ValidationError, validateSchema } from '@dxos/schema';
21
+ import { type MaybePromise } from '@dxos/util';
22
+
23
+ export type FormFieldStatus = {
24
+ status?: 'error';
25
+ error?: string;
26
+ };
27
+
28
+ /**
29
+ * Form properties.
30
+ */
31
+ export interface FormHandlerProps<T extends AnyProperties> {
32
+ /**
33
+ * Effect schema (Type literal).
34
+ */
35
+ schema?: Schema.Schema<T, any>;
36
+
37
+ /**
38
+ * Values for the form if controlled.
39
+ */
40
+ values?: Partial<T>;
41
+
42
+ /**
43
+ * Default values for the form which will be used if values is not provided.
44
+ */
45
+ defaultValues?: Partial<T>;
46
+
47
+ /**
48
+ * Auto-save the form when the values change.
49
+ */
50
+ autoSave?: boolean;
51
+
52
+ /**
53
+ * Callback for value changes. Note: This is called even when values are invalid.
54
+ * Sometimes the parent component may want to know about changes even if the form is
55
+ * in an invalid state.
56
+ */
57
+ onValuesChanged?: (values: Partial<T>, meta: FormUpdateMeta<T>) => void;
58
+
59
+ /**
60
+ * Custom validation function that runs only after schema validation passes.
61
+ * Use this for complex validation logic that can't be expressed in the schema.
62
+ * @returns Array of validation errors, or undefined if validation passes
63
+ */
64
+ onValidate?: (values: T) => ValidationError[] | undefined;
65
+
66
+ /**
67
+ * Called when the form is submitted and passes validation.
68
+ */
69
+ onSave?: (values: T, meta: FormUpdateMeta<T>) => MaybePromise<void>;
70
+
71
+ /**
72
+ * Called when the form is canceled.
73
+ */
74
+ onCancel?: () => void;
75
+ }
76
+
77
+ export type FormUpdateMeta<T extends AnyProperties> = {
78
+ changed: FormHandler<T>['changed'];
79
+ isValid: boolean;
80
+ };
81
+
82
+ /**
83
+ * Form handler properties and methods.
84
+ */
85
+ export type FormHandler<T extends AnyProperties> = Pick<FormHandlerProps<T>, 'schema' | 'autoSave'> & {
86
+ /** Initial values (which may not pass validation). */
87
+ values: Partial<T>;
88
+
89
+ /** Map of changed fields. */
90
+ changed: Record<JsonPath, boolean>;
91
+ // TODO(burdon): How is this different from above?
92
+ touched: Record<JsonPath, boolean>;
93
+
94
+ /** Map of error strings. */
95
+ errors: Record<JsonPath, string>;
96
+
97
+ /** Whether the form can be saved (i.e., data is valid). */
98
+ isValid: boolean;
99
+ // TODO(burdon): Why is this needed separately from isValid?
100
+ canSave: boolean;
101
+
102
+ onSave: () => void;
103
+ onCancel: () => void;
104
+
105
+ //
106
+ // Form field state management
107
+ //
108
+
109
+ getStatus: (path: string | (string | number)[]) => FormFieldStatus;
110
+ getValue: <V>(path: (string | number)[]) => V | undefined;
111
+ onBlur: (path: (string | number)[]) => void;
112
+ onValueChange: <V>(path: (string | number)[], type: SchemaAST.AST, value: V) => void;
113
+ };
114
+
115
+ /**
116
+ * Creates a hook for managing form state, including values, validation, and submission.
117
+ * Deeply integrated with `@dxos/schema` for schema-based validation.
118
+ */
119
+ export const useFormHandler = <T extends AnyProperties>({
120
+ schema,
121
+ autoSave,
122
+ values: valuesProp,
123
+ defaultValues: defaultValuesProp,
124
+ onValuesChanged,
125
+ onValidate,
126
+ onSave,
127
+ onCancel,
128
+ ...props
129
+ }: FormHandlerProps<T>): FormHandler<T> => {
130
+ const [changed, setChanged] = useState<Record<JsonPath, boolean>>({});
131
+ const [touched, setTouched] = useState<Record<JsonPath, boolean>>({});
132
+ const [errors, setErrors] = useState<Record<JsonPath, string>>({});
133
+ const [saving, setSaving] = useState(false);
134
+ const defaultValues = useDefaultValue<Partial<T>>(defaultValuesProp, () => ({}));
135
+ const [values$, setValues] = useControllableState<Partial<T>>({
136
+ prop: valuesProp,
137
+ defaultProp: defaultValues,
138
+ onChange: () => {
139
+ setValues(valuesProp ?? defaultValues);
140
+ setChanged({});
141
+ setTouched({});
142
+ setErrors({});
143
+ setSaving(false);
144
+ },
145
+ });
146
+ // TODO(wittjosiah): Upgrade @radix-ui/react-use-controllable-state.
147
+ const values = values$ as Partial<T>;
148
+
149
+ // Validate.
150
+ const validate = useCallback(
151
+ (values: Partial<T>): values is T => {
152
+ if (!schema) {
153
+ return false;
154
+ }
155
+
156
+ let errors: ValidationError[] = validateSchema(schema, values) ?? [];
157
+ log('validate', { values, errors });
158
+ if (errors.length === 0 && onValidate) {
159
+ const validatedValues = values as T;
160
+ errors = onValidate(validatedValues) ?? [];
161
+ }
162
+
163
+ setErrors(flatMap(errors));
164
+ const valid = errors.length === 0;
165
+ return valid;
166
+ },
167
+ [schema, onValidate],
168
+ );
169
+
170
+ // Validate on schema change.
171
+ const [schemaChanged, setSchemaChanged] = useState(false);
172
+ useEffect(() => setSchemaChanged(true), [schema]);
173
+ useEffect(() => {
174
+ if (schemaChanged) {
175
+ validate(values);
176
+ setSchemaChanged(false);
177
+ }
178
+ }, [validate, values, schemaChanged]);
179
+
180
+ const isValid = useMemo(() => {
181
+ return Object.keys(errors).length === 0;
182
+ }, [errors]);
183
+
184
+ /**
185
+ * NOTE: We can submit if there is no touched field that has an error.
186
+ * If there's a validation message visible in the form, submit should be disabled.
187
+ */
188
+ const canSave = useMemo(
189
+ () =>
190
+ !saving &&
191
+ // Check if any error paths that are touched have errors.
192
+ !Object.entries(touched).some(
193
+ ([path, isTouched]) =>
194
+ isTouched &&
195
+ Object.keys(errors).some(
196
+ (errorPath) => errorPath === path || errorPath.startsWith(`${path}.`) || errorPath.startsWith(`${path}[`),
197
+ ),
198
+ ),
199
+ [touched, errors, saving],
200
+ );
201
+
202
+ const handleSave = useCallback(async () => {
203
+ if (validate(values)) {
204
+ setSaving(true);
205
+ try {
206
+ await onSave?.(values, { changed, isValid });
207
+ } finally {
208
+ setSaving(false);
209
+ }
210
+ }
211
+ }, [values, validate, onSave, changed, isValid]);
212
+
213
+ const handleCancel = useCallback(() => {
214
+ onCancel?.();
215
+ }, [onCancel]);
216
+
217
+ //
218
+ // Fields.
219
+ //
220
+
221
+ const getStatus = useCallback<FormHandler<T>['getStatus']>(
222
+ (path) => {
223
+ const jsonPath = Array.isArray(path) ? createJsonPath(path) : path;
224
+ const [_, error] =
225
+ Object.entries(errors).find(
226
+ ([errorPath]) =>
227
+ errorPath === jsonPath || errorPath.startsWith(`${jsonPath}.`) || errorPath.startsWith(`${jsonPath}[`),
228
+ ) ?? [];
229
+
230
+ // Only show errors for touched fields.
231
+ const isTouched = touched[jsonPath as JsonPath];
232
+ if (!isTouched) {
233
+ return {
234
+ status: undefined,
235
+ error: undefined,
236
+ };
237
+ }
238
+
239
+ return {
240
+ status: error ? 'error' : undefined,
241
+ error: error ? (error ?? undefined) : undefined,
242
+ };
243
+ },
244
+ [errors, touched],
245
+ );
246
+
247
+ const getValue = useCallback<FormHandler<T>['getValue']>(
248
+ (path) => {
249
+ return getValue$(values, createJsonPath(path));
250
+ },
251
+ [values],
252
+ );
253
+
254
+ const onValueChange = useCallback<FormHandler<T>['onValueChange']>(
255
+ (path, type, value) => {
256
+ log('onValueChange', { path, value });
257
+
258
+ const jsonPath = createJsonPath(path);
259
+ let parsedValue = value as any;
260
+ try {
261
+ if (type._tag === 'NumberKeyword') {
262
+ parsedValue = parseFloat(value as string) || 0;
263
+ }
264
+ } catch (err) {
265
+ log.catch(err);
266
+ parsedValue = undefined;
267
+ }
268
+
269
+ // Update.
270
+ const newValues = { ...setValue$(values, jsonPath, parsedValue) };
271
+ setValues(newValues);
272
+
273
+ // TODO(burdon): Check value has changed from original.
274
+ const newChanged = { [jsonPath]: true };
275
+ setChanged({ ...changed, ...newChanged });
276
+
277
+ // Validate.
278
+ const isValid = validate(newValues);
279
+
280
+ // Callback.
281
+ onValuesChanged?.(newValues, { isValid, changed: newChanged });
282
+ },
283
+ [values, changed, validate, onValuesChanged],
284
+ );
285
+
286
+ const onBlur = useCallback(
287
+ async (path: (string | number)[]) => {
288
+ const jsonPath = createJsonPath(path);
289
+
290
+ // TODO(burdon): Check value has changed from original.
291
+ setTouched((touched) => ({ ...touched, [jsonPath]: true }));
292
+ const isValid = validate(values);
293
+
294
+ // Auto-save when a field is blurred and is valid.
295
+ if (Object.keys(changed).length > 0 && isValid && autoSave) {
296
+ await onSave?.(values as T, { changed, isValid });
297
+ }
298
+ },
299
+ [validate, values, changed, autoSave, onSave],
300
+ );
301
+
302
+ return useMemo<FormHandler<T>>(
303
+ () => ({
304
+ // State.
305
+ schema,
306
+ values,
307
+ errors,
308
+ touched,
309
+ changed,
310
+ isValid,
311
+ canSave,
312
+ autoSave,
313
+
314
+ // Actions.
315
+ onSave: handleSave,
316
+ onCancel: handleCancel,
317
+
318
+ // Field utils.
319
+ getStatus,
320
+ getValue,
321
+ onBlur,
322
+ onValueChange,
323
+
324
+ ...props,
325
+ }),
326
+ [
327
+ schema,
328
+ values,
329
+ errors,
330
+ touched,
331
+ changed,
332
+ canSave,
333
+ isValid,
334
+ getStatus,
335
+ getValue,
336
+ onBlur,
337
+ onValueChange,
338
+ onSave,
339
+ onCancel,
340
+ ],
341
+ );
342
+ };
343
+
344
+ const flatMap = (errors: ValidationError[]) => {
345
+ return errors.reduce(
346
+ (result, { path, message }) => {
347
+ // Convert the validation error path format to our JsonPath format.
348
+ const jsonPath = fromEffectValidationPath(path);
349
+ if (!(jsonPath in result)) {
350
+ result[jsonPath] = message;
351
+ }
352
+ return result;
353
+ },
354
+ {} as Record<JsonPath, string>,
355
+ );
356
+ };
@@ -0,0 +1,37 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useCallback, useEffect } from 'react';
6
+
7
+ import { addEventListener } from '@dxos/async';
8
+
9
+ import { type FormHandler } from './useFormHandler';
10
+
11
+ /**
12
+ * Key handler.
13
+ */
14
+ export const useKeyHandler = (formElement: HTMLDivElement | null, form: FormHandler<any>) => {
15
+ const handleKeyDown = useCallback(
16
+ (event: KeyboardEvent) => {
17
+ switch (event.key) {
18
+ case 'Enter': {
19
+ if (event.metaKey && form.canSave) {
20
+ form.onSave();
21
+ }
22
+ break;
23
+ }
24
+ }
25
+ },
26
+ [form.isValid, form.canSave, form.onSave, form.autoSave],
27
+ );
28
+
29
+ useEffect(() => {
30
+ if (!formElement) {
31
+ return;
32
+ }
33
+
34
+ // TODO(burdon): Move to @dxos/dom-util.
35
+ return addEventListener(formElement, 'keydown', handleKeyDown);
36
+ }, [formElement, handleKeyDown]);
37
+ };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- export const translationKey = 'react-ui-form';
7
+ export const translationKey = '@dxos/react-ui-form';
8
8
 
9
9
  export const translations = [
10
10
  {
@@ -32,7 +32,10 @@ export const translations = [
32
32
  'ref field placeholder_one': 'Select…',
33
33
  'ref field placeholder_other': 'Select items…',
34
34
 
35
- // TODO(burdon): Factor out?
35
+ 'example placeholder': 'Example',
36
+ 'latitude placeholder': 'Latitude (e.g., 40.7128)',
37
+ 'longitude placeholder': 'Longitude (e.g., -74.0060)',
38
+
36
39
  'cancel button label': 'Cancel',
37
40
  'save button label': 'Save',
38
41
 
@@ -66,11 +69,11 @@ export const translations = [
66
69
  'format url': 'URL',
67
70
  'format uuid': 'UUID',
68
71
 
69
- // GeoPointInput.
72
+ // GeoPointField.
70
73
  'latitude label': 'Latitude',
71
74
  'longitude label': 'Longitude',
72
75
 
73
- // SelectOptionsInput.
76
+ // SelectOptionsField.
74
77
  'select option label': 'Label',
75
78
  'select option label placeholder': 'Option label',
76
79
  'select option color': 'Color',
@@ -79,8 +82,7 @@ export const translations = [
79
82
 
80
83
  // System schema message.
81
84
  'system schema title': 'System type',
82
- 'system schema description':
83
- 'This is a system type and cannot be modified, though the view can still be configured.',
85
+ 'system schema description': 'System type: cannot add or remove fields.',
84
86
  },
85
87
  },
86
88
  },
package/src/util/index.ts CHANGED
@@ -4,5 +4,5 @@
4
4
 
5
5
  export * from './field';
6
6
  export * from './formatting';
7
+ export * from './properties';
7
8
  export * from './refs';
8
- export * from './schema';
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Option from 'effect/Option';
6
+ import type * as SchemaAST from 'effect/SchemaAST';
7
+
8
+ import { Annotation } from '@dxos/echo';
9
+ import { type SchemaProperty, getProperties } from '@dxos/effect';
10
+
11
+ /**
12
+ * Get the property types of an AST and filter out properties that are not form inputs.
13
+ */
14
+ export const getFormProperties = (ast: SchemaAST.AST): SchemaProperty[] => {
15
+ return getProperties(ast).filter((prop) =>
16
+ Annotation.FormInputAnnotation.getFromAst(prop.type).pipe(Option.getOrElse(() => true)),
17
+ );
18
+ };
package/src/util/refs.ts CHANGED
@@ -4,33 +4,27 @@
4
4
 
5
5
  import type * as SchemaAST from 'effect/SchemaAST';
6
6
 
7
- import { Format, ReferenceAnnotationId } from '@dxos/echo/internal';
8
- import { findAnnotation } from '@dxos/effect';
9
- import { type SchemaProperty } from '@dxos/schema';
10
-
11
- import { findArrayElementType } from './schema';
7
+ import { Ref } from '@dxos/echo';
8
+ import { getArrayElementType, isArrayType } from '@dxos/effect';
12
9
 
13
10
  type RefProps = {
14
11
  ast: SchemaAST.AST;
15
12
  isArray: boolean;
16
13
  };
17
14
 
18
- export const getRefProps = (property: SchemaProperty<any>): RefProps | undefined => {
19
- const { ast, format, array } = property;
20
-
15
+ export const getRefProps = (ast: SchemaAST.AST): RefProps | undefined => {
21
16
  // Array of references.
22
- if (array) {
23
- const elementType = findArrayElementType(ast);
17
+ if (isArrayType(ast)) {
18
+ const elementType = getArrayElementType(ast);
24
19
  if (elementType) {
25
- const annotation = findAnnotation(elementType, ReferenceAnnotationId);
26
- if (annotation) {
20
+ if (Ref.isRefType(elementType)) {
27
21
  return { ast: elementType, isArray: true };
28
22
  }
29
23
  }
30
24
  }
31
25
 
32
26
  // Direct reference.
33
- if (format === Format.TypeFormat.Ref) {
27
+ if (Ref.isRefType(ast)) {
34
28
  return { ast, isArray: false };
35
29
  }
36
30
 
@@ -1,16 +0,0 @@
1
- import React from 'react';
2
- import { type SchemaProperty } from '@dxos/schema';
3
- import { type ComponentLookup } from './Form';
4
- import { type FormFieldProps } from './FormContent';
5
- import { type FormInputStateProps } from './FormContext';
6
- import { type InputComponent } from './Input';
7
- type ArrayFieldProps = {
8
- property: SchemaProperty<any>;
9
- inputProps: FormInputStateProps;
10
- path?: (string | number)[];
11
- Custom?: Partial<Record<string, InputComponent>>;
12
- lookupComponent?: ComponentLookup;
13
- } & Pick<FormFieldProps, 'readonly'>;
14
- export declare const ArrayField: ({ property, readonly, path, inputProps, Custom, lookupComponent }: ArrayFieldProps) => React.JSX.Element | null;
15
- export {};
16
- //# sourceMappingURL=ArrayField.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ArrayField.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/ArrayField.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAKrD,OAAO,EAAE,KAAK,cAAc,EAAuB,MAAM,cAAc,CAAC;AAKxE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAa,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,KAAK,mBAAmB,EAAiB,MAAM,eAAe,CAAC;AACxE,OAAO,EAAE,KAAK,cAAc,EAAe,MAAM,SAAS,CAAC;AAE3D,KAAK,eAAe,GAAG;IACrB,QAAQ,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;IAC9B,UAAU,EAAE,mBAAmB,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IACjD,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,GAAG,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;AAErC,eAAO,MAAM,UAAU,GAAI,mEAAmE,eAAe,6BAgG5G,CAAC"}
@@ -1,14 +0,0 @@
1
- import React from 'react';
2
- import { type InputProps } from './Input';
3
- export declare const TextInput: ({ type, label, inputOnly, readonly, placeholder, getStatus, getValue, onValueChange, onBlur, }: InputProps) => React.JSX.Element | null;
4
- export declare const NumberInput: ({ type, label, inputOnly, readonly, placeholder, getStatus, getValue, onValueChange, onBlur, }: InputProps) => React.JSX.Element | null;
5
- export declare const BooleanInput: ({ type, label, inputOnly, getStatus, getValue, onValueChange, readonly }: InputProps) => React.JSX.Element;
6
- export type SelectInputOptions = InputProps & {
7
- options?: Array<{
8
- value: string | number;
9
- label?: string;
10
- }>;
11
- };
12
- export declare const SelectInput: ({ type, label, inputOnly, readonly, placeholder, options, getStatus, getValue, onValueChange, }: SelectInputOptions) => React.JSX.Element | null;
13
- export declare const MarkdownInput: ({ type, label, inputOnly, readonly, placeholder, getStatus, getValue, onValueChange, onBlur, }: InputProps) => React.JSX.Element;
14
- //# sourceMappingURL=Defaults.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Defaults.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/Defaults.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAM9D,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAEvD,eAAO,MAAM,SAAS,GAAI,gGAUvB,UAAU,6BAsBZ,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,gGAUzB,UAAU,6BAuBZ,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,0EAA0E,UAAU,sBAgBhH,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG;IAC5C,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7D,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,iGAUzB,kBAAkB,6BAiCpB,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,gGAU3B,UAAU,sBA8CZ,CAAC"}
@@ -1,9 +0,0 @@
1
- import React from 'react';
2
- export type FormOuterSpacing = boolean | 'blockStart-0' | 'scroll-fields';
3
- export type FormActionsProps = {
4
- readonly?: boolean;
5
- onCancel?: () => void;
6
- outerSpacing?: FormOuterSpacing;
7
- };
8
- export declare const FormActions: ({ readonly, onCancel, outerSpacing }: FormActionsProps) => React.JSX.Element;
9
- //# sourceMappingURL=FormActions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FormActions.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/FormActions.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAU1B,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,cAAc,GAAG,eAAe,CAAC;AAE1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,gBAAgB,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,sCAA6C,gBAAgB,sBAoCxF,CAAC"}
@@ -1,63 +0,0 @@
1
- import * as Schema from 'effect/Schema';
2
- import React from 'react';
3
- import { type ThemedClassName } from '@dxos/react-ui';
4
- import { type ProjectionModel, type SchemaProperty } from '@dxos/schema';
5
- import { type QueryRefOptions } from '../../hooks';
6
- import { type ComponentLookup } from './Form';
7
- import { type InputComponent } from './Input';
8
- import { type RefFieldProps } from './RefField';
9
- export type FormFieldProps = {
10
- property: SchemaProperty<any>;
11
- path?: (string | number)[];
12
- /** Used to indicate if input should be presented inline (e.g. for array items). */
13
- inline?: boolean;
14
- projection?: ProjectionModel;
15
- lookupComponent?: ComponentLookup;
16
- Custom?: Partial<Record<string, InputComponent>>;
17
- } & Pick<RefFieldProps, 'readonly' | 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreate' | 'createSchema' | 'createInitialValuePath'>;
18
- export declare const FormField: ({ property, path, readonly, inline, projection, onQueryRefOptions, createOptionLabel, createOptionIcon, onCreate, createSchema, createInitialValuePath, lookupComponent, Custom, }: FormFieldProps) => React.JSX.Element | null;
19
- export type FormFieldsProps = ThemedClassName<{
20
- testId?: string;
21
- schema: Schema.Schema.All;
22
- /**
23
- * Path to the current object from the root. Used with nested forms.
24
- */
25
- path?: (string | number)[];
26
- filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
27
- sort?: string[];
28
- /**
29
- * Optional projection for projection-based field management
30
- */
31
- projection?: ProjectionModel;
32
- lookupComponent?: ComponentLookup;
33
- /**
34
- * Map of custom renderers for specific properties.
35
- * Prefer lookupComponent for plugin specific input surfaces.
36
- */
37
- Custom?: Partial<Record<string, InputComponent>>;
38
- onQueryRefOptions?: QueryRefOptions;
39
- } & Pick<FormFieldProps, 'readonly'> & Pick<RefFieldProps, 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreate' | 'createSchema' | 'createInitialValuePath'>>;
40
- export declare const FormFields: React.ForwardRefExoticComponent<Omit<{
41
- testId?: string;
42
- schema: Schema.Schema.All;
43
- /**
44
- * Path to the current object from the root. Used with nested forms.
45
- */
46
- path?: (string | number)[];
47
- filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
48
- sort?: string[];
49
- /**
50
- * Optional projection for projection-based field management
51
- */
52
- projection?: ProjectionModel;
53
- lookupComponent?: ComponentLookup;
54
- /**
55
- * Map of custom renderers for specific properties.
56
- * Prefer lookupComponent for plugin specific input surfaces.
57
- */
58
- Custom?: Partial<Record<string, InputComponent>>;
59
- onQueryRefOptions?: QueryRefOptions;
60
- } & Pick<FormFieldProps, "readonly"> & Pick<RefFieldProps, "onQueryRefOptions" | "createOptionLabel" | "createOptionIcon" | "onCreate" | "createSchema" | "createInitialValuePath">, "className"> & {
61
- classNames?: import("@dxos/react-ui-types").ClassNameValue;
62
- } & React.RefAttributes<HTMLDivElement>>;
63
- //# sourceMappingURL=FormContent.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FormContent.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/FormContent.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAiF,MAAM,OAAO,CAAC;AAGtG,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAuB,MAAM,cAAc,CAAC;AAG9F,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAMnD,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC3B,mFAAmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;CAClD,GAAG,IAAI,CACN,aAAa,EACX,UAAU,GACV,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB,GAClB,UAAU,GACV,cAAc,GACd,wBAAwB,CAC3B,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,oLAcvB,cAAc,6BAwJhB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,eAAe,CAC3C;IACE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC;IAC1B;;OAEG;IACH,IAAI,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,KAAK,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;IACjE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;OAEG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IACjD,iBAAiB,CAAC,EAAE,eAAe,CAAC;CACrC,GAAG,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,GAClC,IAAI,CACF,aAAa,EACX,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB,GAClB,UAAU,GACV,cAAc,GACd,wBAAwB,CAC3B,CACJ,CAAC;AAEF,eAAO,MAAM,UAAU;aA/BV,MAAM;YACP,MAAM,CAAC,MAAM,CAAC,GAAG;IACzB;;OAEG;WACI,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE;aACjB,CAAC,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,KAAK,cAAc,CAAC,GAAG,CAAC,EAAE;WACzD,MAAM,EAAE;IACf;;OAEG;iBACU,eAAe;sBACV,eAAe;IACjC;;;OAGG;aACM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;wBAC5B,eAAe;;;wCAwFtC,CAAC"}
@@ -1,23 +0,0 @@
1
- import React, { type FocusEvent, type PropsWithChildren, type RefObject } from 'react';
2
- import { type AnyProperties } from '@dxos/echo/internal';
3
- import { type SimpleType } from '@dxos/effect';
4
- import { type FormHandler, type FormOptions } from '../../hooks';
5
- type FormContextValue<T extends AnyProperties> = FormHandler<T>;
6
- export declare const useFormContext: <T extends AnyProperties>() => FormContextValue<T>;
7
- export declare const useFormValues: (path?: (string | number)[]) => any;
8
- export type FormInputStateProps = {
9
- getStatus: () => {
10
- status?: 'error';
11
- error?: string;
12
- };
13
- getValue: <V>() => V | undefined;
14
- onValueChange: (type: SimpleType, value: any) => void;
15
- onBlur: (event: FocusEvent<HTMLElement>) => void;
16
- };
17
- export declare const useInputProps: (path?: (string | number)[]) => FormInputStateProps;
18
- export declare const FormProvider: ({ children, formRef, autoSave, ...formOptions }: PropsWithChildren<FormOptions<any> & {
19
- formRef?: RefObject<HTMLDivElement | null>;
20
- autoSave?: boolean;
21
- }>) => React.JSX.Element;
22
- export {};
23
- //# sourceMappingURL=FormContext.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FormContext.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/FormContext.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,EACZ,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAE,KAAK,aAAa,EAAY,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,KAAK,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAW,MAAM,aAAa,CAAC;AAE1E,KAAK,gBAAgB,CAAC,CAAC,SAAS,aAAa,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;AAIhE,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,aAAa,OAAK,gBAAgB,CAAC,CAAC,CAE5E,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAM,CAAC,MAAM,GAAG,MAAM,CAAC,EAAO,KAAG,GAI9D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IACtD,MAAM,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;CAClD,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAM,CAAC,MAAM,GAAG,MAAM,CAAC,EAAO,KAAG,mBAa9D,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,iDAK1B,iBAAiB,CAClB,WAAW,CAAC,GAAG,CAAC,GAAG;IACjB,OAAO,CAAC,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CACF,sBA0CA,CAAC"}