@dxos/react-ui-form 0.9.1-main.c7dcc2e112 → 0.9.1-staging.ee54ba693a

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 (211) hide show
  1. package/dist/lib/browser/index.mjs +1883 -1539
  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 +1883 -1539
  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/annotations.d.ts +47 -0
  8. package/dist/types/src/annotations.d.ts.map +1 -0
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/Form/Form.d.ts +12 -4
  11. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.stories.d.ts +46 -65
  13. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.theme.d.ts +252 -0
  15. package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -0
  16. package/dist/types/src/components/Form/Form.theme.test.d.ts +2 -0
  17. package/dist/types/src/components/Form/Form.theme.test.d.ts.map +1 -0
  18. package/dist/types/src/components/Form/FormControls.d.ts +17 -8
  19. package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField/FormField.d.ts +16 -2
  21. package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts +27 -0
  23. package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/FormField/FormRow.d.ts +85 -0
  25. package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/FormField/{FormFieldWrapper.stories.d.ts → FormRow.stories.d.ts} +4 -1
  27. package/dist/types/src/components/Form/FormField/FormRow.stories.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
  30. package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts +12 -0
  32. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts +2 -0
  34. package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts +12 -0
  36. package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts +2 -0
  38. package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.d.ts.map +1 -1
  40. package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts +12 -0
  41. package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts +2 -0
  43. package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts +1 -1
  45. package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts.map +1 -1
  46. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts +1 -1
  47. package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
  48. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +11 -0
  49. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
  50. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts +2 -0
  51. package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts +4 -0
  53. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -0
  54. package/dist/types/src/components/{Settings/Settings.stories.d.ts → Form/FormField/fields/PasswordField/PasswordField.stories.d.ts} +1 -2
  55. package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -0
  56. package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts +2 -0
  57. package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts.map +1 -0
  58. package/dist/types/src/components/Form/FormField/fields/RefField/InlineRefField.d.ts.map +1 -1
  59. package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
  60. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +18 -2
  61. package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
  62. package/dist/types/src/components/Form/FormField/fields/index.d.ts +4 -0
  63. package/dist/types/src/components/Form/FormField/fields/index.d.ts.map +1 -1
  64. package/dist/types/src/components/Form/FormField/index.d.ts +4 -3
  65. package/dist/types/src/components/Form/FormField/index.d.ts.map +1 -1
  66. package/dist/types/src/components/Form/FormField/presentation.d.ts +22 -0
  67. package/dist/types/src/components/Form/FormField/presentation.d.ts.map +1 -0
  68. package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts +8 -5
  69. package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts.map +1 -1
  70. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +22 -0
  71. package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -0
  72. package/dist/types/src/components/Form/FormFieldSet/index.d.ts +1 -0
  73. package/dist/types/src/components/Form/FormFieldSet/index.d.ts.map +1 -1
  74. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +2 -0
  75. package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
  76. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +5 -5
  77. package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
  78. package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -1
  79. package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
  80. package/dist/types/src/components/Form/meta-tags.d.ts +1 -12
  81. package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -1
  82. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  83. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +2 -1
  84. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
  85. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -2
  86. package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
  87. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  88. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -2
  89. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  90. package/dist/types/src/components/index.d.ts +0 -1
  91. package/dist/types/src/components/index.d.ts.map +1 -1
  92. package/dist/types/src/hooks/index.d.ts +1 -0
  93. package/dist/types/src/hooks/index.d.ts.map +1 -1
  94. package/dist/types/src/hooks/useAsyncFieldEffect.d.ts +24 -0
  95. package/dist/types/src/hooks/useAsyncFieldEffect.d.ts.map +1 -0
  96. package/dist/types/src/hooks/useFormContext.d.ts +8 -3
  97. package/dist/types/src/hooks/useFormContext.d.ts.map +1 -1
  98. package/dist/types/src/hooks/useFormHandler.d.ts +8 -2
  99. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  100. package/dist/types/src/index.d.ts +1 -0
  101. package/dist/types/src/index.d.ts.map +1 -1
  102. package/dist/types/src/testing/TestLayout.d.ts +1 -1
  103. package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
  104. package/dist/types/src/testing/schema.d.ts +13 -16
  105. package/dist/types/src/testing/schema.d.ts.map +1 -1
  106. package/dist/types/src/types.d.ts +50 -1
  107. package/dist/types/src/types.d.ts.map +1 -1
  108. package/dist/types/src/util/index.d.ts +0 -1
  109. package/dist/types/src/util/index.d.ts.map +1 -1
  110. package/dist/types/src/util/omit.d.ts +1 -1
  111. package/dist/types/src/util/omit.d.ts.map +1 -1
  112. package/dist/types/tsconfig.tsbuildinfo +1 -1
  113. package/package.json +41 -40
  114. package/src/annotations.ts +63 -0
  115. package/src/components/FieldEditor/FieldEditor.tsx +5 -2
  116. package/src/components/Form/DESIGN.md +24 -0
  117. package/src/components/Form/Form.stories.tsx +257 -13
  118. package/src/components/Form/Form.theme.test.ts +33 -0
  119. package/src/components/Form/Form.theme.ts +86 -0
  120. package/src/components/Form/Form.tsx +15 -9
  121. package/src/components/Form/FormControls.tsx +94 -37
  122. package/src/components/Form/FormField/FormField.tsx +83 -32
  123. package/src/components/Form/FormField/FormFieldHeader.tsx +68 -0
  124. package/src/components/Form/FormField/{FormFieldWrapper.stories.tsx → FormRow.stories.tsx} +34 -9
  125. package/src/components/Form/FormField/FormRow.tsx +298 -0
  126. package/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.tsx +25 -9
  127. package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +83 -24
  128. package/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.tsx +59 -0
  129. package/src/components/Form/FormField/fields/AsyncSelectField/index.ts +5 -0
  130. package/src/components/Form/FormField/fields/AutofillField/AutofillField.tsx +67 -0
  131. package/src/components/Form/FormField/fields/AutofillField/index.ts +5 -0
  132. package/src/components/Form/FormField/fields/BooleanField/BooleanField.tsx +5 -6
  133. package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +104 -0
  134. package/src/components/Form/FormField/fields/ComboboxField/index.ts +5 -0
  135. package/src/components/Form/FormField/fields/DateField/DateField.tsx +9 -9
  136. package/src/components/Form/FormField/fields/GeoPointField/GeoPointField.tsx +17 -20
  137. package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +2 -2
  138. package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +39 -40
  139. package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +29 -0
  140. package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +60 -21
  141. package/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.tsx +53 -0
  142. package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +41 -0
  143. package/src/components/Form/FormField/fields/PasswordField/index.ts +5 -0
  144. package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +16 -10
  145. package/src/components/Form/FormField/fields/RefField/RefField.test.ts +8 -0
  146. package/src/components/Form/FormField/fields/RefField/RefField.tsx +50 -23
  147. package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +104 -47
  148. package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +3 -3
  149. package/src/components/Form/FormField/fields/TextAreaField/TextAreaField.tsx +3 -3
  150. package/src/components/Form/FormField/fields/TextField/TextField.tsx +3 -3
  151. package/src/components/Form/FormField/fields/TupleField/TupleField.tsx +1 -1
  152. package/src/components/Form/FormField/fields/index.ts +4 -0
  153. package/src/components/Form/FormField/index.ts +5 -3
  154. package/src/components/Form/FormField/presentation.tsx +32 -0
  155. package/src/components/Form/FormFieldSet/FormFieldSet.tsx +49 -70
  156. package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +93 -0
  157. package/src/components/Form/FormFieldSet/index.ts +1 -0
  158. package/src/components/Form/FormLayout/FormLayout.stories.tsx +7 -5
  159. package/src/components/Form/FormLayout/FormLayout.tsx +24 -14
  160. package/src/components/Form/FormLayout/index.ts +1 -1
  161. package/src/components/Form/meta-tags.test.ts +1 -1
  162. package/src/components/ObjectForm/ObjectForm.tsx +2 -2
  163. package/src/components/ObjectPicker/ObjectPicker.tsx +1 -0
  164. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
  165. package/src/components/ObjectProperties/ObjectProperties.tsx +12 -8
  166. package/src/components/RefEditor/RefEditor.stories.tsx +2 -2
  167. package/src/components/RefEditor/RefEditor.tsx +1 -1
  168. package/src/components/ViewEditor/ViewEditor.stories.tsx +2 -2
  169. package/src/components/ViewEditor/ViewEditor.test.tsx +8 -2
  170. package/src/components/ViewEditor/ViewEditor.tsx +3 -3
  171. package/src/components/index.ts +0 -1
  172. package/src/hooks/index.ts +1 -0
  173. package/src/hooks/useAsyncFieldEffect.ts +81 -0
  174. package/src/hooks/useFormContext.ts +6 -3
  175. package/src/hooks/useFormHandler.ts +11 -6
  176. package/src/index.ts +1 -0
  177. package/src/testing/TestLayout.tsx +15 -12
  178. package/src/testing/schema.ts +43 -43
  179. package/src/types.ts +53 -1
  180. package/src/util/index.ts +0 -1
  181. package/src/util/omit.ts +25 -11
  182. package/src/util/properties.test.ts +110 -28
  183. package/src/util/refs.ts +1 -1
  184. package/dist/types/src/components/Form/FormField/FieldHeader.d.ts +0 -19
  185. package/dist/types/src/components/Form/FormField/FieldHeader.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts +0 -47
  187. package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/FormField/FormFieldWrapper.stories.d.ts.map +0 -1
  189. package/dist/types/src/components/Settings/Settings.d.ts +0 -51
  190. package/dist/types/src/components/Settings/Settings.d.ts.map +0 -1
  191. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +0 -1
  192. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +0 -43
  193. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +0 -1
  194. package/dist/types/src/components/Settings/SettingsItem.d.ts +0 -16
  195. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +0 -1
  196. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +0 -18
  197. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +0 -1
  198. package/dist/types/src/components/Settings/index.d.ts +0 -4
  199. package/dist/types/src/components/Settings/index.d.ts.map +0 -1
  200. package/dist/types/src/util/field-type.d.ts +0 -19
  201. package/dist/types/src/util/field-type.d.ts.map +0 -1
  202. package/src/components/Form/FormField/FieldHeader.tsx +0 -36
  203. package/src/components/Form/FormField/FormFieldWrapper.tsx +0 -179
  204. package/src/components/Settings/AUDIT.md +0 -51
  205. package/src/components/Settings/Settings.stories.tsx +0 -104
  206. package/src/components/Settings/Settings.tsx +0 -81
  207. package/src/components/Settings/SettingsFieldSet.tsx +0 -257
  208. package/src/components/Settings/SettingsItem.tsx +0 -58
  209. package/src/components/Settings/SettingsSelectField.tsx +0 -59
  210. package/src/components/Settings/index.ts +0 -7
  211. package/src/util/field-type.ts +0 -60
@@ -15,15 +15,17 @@ import { HuePicker } from '@dxos/react-ui-pickers';
15
15
  import { FactoryAnnotation } from '@dxos/schema';
16
16
 
17
17
  import { translationKey } from '#translations';
18
- import { type FormFieldMap } from '#types';
18
+ import { type FormFieldMap, type RefFieldDataProps } from '#types';
19
19
 
20
20
  import { Form, META_TAGS_KEY, withMetaTags } from '../Form';
21
21
 
22
- export type ObjectPropertiesProps = PropsWithChildren<{ object: Obj.Unknown }>;
22
+ export type ObjectPropertiesProps = PropsWithChildren<
23
+ { object: Obj.Unknown } & Pick<RefFieldDataProps, 'getCreateDefaults' | 'resolveCreateEntry'>
24
+ >;
23
25
 
24
26
  // TODO(wittjosiah): Reconcile w/ ObjectForm.
25
27
  export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps>(
26
- ({ children, object, ...props }, forwardedRef) => {
28
+ ({ children, object, getCreateDefaults, resolveCreateEntry, ...props }, forwardedRef) => {
27
29
  const db = Obj.getDatabase(object);
28
30
  const meta = Obj.getMeta(object);
29
31
  // `meta.tags` already holds `Ref<Tag>`s (materialized by the database handler).
@@ -111,6 +113,7 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
111
113
 
112
114
  return (
113
115
  <Form.Root
116
+ db={db}
114
117
  schema={formSchema}
115
118
  defaultValues={values as any}
116
119
  createTypename={Type.getTypename(Tag.Tag)}
@@ -118,14 +121,15 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
118
121
  createOptionLabel={['add-tag.label', { ns: translationKey }]}
119
122
  createInitialValuePath='label'
120
123
  createFieldMap={createFieldMap}
121
- db={db}
122
124
  onValuesChanged={handleChange}
123
125
  onCreate={handleCreate}
126
+ getCreateDefaults={getCreateDefaults}
127
+ resolveCreateEntry={resolveCreateEntry}
124
128
  >
125
- <Form.Viewport {...composableProps(props)} ref={forwardedRef}>
129
+ <Form.Viewport {...composableProps(props)} scroll ref={forwardedRef}>
126
130
  <Form.Content>
127
131
  <Form.FieldSet />
128
- {children}
132
+ <Form.Section>{children}</Form.Section>
129
133
  </Form.Content>
130
134
  </Form.Viewport>
131
135
  </Form.Root>
@@ -134,12 +138,12 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
134
138
  );
135
139
 
136
140
  const createFieldMap: FormFieldMap = {
137
- hue: ({ type, label, layout, getValue, onValueChange }) => {
141
+ hue: ({ type, label, presentation, getValue, onValueChange }) => {
138
142
  const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
139
143
  const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
140
144
  return (
141
145
  <>
142
- {layout !== 'inline' && <Form.Label label={label} />}
146
+ {presentation !== 'inline' && <Form.Label label={label} />}
143
147
  <HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
144
148
  </>
145
149
  );
@@ -69,11 +69,11 @@ const PeopleGrid = ({ db }: { db?: Database.Database }) => {
69
69
  );
70
70
  };
71
71
 
72
- type StoryProps = Partial<Pick<RefEditorProps, 'value' | 'mode' | 'activateOnTyping' | 'autoFocus'>>;
72
+ type StoryArgs = Partial<Pick<RefEditorProps, 'value' | 'mode' | 'activateOnTyping' | 'autoFocus'>>;
73
73
 
74
74
  // NOTE: The Person config (type/match/getValues) is supplied in render, not args — Storybook's
75
75
  // arg traversal attempts to mutate the proxied ECHO schema and crashes.
76
- const DefaultStory = ({ value: initialValue, ...args }: StoryProps) => {
76
+ const DefaultStory = ({ value: initialValue, ...args }: StoryArgs) => {
77
77
  const { space } = useClientStory();
78
78
  const [value, setValue] = useState(initialValue ?? '');
79
79
 
@@ -246,7 +246,7 @@ export const RefEditor = forwardRef<EditorController, RefEditorProps>(
246
246
  >
247
247
  <Editor.View
248
248
  // TOOD(burdon): Use same style as react-ui Input.
249
- classNames='border border-separator rounded-xs px-2'
249
+ classNames='border border-input-separator rounded-xs px-2'
250
250
  {...props}
251
251
  initialValue={value}
252
252
  selectionEnd
@@ -35,9 +35,9 @@ export type ViewEditorDebugObjects = {
35
35
  projection: ProjectionModel;
36
36
  };
37
37
 
38
- type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
38
+ type StoryArgs = Pick<ViewEditorProps, 'readonly' | 'mode'>;
39
39
 
40
- const DefaultStory = (props: DefaultStoryProps) => {
40
+ const DefaultStory = (props: StoryArgs) => {
41
41
  const { space } = useClientStory();
42
42
  const [type, setType] = useState<Type.AnyEntity>();
43
43
  const [view, setView] = useState<View.View>();
@@ -138,11 +138,17 @@ describe('ViewEditor', () => {
138
138
  expect(addedPropertyProjection!.props.property).toBe('added_property');
139
139
  });
140
140
 
141
- // TODO(mykola): Fix flaky test.
142
- test('delete property', { retry: 2 }, async () => {
141
+ test('delete property', async () => {
143
142
  await Default.run();
144
143
  await waitForViewEditor();
145
144
 
145
+ // Wait for the field list to render — useObject(view) inside ViewEditor resolves
146
+ // asynchronously after the outer story's type/view state is set, so the debug
147
+ // element can appear before the list items are in the DOM.
148
+ await waitFor(() => {
149
+ expect(screen.getByText('name')).toBeInTheDocument();
150
+ });
151
+
146
152
  // Find the delete button for the 'name' property.
147
153
  const nameProperty = screen.getByText('name');
148
154
  const propertyRow = nameProperty.closest('[role="listitem"]');
@@ -39,10 +39,10 @@ import {
39
39
  import { mx } from '@dxos/ui-theme';
40
40
 
41
41
  import { translationKey } from '#translations';
42
- import { type FormFieldRenderer, type FormFieldRendererProps, type FormFieldMap } from '#types';
42
+ import { type FormFieldMap, type FormFieldRenderer, type FormFieldRendererProps } from '#types';
43
43
 
44
44
  import { FieldEditor } from '../FieldEditor';
45
- import { CompactIconButton, FieldHeader, Form, FormFieldLabel, type FormRootProps } from '../Form';
45
+ import { CompactIconButton, Form, FormFieldHeader, FormFieldLabel, type FormRootProps } from '../Form';
46
46
 
47
47
  export type ViewEditorProps = ThemedClassName<
48
48
  {
@@ -352,7 +352,7 @@ const FieldList = ({ type, view, registry, readonly, showHeading = false, onDele
352
352
  >
353
353
  {({ items: fields }) => (
354
354
  <>
355
- <FieldHeader
355
+ <FormFieldHeader
356
356
  label={t('fields.label')}
357
357
  readonly={readonly}
358
358
  add={{
@@ -9,5 +9,4 @@ export * from './ObjectPicker';
9
9
  export * from './ObjectProperties';
10
10
  export * from './ObjectTree';
11
11
  export * from './RefEditor';
12
- export * from './Settings';
13
12
  export * from './ViewEditor';
@@ -2,6 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ export * from './useAsyncFieldEffect';
5
6
  export * from './useFormContext';
6
7
  export * from './useFormHandler';
7
8
  export * from './useKeyHandler';
@@ -0,0 +1,81 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Cause from 'effect/Cause';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Exit from 'effect/Exit';
8
+ import { useEffect, useRef, useState } from 'react';
9
+
10
+ import { type AnyProperties } from '@dxos/echo/internal';
11
+
12
+ /** Picks the declared dependency fields from the form values — the subset a dynamic field subscribes to. */
13
+ export const pickValues = (values: AnyProperties | undefined, deps: readonly string[]): Record<string, unknown> => {
14
+ const subset: Record<string, unknown> = {};
15
+ for (const dep of deps) {
16
+ subset[dep] = values?.[dep];
17
+ }
18
+ return subset;
19
+ };
20
+
21
+ export type AsyncFieldEffectState<A> = {
22
+ loading: boolean;
23
+ error: unknown;
24
+ data: A | undefined;
25
+ };
26
+
27
+ export type UseAsyncFieldEffectOptions = {
28
+ /** Debounce applied before running, so per-keystroke value changes coalesce into one run. */
29
+ debounceMs?: number;
30
+ /** When false, the effect is not run and prior data/error is cleared. */
31
+ enabled?: boolean;
32
+ };
33
+
34
+ /**
35
+ * Runs a self-contained Effect (`R = never`) whenever `key` changes, debounced and abortable. Used by
36
+ * dynamic form fields to load select options, derive values, or validate against an external source from
37
+ * the current form values. The latest `factory` is always used, but a run is only (re)triggered when
38
+ * `key` changes — derive `key` from the inputs the factory actually reads. Re-running interrupts the
39
+ * prior run (cancelling any in-flight request).
40
+ */
41
+ export const useAsyncFieldEffect = <A>(
42
+ factory: () => Effect.Effect<A, unknown>,
43
+ key: string,
44
+ { debounceMs = 400, enabled = true }: UseAsyncFieldEffectOptions = {},
45
+ ): AsyncFieldEffectState<A> => {
46
+ const factoryRef = useRef(factory);
47
+ factoryRef.current = factory;
48
+ const [state, setState] = useState<AsyncFieldEffectState<A>>({ loading: false, error: undefined, data: undefined });
49
+
50
+ useEffect(() => {
51
+ if (!enabled) {
52
+ setState({ loading: false, error: undefined, data: undefined });
53
+ return;
54
+ }
55
+
56
+ let cancel: (() => void) | undefined;
57
+ const timer = setTimeout(() => {
58
+ setState((prev) => ({ ...prev, loading: true, error: undefined }));
59
+ cancel = Effect.runCallback(factoryRef.current(), {
60
+ onExit: (exit: Exit.Exit<A, unknown>) => {
61
+ Exit.match(exit, {
62
+ onSuccess: (data) => setState({ loading: false, error: undefined, data }),
63
+ onFailure: (cause) => {
64
+ // A superseding run interrupts this one; ignore the resulting interruption.
65
+ if (!Cause.isInterruptedOnly(cause)) {
66
+ setState((prev) => ({ loading: false, error: Cause.squash(cause), data: prev.data }));
67
+ }
68
+ },
69
+ });
70
+ },
71
+ });
72
+ }, debounceMs);
73
+
74
+ return () => {
75
+ clearTimeout(timer);
76
+ cancel?.();
77
+ };
78
+ }, [key, enabled, debounceMs]);
79
+
80
+ return state;
81
+ };
@@ -9,8 +9,9 @@ import { useEffect, useMemo, useRef } from 'react';
9
9
  import { type AnyProperties } from '@dxos/echo/internal';
10
10
  import { SchemaEx } from '@dxos/effect';
11
11
 
12
- import { type CreateOptions, type FormFieldOptions, type FormFieldStateProps } from '#types';
12
+ import { type FieldContext, type FormFieldStateProps } from '#types';
13
13
 
14
+ import { type FormVariant } from '../components/Form/Form.theme';
14
15
  import { type FormHandler } from './useFormHandler';
15
16
 
16
17
  //
@@ -27,8 +28,10 @@ export type FormContextValue<T extends AnyProperties = any> = {
27
28
  * Testing.
28
29
  */
29
30
  testId?: string;
30
- } & FormFieldOptions &
31
- Pick<CreateOptions, 'createFieldMap'>;
31
+
32
+ /** Visual variant applied across the form's parts (see Form.theme). */
33
+ variant?: FormVariant;
34
+ } & FieldContext;
32
35
 
33
36
  export const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
34
37
 
@@ -77,17 +77,21 @@ export type FormHandler<T extends AnyProperties> = Pick<FormHandlerProps<T>, 'sc
77
77
  /** Initial values (which may not pass validation). */
78
78
  values: Partial<T>;
79
79
 
80
- /** Map of changed fields. */
80
+ /** Fields whose value has been edited (set by `onValueChange`). Gates autosave-on-blur. */
81
81
  changed: Record<SchemaEx.JsonPath, boolean>;
82
- // TODO(burdon): How is this different from above?
82
+ /** Fields the user has interacted with (set by `onBlur`). Gates error *visibility* — errors only surface once touched. */
83
83
  touched: Record<SchemaEx.JsonPath, boolean>;
84
84
 
85
85
  /** Map of error strings. */
86
86
  errors: Record<SchemaEx.JsonPath, string>;
87
87
 
88
- /** Whether the form can be saved (i.e., data is valid). */
88
+ /** Whether the values pass validation (no errors at all). */
89
89
  isValid: boolean;
90
- // TODO(burdon): Why is this needed separately from isValid?
90
+ /**
91
+ * Whether the submit affordance should be enabled. Stricter than `isValid`: it also requires no save
92
+ * in flight (`saving`) and only blocks on errors for *touched* fields, so a pristine form with
93
+ * not-yet-touched required fields can still show an enabled control.
94
+ */
91
95
  canSave: boolean;
92
96
 
93
97
  onSave: () => void;
@@ -325,6 +329,7 @@ export const useFormHandler = <T extends AnyProperties>({
325
329
  }),
326
330
  [
327
331
  schema,
332
+ autoSave,
328
333
  values,
329
334
  errors,
330
335
  touched,
@@ -335,8 +340,8 @@ export const useFormHandler = <T extends AnyProperties>({
335
340
  getValue,
336
341
  onBlur,
337
342
  onValueChange,
338
- onSave,
339
- onCancel,
343
+ handleSave,
344
+ handleCancel,
340
345
  ],
341
346
  );
342
347
  };
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './annotations';
5
6
  export * from './components';
6
7
  export * from './hooks';
7
8
  export * from './types';
@@ -7,23 +7,26 @@ import React, { type PropsWithChildren } from 'react';
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
8
  import { composableProps, slottable } from '@dxos/react-ui';
9
9
  import { Syntax } from '@dxos/react-ui-syntax-highlighter';
10
+ import { mx } from '@dxos/ui-theme';
10
11
 
11
- type TestLayoutProps = PropsWithChildren<{ json?: any }>;
12
+ type TestLayoutProps = PropsWithChildren<{ json?: unknown }>;
12
13
 
13
14
  export const TestLayout = ({ children, json }: TestLayoutProps) => {
14
15
  return (
15
- <div className='dx-container grid grid-cols-[1fr_1fr] p-4 gap-4'>
16
+ <div className={mx('dx-container grid grid-cols-1 p-4 gap-4', !!json && 'grid-cols-[1fr_1fr]')}>
16
17
  <TestPanel>{children}</TestPanel>
17
- <TestPanel>
18
- <Syntax.Root data={json}>
19
- <Syntax.Content>
20
- <Syntax.Filter />
21
- <Syntax.Viewport>
22
- <Syntax.Code testId='debug' classNames='text-sm' />
23
- </Syntax.Viewport>
24
- </Syntax.Content>
25
- </Syntax.Root>
26
- </TestPanel>
18
+ {!!json && (
19
+ <TestPanel>
20
+ <Syntax.Root data={json}>
21
+ <Syntax.Content>
22
+ <Syntax.Filter />
23
+ <Syntax.Viewport>
24
+ <Syntax.Code testId='debug' classNames='text-sm' />
25
+ </Syntax.Viewport>
26
+ </Syntax.Content>
27
+ </Syntax.Root>
28
+ </TestPanel>
29
+ )}
27
30
  </div>
28
31
  );
29
32
  };
@@ -12,48 +12,48 @@ import { Annotation, DXN, Format, Ref, Tag, Type } from '@dxos/echo';
12
12
  * structs, refs, ref arrays, enums, and the various formats.
13
13
  */
14
14
 
15
- export const Organization = Schema.Struct({
16
- name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
17
- }).pipe(Type.makeObject(DXN.make('com.example.type.organization', '0.1.0')));
15
+ export class Organization extends Type.makeObject<Organization>(DXN.make('com.example.type.organization', '0.1.0'))(
16
+ Schema.Struct({
17
+ name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
18
+ }),
19
+ ) {}
18
20
 
19
- export type Organization = Type.InstanceType<typeof Organization>;
20
-
21
- export const Person = Schema.Struct({
22
- name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
23
- ignore: Schema.String.pipe(Annotation.FormInputAnnotation.set(false), Schema.optional),
24
- active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
25
- address: Schema.optional(
26
- Schema.Struct({
27
- street: Schema.String,
28
- city: Schema.String,
29
- // TODO(burdon): Constrain input control.
30
- state: Schema.String.pipe(Schema.minLength(2), Schema.maxLength(2)).annotations({
31
- title: 'State',
32
- description: 'State code',
33
- }),
34
- zip: Schema.Number.annotations({ title: 'ZIP Code' }),
35
- }).annotations({ title: 'Address' }),
36
- ),
37
- employer: Schema.optional(Ref.Ref(Organization).annotations({ title: 'Employer' })),
38
- tags: Schema.optional(Schema.Array(Ref.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
39
- status: Schema.optional(Schema.Literal('active', 'inactive').annotations({ title: 'Status' })),
40
- notes: Schema.optional(Format.Text.annotations({ title: 'Notes' })),
41
- location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
42
- birthday: Schema.optional(Format.DateOnly.annotations({ title: 'Birthday' })),
43
- meetingAt: Schema.optional(Format.DateTime.annotations({ title: 'Next meeting' })),
44
- reminderAt: Schema.optional(Format.TimeOnly.annotations({ title: 'Reminder time' })),
45
- tasks: Schema.optional(Schema.Array(Schema.String).annotations({ title: 'Tasks' })),
46
- locations: Schema.optional(Schema.Array(Format.GeoPoint).annotations({ title: 'Locations' })),
47
- identities: Schema.optional(
48
- Schema.Array(
21
+ export class Person extends Type.makeObject<Person>(DXN.make('org.dxos.type.person', '0.1.0'))(
22
+ Schema.Struct({
23
+ name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
24
+ ignore: Schema.String.pipe(Annotation.FormInputAnnotation.set(false), Schema.optional),
25
+ active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
26
+ address: Schema.optional(
49
27
  Schema.Struct({
50
- type: Schema.String.annotations({ title: 'Type' }),
51
- value: Schema.String.annotations({ title: 'Value' }),
52
- }).annotations({ title: 'Identities' }),
53
- ).annotations({
54
- title: 'Identities',
55
- }),
56
- ),
57
- }).pipe(Type.makeObject(DXN.make('org.dxos.type.person', '0.1.0')));
58
-
59
- export type Person = Type.InstanceType<typeof Person>;
28
+ street: Schema.String,
29
+ city: Schema.String,
30
+ // TODO(burdon): Constrain input control.
31
+ state: Schema.String.pipe(Schema.minLength(2), Schema.maxLength(2)).annotations({
32
+ title: 'State',
33
+ description: 'State code',
34
+ }),
35
+ zip: Schema.Number.annotations({ title: 'ZIP Code' }),
36
+ }).annotations({ title: 'Address' }),
37
+ ),
38
+ employer: Schema.optional(Ref.Ref(Organization).annotations({ title: 'Employer' })),
39
+ tags: Schema.optional(Schema.Array(Ref.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
40
+ status: Schema.optional(Schema.Literal('active', 'inactive').annotations({ title: 'Status' })),
41
+ notes: Schema.optional(Format.Text.annotations({ title: 'Notes' })),
42
+ location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
43
+ birthday: Schema.optional(Format.DateOnly.annotations({ title: 'Birthday' })),
44
+ meetingAt: Schema.optional(Format.DateTime.annotations({ title: 'Next meeting' })),
45
+ reminderAt: Schema.optional(Format.TimeOnly.annotations({ title: 'Reminder time' })),
46
+ tasks: Schema.optional(Schema.Array(Schema.String).annotations({ title: 'Tasks' })),
47
+ locations: Schema.optional(Schema.Array(Format.GeoPoint).annotations({ title: 'Locations' })),
48
+ identities: Schema.optional(
49
+ Schema.Array(
50
+ Schema.Struct({
51
+ type: Schema.String.annotations({ title: 'Type' }),
52
+ value: Schema.String.annotations({ title: 'Value' }),
53
+ }).annotations({ title: 'Identities' }),
54
+ ).annotations({
55
+ title: 'Identities',
56
+ }),
57
+ ),
58
+ }),
59
+ ) {}
package/src/types.ts CHANGED
@@ -52,13 +52,21 @@ export type FormFieldRendererProps<T = any> = {
52
52
  format?: Format.TypeFormat;
53
53
  readonly?: boolean;
54
54
  label: string;
55
+ /**
56
+ * Human-readable description (from the schema's description annotation). Rendered as visible text
57
+ * only when the resolved presentation opts in; otherwise it falls back to the input placeholder.
58
+ */
59
+ description?: string;
55
60
  /**
56
61
  * Dotted JSON path of this field within the form values (e.g. `runtime.client.storage.persistent`).
57
62
  */
58
63
  jsonPath?: string;
59
64
  placeholder?: string;
60
65
  autoFocus?: boolean;
61
- layout?: FormPresentation;
66
+ /** Presentation mode for the field (full/compact/inline/static); see {@link FormPresentation}. */
67
+ presentation?: FormPresentation;
68
+ /** Whether the field is required (non-optional in the schema); surfaces a trailing asterisk on the label. */
69
+ required?: boolean;
62
70
  } & FormFieldStateProps<T>;
63
71
 
64
72
  export type FormFieldRenderer = FC<FormFieldRendererProps>;
@@ -75,6 +83,18 @@ export type FormFieldProvider = (props: {
75
83
  // Ref / create contracts.
76
84
  //
77
85
 
86
+ /**
87
+ * Per-typename override for the ref-field inline create form and its submission handler.
88
+ * The type parameter S links the form schema's decoded type to the createObject values argument,
89
+ * ensuring the schema and the handler agree on the shape of the form data.
90
+ */
91
+ export type CreateEntryOverride<S extends Schema.Schema.AnyNoContext = Schema.Schema.AnyNoContext> = {
92
+ /** Replaces the raw ECHO type schema for the inline create form. */
93
+ inputSchema?: S;
94
+ /** Runs instead of the default onCreate(schema, values) path; values are typed from inputSchema. */
95
+ createObject?: (values: Schema.Schema.Type<S>, db: Database.Database) => Promise<Obj.Unknown>;
96
+ };
97
+
78
98
  /**
79
99
  * Configuration for the inline "create a new referenced object" affordance, shared by every
80
100
  * surface that can spawn a ref target (the picker, `RefField`, and the `Form*` props that forward
@@ -91,6 +111,8 @@ export type CreateOptions = {
91
111
  export type RefOption = {
92
112
  id: string;
93
113
  label: string;
114
+ /** Optional secondary line shown beneath the label in the picker list. */
115
+ description?: string;
94
116
  hue?: Palette;
95
117
  };
96
118
 
@@ -112,6 +134,20 @@ export type RefFieldDataProps = {
112
134
  * object is then wired into this slot's form value as a Ref.
113
135
  */
114
136
  onCreate?: (schema: Type.AnyEntity, values: any) => Obj.Unknown | Promise<Obj.Unknown> | undefined | void;
137
+ /**
138
+ * Supply default values for a newly-created owned object when the user clicks "add" on an owned-ref
139
+ * array field (see `FormCreateAnnotation`). Keyed by the field's json path so a container can pre-populate
140
+ * the new object (e.g. a back-reference to the parent). The returned values are passed to
141
+ * `onCreate(schema, values)` before the object is persisted.
142
+ */
143
+ getCreateDefaults?: (props: { jsonPath: string; schema: Type.AnyEntity }) => Record<string, unknown> | undefined;
144
+ /**
145
+ * Optional resolver that maps a ref's typename to a {@link CreateEntryOverride}, replacing the
146
+ * raw ECHO type schema and/or the default `onCreate` path with plugin-registered create logic
147
+ * (e.g. `SpaceCapabilities.CreateObjectEntry`). When absent or returning `undefined` for a given
148
+ * typename the existing fallback behaviour is preserved.
149
+ */
150
+ resolveCreateEntry?: (typename: string) => CreateEntryOverride | undefined;
115
151
  };
116
152
 
117
153
  /**
@@ -132,3 +168,19 @@ export type FormFieldOptions = {
132
168
  */
133
169
  createTypename?: string;
134
170
  };
171
+
172
+ /**
173
+ * The subset of {@link RefFieldDataProps} that is forwarded down the field recursion. `useResults`
174
+ * is consumed only by `RefField` itself (it defaults internally), so it is not threaded.
175
+ */
176
+ export type RefThreadedProps = Pick<
177
+ RefFieldDataProps,
178
+ 'useType' | 'getOptions' | 'onCreate' | 'getCreateDefaults' | 'resolveCreateEntry'
179
+ >;
180
+
181
+ /**
182
+ * Form-wide configuration threaded unchanged down the entire field recursion. This is the single
183
+ * canonical source for the bag that `FormContextValue`, `FormFieldProps`, and `FormFieldSetProps`
184
+ * all carry — so none of them re-Picks fragments of the others.
185
+ */
186
+ export type FieldContext = FormFieldOptions & CreateOptions & RefThreadedProps;
package/src/util/index.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './field';
6
- export * from './field-type';
7
6
  export * from './formatting';
8
7
  export * from './omit';
9
8
  export * from './properties';
package/src/util/omit.ts CHANGED
@@ -9,14 +9,13 @@ import * as SchemaAST from 'effect/SchemaAST';
9
9
  import { Annotation, Type } from '@dxos/echo';
10
10
 
11
11
  // TODO(burdon): Reconcile with @dxos/echo.
12
- export type ExcludeId<S extends Schema.Schema.AnyNoContext | Type.AnyEntity> = Omit<
13
- S extends Type.AnyEntity
14
- ? Type.InstanceType<S>
15
- : S extends Schema.Schema.AnyNoContext
16
- ? Schema.Schema.Type<S>
17
- : never,
18
- 'id'
19
- >;
12
+ // Distributive: applied per union member so `ExcludeId<A | B>` → `ExcludeId<A> | ExcludeId<B>` rather
13
+ // than `Omit<InstanceType<A> | InstanceType<B>, 'id'>` (which would intersect away per-member narrowing).
14
+ export type ExcludeId<S extends Schema.Schema.AnyNoContext | Type.AnyEntity> = S extends Type.AnyEntity
15
+ ? Omit<Type.InstanceType<S>, 'id'>
16
+ : S extends Schema.Schema.AnyNoContext
17
+ ? Omit<Schema.Schema.Type<S>, 'id'>
18
+ : never;
20
19
 
21
20
  // TODO(burdon): Move to @dxos/schema (re-export here).
22
21
  export const omitId = <S extends Schema.Schema.AnyNoContext | Type.AnyEntity>(
@@ -25,9 +24,24 @@ export const omitId = <S extends Schema.Schema.AnyNoContext | Type.AnyEntity>(
25
24
  const schema = Type.isType(schemaOrType)
26
25
  ? Type.getSchema(schemaOrType)
27
26
  : (schemaOrType as Schema.Schema.AnyNoContext);
28
- // Cast: `Schema.omit` cannot statically express the `ExcludeId<S>` result type, so the pipe
29
- // returns a widened schema; the runtime shape matches the declared return type.
30
- return schema.pipe(Schema.omit('id')) as any;
27
+ // Cast: `Schema.omit` cannot statically express the `ExcludeId<S>` result type, so the result is a
28
+ // widened schema; the runtime shape matches the declared return type.
29
+ return omitIdFromSchema(schema) as any;
30
+ };
31
+
32
+ /**
33
+ * Removes the `id` field where present, preserving the schema's shape. Recurses into unions so a
34
+ * discriminated (create) schema keeps its discriminant — `Schema.omit('id')` is a struct operation and
35
+ * would otherwise flatten the union, breaking variant rendering. A no-op when there is no `id` (plain
36
+ * create structs that never carried one).
37
+ */
38
+ const omitIdFromSchema = (schema: Schema.Schema.AnyNoContext): Schema.Schema.AnyNoContext => {
39
+ const ast = schema.ast;
40
+ if (SchemaAST.isUnion(ast)) {
41
+ return Schema.Union(...ast.types.map((type) => omitIdFromSchema(Schema.make(type))));
42
+ }
43
+ const hasId = SchemaAST.getPropertySignatures(ast).some((prop) => prop.name === 'id');
44
+ return hasId ? schema.pipe(Schema.omit('id')) : schema;
31
45
  };
32
46
 
33
47
  /**