@dxos/react-ui-form 0.10.0 → 0.11.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 (182) hide show
  1. package/dist/lib/index.mjs +4064 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/lib/translations.mjs +77 -0
  4. package/dist/lib/translations.mjs.map +1 -0
  5. package/dist/lib/types.mjs +0 -0
  6. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +1 -0
  7. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Form/Card.stories.d.ts +88 -0
  9. package/dist/types/src/components/Form/Card.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/Form/Form.d.ts +7 -7
  11. package/dist/types/src/components/Form/Form.stories.d.ts +21 -0
  12. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.theme.d.ts +6 -0
  14. package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/FormControls.d.ts +7 -7
  16. package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/FormField/FormField.d.ts +1 -1
  18. package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormField/FormRow.d.ts +17 -3
  20. package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts +0 -6
  22. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
  24. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts +7 -0
  26. package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts +1 -0
  28. package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts +1 -0
  30. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts +1 -0
  33. package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts +1 -0
  35. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts +1 -0
  38. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +0 -11
  40. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts +1 -0
  42. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts +1 -0
  44. package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts.map +1 -1
  45. package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts +12 -0
  46. package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -1
  48. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts +1 -0
  49. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -1
  50. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts +1 -11
  51. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
  52. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts +1 -0
  53. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts +13 -0
  55. package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts.map +1 -0
  56. package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts +1 -0
  57. package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts.map +1 -1
  58. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +1 -13
  59. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
  60. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts +1 -0
  61. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts.map +1 -1
  62. package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts +15 -0
  63. package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts.map +1 -0
  64. package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts +1 -0
  65. package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts.map +1 -1
  66. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.d.ts.map +1 -1
  67. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts +1 -0
  68. package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts.map +1 -1
  69. package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts +1 -0
  70. package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts.map +1 -1
  71. package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts +1 -0
  72. package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts.map +1 -1
  73. package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts +1 -0
  74. package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts.map +1 -1
  75. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +1 -1
  76. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -1
  77. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +0 -27
  78. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
  79. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +1 -0
  80. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
  81. package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -0
  82. package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
  83. package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts +29 -0
  84. package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts.map +1 -0
  85. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +10 -1
  86. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
  87. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +2 -0
  88. package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -1
  89. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +8 -23
  90. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  91. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +1 -0
  92. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  93. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +1 -1
  94. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
  95. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +1 -0
  96. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
  97. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +1 -0
  98. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -1
  99. package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -1
  100. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -0
  101. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
  102. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +9 -8
  103. package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -1
  104. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +1 -0
  105. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  106. package/dist/types/src/hooks/useFormHandler.d.ts +6 -2
  107. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  108. package/dist/types/src/hooks/useFormHandler.test.d.ts +2 -0
  109. package/dist/types/src/hooks/useFormHandler.test.d.ts.map +1 -0
  110. package/dist/types/src/testing/TestLayout.d.ts +1 -1
  111. package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
  112. package/dist/types/src/translations.d.ts +1 -0
  113. package/dist/types/src/translations.d.ts.map +1 -1
  114. package/dist/types/src/types.d.ts +5 -0
  115. package/dist/types/src/types.d.ts.map +1 -1
  116. package/dist/types/src/util/index.d.ts +0 -1
  117. package/dist/types/src/util/index.d.ts.map +1 -1
  118. package/dist/types/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +39 -42
  120. package/src/components/Form/Card.stories.tsx +93 -0
  121. package/src/components/Form/Form.stories.tsx +95 -3
  122. package/src/components/Form/Form.theme.ts +14 -8
  123. package/src/components/Form/Form.tsx +2 -2
  124. package/src/components/Form/FormControls.tsx +32 -13
  125. package/src/components/Form/FormField/FormField.tsx +11 -3
  126. package/src/components/Form/FormField/FormRow.tsx +20 -1
  127. package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +3 -46
  128. package/src/components/Form/FormField/fields/ArrayField/default-value.ts +49 -0
  129. package/src/components/Form/FormField/fields/ArrayField/index.ts +2 -0
  130. package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +12 -2
  131. package/src/components/Form/FormField/fields/DateField/DateField.tsx +10 -3
  132. package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +1 -1
  133. package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +2 -12
  134. package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +1 -1
  135. package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +1 -36
  136. package/src/components/Form/FormField/fields/NumberField/index.ts +2 -0
  137. package/src/components/Form/FormField/fields/NumberField/numeric-constraints.ts +43 -0
  138. package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +1 -0
  139. package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +2 -2
  140. package/src/components/Form/FormField/fields/RefField/RefField.test.ts +1 -1
  141. package/src/components/Form/FormField/fields/RefField/RefField.tsx +3 -45
  142. package/src/components/Form/FormField/fields/RefField/find-ref-option.ts +56 -0
  143. package/src/components/Form/FormField/fields/RefField/index.ts +2 -0
  144. package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +2 -67
  145. package/src/components/Form/FormField/fields/SelectField/create-select-field.tsx +80 -0
  146. package/src/components/Form/FormField/fields/SelectField/index.ts +2 -0
  147. package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +2 -1
  148. package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +11 -9
  149. package/src/components/Form/FormLayout/FormLayout.test.ts +1 -1
  150. package/src/components/Form/FormLayout/FormLayout.tsx +1 -86
  151. package/src/components/Form/FormLayout/index.ts +2 -0
  152. package/src/components/Form/FormLayout/resolve-layout-field.ts +98 -0
  153. package/src/components/ObjectForm/ObjectForm.stories.tsx +93 -3
  154. package/src/components/ObjectForm/ObjectForm.tsx +22 -4
  155. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +1 -1
  156. package/src/components/ObjectPicker/ObjectPicker.tsx +3 -3
  157. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
  158. package/src/components/ObjectProperties/ObjectProperties.tsx +7 -1
  159. package/src/components/ObjectTree/ObjectTree.stories.tsx +1 -1
  160. package/src/components/RefEditor/RefEditor.stories.tsx +9 -1
  161. package/src/components/RefEditor/RefEditor.tsx +42 -6
  162. package/src/components/RefEditor/ref-editor-extension.ts +168 -91
  163. package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -1
  164. package/src/components/ViewEditor/ViewEditor.tsx +9 -9
  165. package/src/hooks/useFormHandler.test.ts +85 -0
  166. package/src/hooks/useFormHandler.ts +118 -31
  167. package/src/translations.ts +1 -0
  168. package/src/types.ts +5 -0
  169. package/src/util/index.ts +0 -1
  170. package/dist/lib/browser/index.mjs +0 -4675
  171. package/dist/lib/browser/index.mjs.map +0 -7
  172. package/dist/lib/browser/meta.json +0 -1
  173. package/dist/lib/browser/translations.mjs +0 -87
  174. package/dist/lib/browser/translations.mjs.map +0 -7
  175. package/dist/lib/node-esm/index.mjs +0 -4677
  176. package/dist/lib/node-esm/index.mjs.map +0 -7
  177. package/dist/lib/node-esm/meta.json +0 -1
  178. package/dist/lib/node-esm/translations.mjs +0 -89
  179. package/dist/lib/node-esm/translations.mjs.map +0 -7
  180. package/dist/types/src/util/formatting.d.ts +0 -15
  181. package/dist/types/src/util/formatting.d.ts.map +0 -1
  182. package/src/util/formatting.ts +0 -171
@@ -2,11 +2,13 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useControllableState } from '@radix-ui/react-use-controllable-state';
5
+ import * as Equal from 'effect/Equal';
6
6
  import type * as Schema from 'effect/Schema';
7
7
  import type * as SchemaAST from 'effect/SchemaAST';
8
+ import * as Utils from 'effect/Utils';
8
9
  import { useCallback, useEffect, useMemo, useState } from 'react';
9
10
 
11
+ import { Ref } from '@dxos/echo';
10
12
  import { type AnyProperties } from '@dxos/echo/internal';
11
13
  import { SchemaEx } from '@dxos/effect';
12
14
  import { log } from '@dxos/log';
@@ -26,12 +28,16 @@ export interface FormHandlerProps<T extends AnyProperties> {
26
28
  schema?: Schema.Schema<T, any>;
27
29
 
28
30
  /**
29
- * Values for the form if controlled.
31
+ * Source values. Fields the user is not editing reflect this value; in-progress edits (including intermediate
32
+ * invalid values) are held locally and never snap back until they are committed and the source catches up. Persist
33
+ * committed values from `onValuesChanged`/`onSave`. The form re-renders when a new value is passed, so to reflect
34
+ * external/remote mutations pass a reactive source (e.g. a `useObject` snapshot that changes on mutation) rather
35
+ * than a raw live object — a render-time read of an ECHO object does not itself subscribe to it.
30
36
  */
31
37
  values?: Partial<T>;
32
38
 
33
39
  /**
34
- * Default values for the form which will be used if values is not provided.
40
+ * Seeds an uncontrolled form once at mount when `values` is not provided; not reconciled thereafter.
35
41
  */
36
42
  defaultValues?: Partial<T>;
37
43
 
@@ -127,23 +133,21 @@ export const useFormHandler = <T extends AnyProperties>({
127
133
  const [errors, setErrors] = useState<Record<SchemaEx.JsonPath, string>>({});
128
134
  const [saving, setSaving] = useState(false);
129
135
  const defaultValues = useDefaultValue<Partial<T>>(defaultValuesProp, () => ({}));
130
- const [values$, setValues] = useControllableState<Partial<T>>({
131
- prop: valuesProp,
132
- defaultProp: defaultValues,
133
- onChange: () => {
134
- // Only reset when controlled: parent passed new values.
135
- // Uncontrolled updates come from setValues in onValueChange.
136
- if (valuesProp !== undefined) {
137
- setValues(valuesProp ?? defaultValues);
138
- setChanged({});
139
- setTouched({});
140
- setErrors({});
141
- setSaving(false);
142
- }
143
- },
144
- });
145
- // TODO(wittjosiah): Upgrade @radix-ui/react-use-controllable-state.
146
- const values = values$ as Partial<T>;
136
+
137
+ // The source the form reads from for every field the user is not actively editing. The form is a pure function of
138
+ // this value and re-renders when the parent passes a new one; to reflect external/remote mutations the parent must
139
+ // supply a reactive source (e.g. a `useObject` snapshot that changes on mutation). `defaultValues` seeds an
140
+ // uncontrolled form once when no `values` are provided.
141
+ const source = valuesProp ?? defaultValues;
142
+
143
+ // Sparse local edits keyed by json-path. A path stays here — holding even an intermediate invalid value, so it
144
+ // never snaps back — until the source catches up to it (typically once a valid edit has been persisted and echoed
145
+ // back through `values`). Un-edited paths are never buffered, so they always reflect the current source.
146
+ const [overrides, setOverrides] = useState<Record<SchemaEx.JsonPath, unknown>>({});
147
+
148
+ // Merged view (source overlaid with local edits) for whole-form consumers: validation, save, and context/debug.
149
+ // Computed each render (not memoized) so it stays fresh when the source value changes.
150
+ const values = applyOverrides(source, overrides);
147
151
 
148
152
  // Validate.
149
153
  const validate = useCallback(
@@ -176,6 +180,22 @@ export const useFormHandler = <T extends AnyProperties>({
176
180
  }
177
181
  }, [validate, values, schemaChanged]);
178
182
 
183
+ // Drop an override once the source has caught up to it (the committed value has been echoed back), so the field
184
+ // returns to reflecting the source. Edits the source has not yet reflected — including intermediate invalid values,
185
+ // and in-progress edits to a field that changed remotely — are kept, so the form never snaps back. Runs on every
186
+ // render (a new source value re-renders the form), reconciling against whatever the source currently holds.
187
+ useEffect(() => {
188
+ const reconciled = (Object.keys(overrides) as SchemaEx.JsonPath[]).filter((path) =>
189
+ valuesEqual(SchemaEx.getValue(source, path), overrides[path]),
190
+ );
191
+ if (reconciled.length === 0) {
192
+ return;
193
+ }
194
+ setOverrides((prev) => omitPaths(prev, reconciled));
195
+ setChanged((prev) => omitPaths(prev, reconciled));
196
+ setTouched((prev) => omitPaths(prev, reconciled));
197
+ });
198
+
179
199
  const isValid = useMemo(() => {
180
200
  return Object.keys(errors).length === 0;
181
201
  }, [errors]);
@@ -245,9 +265,14 @@ export const useFormHandler = <T extends AnyProperties>({
245
265
 
246
266
  const getValue = useCallback<FormHandler<T>['getValue']>(
247
267
  (path) => {
248
- return SchemaEx.getValue(values, SchemaEx.createJsonPath(path));
268
+ const jsonPath = SchemaEx.createJsonPath(path);
269
+ if (Object.prototype.hasOwnProperty.call(overrides, jsonPath)) {
270
+ return overrides[jsonPath] as any;
271
+ }
272
+ // Un-edited fields reflect the current source value.
273
+ return SchemaEx.getValue(source, jsonPath);
249
274
  },
250
- [values],
275
+ [source, overrides],
251
276
  );
252
277
 
253
278
  const onValueChange = useCallback<FormHandler<T>['onValueChange']>(
@@ -266,25 +291,25 @@ export const useFormHandler = <T extends AnyProperties>({
266
291
  parsedValue = undefined;
267
292
  }
268
293
 
269
- // Create merged values for validation and callback. Form never mutates; parent applies via onValuesChanged.
270
- const newValues = mergeAtPath(values, pathArray as (string | number)[], parsedValue) as Partial<T>;
294
+ // Hold the edit locally as an override (including invalid values, so it never snaps back); un-edited paths keep
295
+ // tracking the live source. The prune effect drops the override once the source catches up.
296
+ const newOverrides = { ...overrides, [jsonPath]: parsedValue };
297
+ setOverrides(newOverrides);
271
298
 
272
299
  // TODO(burdon): Check value has changed from original.
273
300
  const newChanged = { [jsonPath]: true };
274
301
  setChanged((prev) => ({ ...prev, ...newChanged }));
275
302
 
303
+ // Merged values for validation and the callback. Form never mutates the source; the parent persists.
304
+ const newValues = applyOverrides(source, newOverrides);
305
+
276
306
  // Validate.
277
307
  const isValid = validate(newValues);
278
308
 
279
- // Notify parent; parent is responsible for applying the change.
309
+ // Notify parent; parent is responsible for persisting the change (typically once valid).
280
310
  onValuesChanged?.(newValues, { isValid, changed: newChanged });
281
-
282
- // For uncontrolled mode, update internal state. Controlled mode relies on parent to pass new values.
283
- if (valuesProp === undefined) {
284
- setValues(newValues);
285
- }
286
311
  },
287
- [values, validate, onValuesChanged, valuesProp],
312
+ [source, overrides, validate, onValuesChanged],
288
313
  );
289
314
 
290
315
  const onBlur = useCallback(
@@ -367,6 +392,68 @@ const mergeAtPath = (obj: any, path: readonly (string | number)[], value: any):
367
392
  return { ...obj, [head]: child };
368
393
  };
369
394
 
395
+ /** Overlays sparse json-path edits onto a base value without mutating it; un-edited paths keep the base reference. */
396
+ const applyOverrides = <T>(base: Partial<T>, overrides: Record<SchemaEx.JsonPath, unknown>): Partial<T> => {
397
+ let result: Partial<T> = base;
398
+ for (const path of Object.keys(overrides) as SchemaEx.JsonPath[]) {
399
+ result = mergeAtPath(result, SchemaEx.splitJsonPath(path), overrides[path]) as Partial<T>;
400
+ }
401
+ return result;
402
+ };
403
+
404
+ // Copied from `@dxos/echo` (internal `Obj.valuesEqual`): references compare by target URI, arrays and plain
405
+ // object-shaped property bags (excluding `id`) compare recursively, and leaves fall back to Effect `Equal.equals`
406
+ // inside a structural region. Effect's `Schema.equivalence` is not a safe substitute — it returns false-positive
407
+ // equality for dynamic/union/ref-array schemas, which would silently prune edits.
408
+ // TODO(wittjosiah): Factor out into a shared util rather than duplicating echo's internal implementation.
409
+ const valuesEqual = (left: unknown, right: unknown): boolean => {
410
+ if (left === right) {
411
+ return true;
412
+ }
413
+ if (left === null || right === null) {
414
+ return left === right;
415
+ }
416
+ if (typeof left !== 'object' || typeof right !== 'object') {
417
+ return Utils.structuralRegion(() => Equal.equals(left, right));
418
+ }
419
+ if (Ref.isRef(left) && Ref.isRef(right)) {
420
+ return left.uri === right.uri;
421
+ }
422
+ if (Ref.isRef(left) || Ref.isRef(right)) {
423
+ return false;
424
+ }
425
+ if (Array.isArray(left) && Array.isArray(right)) {
426
+ if (left.length !== right.length) {
427
+ return false;
428
+ }
429
+ return left.every((value, index) => valuesEqual(value, right[index]));
430
+ }
431
+ if (Array.isArray(left) || Array.isArray(right)) {
432
+ return false;
433
+ }
434
+ const leftRecord = left as Record<string, unknown>;
435
+ const rightRecord = right as Record<string, unknown>;
436
+ const keys = new Set([
437
+ ...Object.keys(leftRecord).filter((key) => key !== 'id'),
438
+ ...Object.keys(rightRecord).filter((key) => key !== 'id'),
439
+ ]);
440
+ for (const key of keys) {
441
+ if (!valuesEqual(leftRecord[key], rightRecord[key])) {
442
+ return false;
443
+ }
444
+ }
445
+ return true;
446
+ };
447
+
448
+ /** Returns a copy of `record` without the given json-path keys. */
449
+ const omitPaths = <V>(
450
+ record: Record<SchemaEx.JsonPath, V>,
451
+ paths: SchemaEx.JsonPath[],
452
+ ): Record<SchemaEx.JsonPath, V> => {
453
+ const omit = new Set<string>(paths);
454
+ return Object.fromEntries(Object.entries(record).filter(([key]) => !omit.has(key))) as Record<SchemaEx.JsonPath, V>;
455
+ };
456
+
370
457
  const flatMap = (errors: ValidationError[]) => {
371
458
  return errors.reduce(
372
459
  (result, { path, message }) => {
@@ -63,6 +63,7 @@ export const translations = [
63
63
  'format.json.label': 'JSON',
64
64
  'format.lnglat.label': 'Geopoint',
65
65
  'format.markdown.label': 'Markdown',
66
+ 'format.password.label': 'Password',
66
67
  'format.percent.label': 'Percent',
67
68
  'format.ref.label': 'Reference',
68
69
  'format.regex.label': 'RegExp',
package/src/types.ts CHANGED
@@ -158,6 +158,11 @@ export type RefFieldDataProps = {
158
158
  export type FormFieldOptions = {
159
159
  db?: Database.Database;
160
160
  readonly?: boolean;
161
+ /**
162
+ * When `readonly`, omit fields whose value is empty (an empty read-only row is just noise).
163
+ * Defaults to `true`; set `false` to keep every schema field visible as a static row.
164
+ */
165
+ hideEmpty?: boolean;
161
166
  layout?: FormPresentation;
162
167
  projection?: ProjectionModel;
163
168
  fieldMap?: FormFieldMap;
package/src/util/index.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './field';
6
- export * from './formatting';
7
6
  export * from './omit';
8
7
  export * from './properties';
9
8
  export * from './refs';