@dxos/react-ui-form 0.9.1-main.c7dcc2e112 → 0.10.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 (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
@@ -14,7 +14,7 @@ import { hues, osTranslations } from '@dxos/ui-theme';
14
14
  import { translationKey } from '#translations';
15
15
  import { type FormFieldRendererProps } from '#types';
16
16
 
17
- import { FormFieldLabel } from '../../FormFieldWrapper';
17
+ import { FormFieldLabel } from '../../FormRow';
18
18
 
19
19
  export const SelectOptionField = ({
20
20
  type,
@@ -145,7 +145,7 @@ export const SelectOptionField = ({
145
145
  <OrderedList.DragHandle />
146
146
  {/* TODO(ZaymonFC): Move spacer into Tag component. */}
147
147
  <div className='flex grow items-center truncate px-2' onClick={() => handleClick(item.id)}>
148
- <Tag palette={item.color as ChromaticPalette}>{item.title || '\u200b'}</Tag>
148
+ <Tag hue={item.color as ChromaticPalette}>{item.title || '\u200b'}</Tag>
149
149
  </div>
150
150
  <ToggleIconButton
151
151
  iconOnly
@@ -183,12 +183,12 @@ export const SelectOptionField = ({
183
183
  </OrderedList.Item>
184
184
  ))}
185
185
  <IconButton
186
+ classNames='w-full'
186
187
  variant='ghost'
187
188
  icon='ph--plus--regular'
188
189
  label={t('select-option-add.button')}
189
190
  onClick={handleAdd}
190
191
  disabled={!!readonly}
191
- classNames='w-full'
192
192
  />
193
193
  </OrderedList.Content>
194
194
  )}
@@ -8,7 +8,7 @@ import { Input, type TextAreaProps } from '@dxos/react-ui';
8
8
 
9
9
  import { type FormFieldRendererProps } from '#types';
10
10
 
11
- import { FormFieldWrapper } from '../../FormFieldWrapper';
11
+ import { FormRow } from '../../FormRow';
12
12
 
13
13
  export const TextAreaField = ({
14
14
  type,
@@ -24,7 +24,7 @@ export const TextAreaField = ({
24
24
  );
25
25
 
26
26
  return (
27
- <FormFieldWrapper<string> readonly={readonly} {...props}>
27
+ <FormRow<string> readonly={readonly} {...props}>
28
28
  {({ value = '' }) => (
29
29
  <Input.TextArea
30
30
  rows={5}
@@ -35,6 +35,6 @@ export const TextAreaField = ({
35
35
  onBlur={onBlur}
36
36
  />
37
37
  )}
38
- </FormFieldWrapper>
38
+ </FormRow>
39
39
  );
40
40
  };
@@ -8,7 +8,7 @@ import { Input, type TextInputProps } from '@dxos/react-ui';
8
8
 
9
9
  import { type FormFieldRendererProps } from '#types';
10
10
 
11
- import { FormFieldWrapper } from '../../FormFieldWrapper';
11
+ import { FormRow } from '../../FormRow';
12
12
 
13
13
  export const TextField = ({
14
14
  type,
@@ -24,7 +24,7 @@ export const TextField = ({
24
24
  );
25
25
 
26
26
  return (
27
- <FormFieldWrapper<string> readonly={readonly} {...props}>
27
+ <FormRow<string> readonly={readonly} {...props}>
28
28
  {({ value = '' }) => (
29
29
  <Input.TextInput
30
30
  noAutoFill
@@ -35,6 +35,6 @@ export const TextField = ({
35
35
  onChange={handleChange}
36
36
  />
37
37
  )}
38
- </FormFieldWrapper>
38
+ </FormRow>
39
39
  );
40
40
  };
@@ -10,7 +10,7 @@ import { safeParseFloat } from '@dxos/util';
10
10
 
11
11
  import { type FormFieldRendererProps } from '#types';
12
12
 
13
- import { FormFieldLabel } from '../../FormFieldWrapper';
13
+ import { FormFieldLabel } from '../../FormRow';
14
14
 
15
15
  const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
16
16
 
@@ -3,11 +3,15 @@
3
3
  //
4
4
 
5
5
  export * from './ArrayField';
6
+ export * from './AsyncSelectField';
7
+ export * from './AutofillField';
6
8
  export * from './BooleanField';
9
+ export * from './ComboboxField';
7
10
  export * from './DateField';
8
11
  export * from './GeoPointField';
9
12
  export * from './MarkdownField';
10
13
  export * from './NumberField';
14
+ export * from './PasswordField';
11
15
  export * from './RefField';
12
16
  export * from './SelectField';
13
17
  export * from './SelectOptionField';
@@ -2,7 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './FieldHeader';
6
- export * from './FormField';
7
5
  export * from './fields';
8
- export * from './FormFieldWrapper';
6
+ export * from './presentation';
7
+
8
+ export * from './FormFieldHeader';
9
+ export * from './FormField';
10
+ export * from './FormRow';
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type FormPresentation } from '#types';
6
+
7
+ /**
8
+ * Resolved presentation strategy for a field — which parts render, derived from {@link FormPresentation}.
9
+ * Visual chrome (borders, grid, spacing) is owned by the Form theme recipe, not here.
10
+ */
11
+ export type FormFieldPresentation = {
12
+ layout?: FormPresentation;
13
+ /** Render the value as read-only plain DOM rather than an editable control. */
14
+ isStatic: boolean;
15
+ /** Render the field's label row. */
16
+ showLabel: boolean;
17
+ /** Render the inline validation/error block beneath the control. */
18
+ showError: boolean;
19
+ };
20
+
21
+ /**
22
+ * Derive the {@link FormFieldPresentation} for a layout.
23
+ * - `static` renders read-only plain DOM.
24
+ * - `inline` drops the label (the control stands alone, e.g. an array row).
25
+ * - `full` adds the inline error block.
26
+ */
27
+ export const presentationFor = (layout?: FormPresentation): FormFieldPresentation => ({
28
+ layout,
29
+ isStatic: layout === 'static',
30
+ showLabel: layout !== 'inline',
31
+ showError: layout === 'full',
32
+ });
@@ -3,65 +3,66 @@
3
3
  //
4
4
 
5
5
  import * as Option from 'effect/Option';
6
- import React, { useMemo, useState } from 'react';
6
+ import React, { useMemo } from 'react';
7
7
 
8
8
  import { DEFAULT_LAYOUT_NAME, FormLayoutAnnotation } from '@dxos/echo/Annotation';
9
9
  import { type AnyProperties } from '@dxos/echo/internal';
10
10
  import { SchemaEx } from '@dxos/effect';
11
- import { ToggleIconButton, useTranslation } from '@dxos/react-ui';
11
+ import { type Merge } from '@dxos/util';
12
12
 
13
- import { translationKey } from '#translations';
14
- import { type CreateOptions, type FormFieldOptions, type RefFieldDataProps } from '#types';
13
+ import { type FieldContext } from '#types';
15
14
 
16
15
  import { type FormHandlerProps, useFormValues } from '../../../hooks';
17
16
  import { getRootFormProperties } from '../../../util';
18
- import { FormField, FormFieldErrorBoundary, FormFieldLabel, type FormFieldProps } from '../FormField';
17
+ import { FormField, FormFieldErrorBoundary, type FormFieldProps, presentationFor } from '../FormField';
19
18
  import { FormLayout } from '../FormLayout';
19
+ import { FormFieldSetContainer } from './FormFieldSetContainer';
20
20
 
21
21
  const FORM_FIELDSET_NAME = 'Form.FieldSet';
22
22
 
23
- export type FormFieldSetProps<T extends AnyProperties> = {
24
- label?: string;
25
- sort?: string[];
26
- /**
27
- * When set, renders a toggle button at the end of the label row that
28
- * shows/hides the field set body. Used for nested objects.
29
- */
30
- collapsible?: boolean;
31
- exclude?: (props: SchemaEx.SchemaProperty[]) => SchemaEx.SchemaProperty[];
32
- /**
33
- * Picks a named layout out of `FormLayoutAnnotation` when present.
34
- * Falls back to `'default'`. Ignored when the schema has no annotation
35
- * (linear rendering then takes over).
36
- */
37
- layoutName?: string;
38
- } & Pick<FormHandlerProps<T>, 'schema'> &
39
- Pick<FormFieldProps, 'path' | 'autoFocus'> &
40
- FormFieldOptions &
41
- CreateOptions &
42
- Pick<RefFieldDataProps, 'useType' | 'getOptions' | 'onCreate'>;
23
+ export type FormFieldSetProps<T extends AnyProperties> = Merge<
24
+ {
25
+ /** Applied to the field set's container (the bordered box when collapsible). */
26
+ classNames?: string;
27
+ label?: string;
28
+ sort?: string[];
29
+ /**
30
+ * When set, renders a toggle button at the end of the label row that
31
+ * shows/hides the field set body. Used for nested objects.
32
+ */
33
+ collapsible?: boolean;
34
+ filter?: (props: SchemaEx.SchemaProperty[]) => SchemaEx.SchemaProperty[];
35
+ /**
36
+ * Picks a named layout out of `FormLayoutAnnotation` when present.
37
+ * Falls back to `'default'`. Ignored when the schema has no annotation
38
+ * (linear rendering then takes over).
39
+ */
40
+ layoutName?: string;
41
+ },
42
+ Pick<FormHandlerProps<T>, 'schema'>,
43
+ Pick<FormFieldProps, 'path' | 'autoFocus'>,
44
+ FieldContext
45
+ >;
43
46
 
44
47
  /**
45
48
  * Renders a set of form fields derived from a schema object.
46
49
  */
47
50
  export const FormFieldSet = ({
51
+ classNames,
48
52
  label,
49
53
  schema,
50
54
  readonly,
51
55
  path,
52
56
  sort,
53
57
  collapsible,
54
- exclude,
58
+ filter,
55
59
  projection,
56
60
  layout,
57
61
  layoutName = DEFAULT_LAYOUT_NAME,
58
62
  ...props
59
63
  }: FormFieldSetProps<any>) => {
60
- const { t } = useTranslation(translationKey);
61
64
  const values = useFormValues(FORM_FIELDSET_NAME, path);
62
- const properties = useFormFieldSetProperties({ schema, values, exclude, sort, projection });
63
- // TODO(burdon): Generalize collapse state (cf. useSelection in react-ui-attention, plugin-markdown cursor state).
64
- const [collapsed, setCollapsed] = useState(false);
65
+ const properties = useFormFieldSetProperties({ schema, values, filter, sort, projection });
65
66
  if ((readonly || layout === 'static') && values == null) {
66
67
  return null;
67
68
  }
@@ -89,6 +90,7 @@ export const FormFieldSet = ({
89
90
  type={property.type}
90
91
  name={name}
91
92
  path={[...(path ?? []), name]}
93
+ required={!property.isOptional}
92
94
  readonly={readonly}
93
95
  layout={layout}
94
96
  projection={projection}
@@ -99,56 +101,33 @@ export const FormFieldSet = ({
99
101
  })
100
102
  );
101
103
 
102
- const showBody = !(collapsible && collapsed);
103
- const content = (
104
- <>
105
- {layout !== 'inline' && label && (
106
- <FormFieldLabel
107
- standalone
108
- classNames='pl-2'
109
- label={label}
110
- path={SchemaEx.createJsonPath(path ?? [])}
111
- button={
112
- collapsible && (
113
- <ToggleIconButton
114
- active={!collapsed}
115
- classNames='px-1 mr-0.5'
116
- variant='ghost'
117
- density='xs'
118
- iconOnly
119
- icon='ph--caret-right--regular'
120
- label={t(collapsed ? 'expand-fields.label' : 'collapse-fields.label')}
121
- />
122
- )
123
- }
124
- onClick={() => setCollapsed((value) => !value)}
125
- />
126
- )}
127
- {showBody && (collapsible ? <div className='px-2 pb-2'>{body}</div> : body)}
128
- </>
104
+ return (
105
+ <FormFieldSetContainer
106
+ classNames={classNames}
107
+ label={label}
108
+ path={SchemaEx.createJsonPath(path ?? [])}
109
+ readonly={readonly}
110
+ presentation={presentationFor(layout)}
111
+ collapsible={collapsible}
112
+ >
113
+ {body}
114
+ </FormFieldSetContainer>
129
115
  );
130
-
131
- // Nested field sets render inside an indented, bordered container with a collapse toggle.
132
- if (collapsible) {
133
- return <div className='border border-subdued-separator rounded-sm mb-1'>{content}</div>;
134
- }
135
-
136
- return content;
137
116
  };
138
117
 
139
118
  FormFieldSet.displayName = FORM_FIELDSET_NAME;
140
119
 
141
- type UseFormFieldSetPropertiesParams = Pick<FormFieldSetProps<any>, 'schema' | 'exclude' | 'projection' | 'sort'> & {
120
+ type UseFormFieldSetPropertiesParams = Pick<FormFieldSetProps<any>, 'schema' | 'filter' | 'projection' | 'sort'> & {
142
121
  values: AnyProperties | undefined;
143
122
  sort?: string[];
144
123
  };
145
124
 
146
125
  /**
147
- * Resolves ordered schema properties for a field set (projection order, exclude, or sort).
126
+ * Resolves ordered schema properties for a field set (projection order, filter, or sort).
148
127
  */
149
128
  const useFormFieldSetProperties = ({
150
129
  schema,
151
- exclude,
130
+ filter,
152
131
  projection,
153
132
  values,
154
133
  sort,
@@ -160,9 +139,9 @@ const useFormFieldSetProperties = ({
160
139
  return [];
161
140
  }
162
141
 
163
- // TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
142
+ // TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & filter function.
164
143
  const schemaProps = getRootFormProperties(schema.ast, values);
165
- const filteredProps = exclude ? exclude(schemaProps) : schemaProps;
144
+ const filteredProps = filter ? filter(schemaProps) : schemaProps;
166
145
 
167
146
  // Use projection-based field management when view and projection are available.
168
147
  if (projection) {
@@ -193,5 +172,5 @@ const useFormFieldSetProperties = ({
193
172
  return sort
194
173
  ? [...filteredProps].sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
195
174
  : filteredProps;
196
- }, [schema, values, exclude, sort, projection]);
175
+ }, [schema, values, filter, sort, projection]);
197
176
  };
@@ -0,0 +1,93 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren, useState } from 'react';
6
+
7
+ import { type ThemedClassName, ToggleIconButton, useTranslation } from '@dxos/react-ui';
8
+ import { mx } from '@dxos/ui-theme';
9
+
10
+ import { translationKey } from '#translations';
11
+
12
+ import { formTheme } from '../Form.theme';
13
+ import { FormFieldHeader } from '../FormField/FormFieldHeader';
14
+ import { type FormFieldPresentation } from '../FormField/presentation';
15
+
16
+ const styles = formTheme.styles();
17
+
18
+ export type FormFieldSetContainerProps = ThemedClassName<
19
+ PropsWithChildren<{
20
+ label?: string;
21
+ /** JSON path of the group, forwarded to the label as field metadata. */
22
+ path?: string;
23
+ readonly?: boolean;
24
+ presentation: FormFieldPresentation;
25
+ /**
26
+ * Render a collapse toggle in the header and wrap the body in an indented, bordered box.
27
+ * Used for nested objects (struct fields, object-array items, inline refs).
28
+ */
29
+ collapsible?: boolean;
30
+ }>
31
+ >;
32
+
33
+ /**
34
+ * Shared chrome for a labelled group of sub-fields: an optional header (label + collapse toggle) and,
35
+ * when collapsible, an indented bordered container around the body. Nested structs, object-array items,
36
+ * and inline refs all reach this via `FormFieldSet`, so their visual containment is identical.
37
+ */
38
+ export const FormFieldSetContainer = ({
39
+ classNames,
40
+ label,
41
+ path,
42
+ readonly,
43
+ presentation,
44
+ collapsible,
45
+ children,
46
+ }: FormFieldSetContainerProps) => {
47
+ const { t } = useTranslation(translationKey);
48
+ // TODO(burdon): Generalize collapse state (cf. useSelection in react-ui-attention, plugin-markdown cursor state).
49
+ const [collapsed, setCollapsed] = useState(false);
50
+ const showBody = !(collapsible && collapsed);
51
+
52
+ const content = (
53
+ <>
54
+ {presentation.showLabel && label && (
55
+ <FormFieldHeader
56
+ label={label}
57
+ path={path}
58
+ readonly={readonly}
59
+ classNames='pl-2'
60
+ actions={
61
+ collapsible ? (
62
+ <ToggleIconButton
63
+ active={!collapsed}
64
+ classNames='px-1 mr-0.5'
65
+ variant='ghost'
66
+ density='xs'
67
+ iconOnly
68
+ icon='ph--caret-right--regular'
69
+ label={t(collapsed ? 'expand-fields.label' : 'collapse-fields.label')}
70
+ />
71
+ ) : undefined
72
+ }
73
+ onClick={collapsible ? () => setCollapsed((value) => !value) : undefined}
74
+ />
75
+ )}
76
+ {showBody && (collapsible ? <div className={styles.fieldSetBody()}>{children}</div> : children)}
77
+ </>
78
+ );
79
+
80
+ // Nested groups render inside an indented, bordered container with a collapse toggle. A non-collapsible
81
+ // group only materializes a wrapper when `classNames` is supplied — otherwise the body flows straight
82
+ // into the parent grid (the default, grid-transparent behavior).
83
+ if (collapsible) {
84
+ return (
85
+ <div className={styles.fieldSetBoxOuter()}>
86
+ <div className={styles.fieldSetBox({ class: mx(classNames) })}>{content}</div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ // Only materialize a wrapper when `classNames` is supplied; otherwise stay grid-transparent.
92
+ return classNames ? <div className={mx(classNames)}>{content}</div> : <>{content}</>;
93
+ };
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './FormFieldSet';
6
+ export * from './FormFieldSetContainer';
@@ -125,12 +125,12 @@ const JOURNEY_LAYOUT = trim`
125
125
  </grid>
126
126
  `;
127
127
 
128
- type StoryProps = {
128
+ type StoryArgs = {
129
129
  schema: Schema.Schema<any>;
130
130
  template?: string;
131
131
  };
132
132
 
133
- const DefaultStory = ({ schema, template }: StoryProps) => {
133
+ const DefaultStory = ({ schema, template }: StoryArgs) => {
134
134
  const [values, setValues] = useState<Partial<FlightValues>>(flight);
135
135
 
136
136
  const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>((next) => {
@@ -164,11 +164,11 @@ const meta = {
164
164
  layout: 'fullscreen',
165
165
  translations,
166
166
  },
167
- } satisfies Meta<StoryProps>;
167
+ } satisfies Meta<StoryArgs>;
168
168
 
169
169
  export default meta;
170
170
 
171
- type Story = StoryObj<StoryProps>;
171
+ type Story = StoryObj<StoryArgs>;
172
172
 
173
173
  /** Baseline: no annotation, no override — `Form.FieldSet` renders linearly (one field per row). */
174
174
  export const Linear: Story = {
@@ -383,7 +383,9 @@ const PlaygroundStory = ({ card = false }: PlaygroundStoryProps) => {
383
383
  onChange={setTemplate}
384
384
  />
385
385
  </Editor.Root>
386
- {error && <div className='border-t border-separator p-2 text-sm text-error-text font-mono'>{error}</div>}
386
+ {error && (
387
+ <div className='border-t border-input-separator p-2 text-sm text-error-text font-mono'>{error}</div>
388
+ )}
387
389
  </div>
388
390
  </TestPanel>
389
391
  <TestPanel>
@@ -15,10 +15,10 @@ import { Input } from '@dxos/react-ui';
15
15
 
16
16
  import { type FormPresentation } from '#types';
17
17
 
18
- import { useFormFieldState } from '../../../hooks';
19
- import { FormField, type FormFieldProps } from '../FormField';
20
- import { FormFieldErrorBoundary, FormFieldLabel } from '../FormField';
21
- import { LayoutParseError, type LayoutNode, parseLayout } from './parser';
18
+ import { useFormContext, useFormFieldState } from '../../../hooks';
19
+ import { formTheme } from '../Form.theme';
20
+ import { FormField, FormFieldErrorBoundary, FormFieldLabel, type FormFieldProps, presentationFor } from '../FormField';
21
+ import { type LayoutNode, LayoutParseError, parseLayout } from './parser';
22
22
 
23
23
  const FORM_LAYOUT_NAME = 'Form.Layout';
24
24
 
@@ -115,7 +115,7 @@ const RenderNode = ({ node, schema, basePath, ...props }: RenderNodeProps) => {
115
115
  throw new LayoutParseError(`field "${node.name}" not found on schema`);
116
116
  }
117
117
 
118
- const { type, segments, leafName, title, labelType } = resolved;
118
+ const { type, segments, leafName, title, labelType, required } = resolved;
119
119
  const path = [...basePath, ...segments];
120
120
  const span = node.span ? { gridColumn: `span ${node.span} / span ${node.span}` } : undefined;
121
121
 
@@ -130,7 +130,7 @@ const RenderNode = ({ node, schema, basePath, ...props }: RenderNodeProps) => {
130
130
  layout={props.layout}
131
131
  />
132
132
  ) : (
133
- <FormField type={type} name={leafName} path={path} {...props} />
133
+ <FormField type={type} name={leafName} path={path} required={required} {...props} />
134
134
  )}
135
135
  </FormFieldErrorBoundary>
136
136
  </div>
@@ -152,6 +152,8 @@ export type ResolvedLayoutField = {
152
152
  * sub-fields. Undefined for ordinary (non-auto-labelled) fields.
153
153
  */
154
154
  labelType?: SchemaAST.AST;
155
+ /** Whether the resolved property is required (non-optional). */
156
+ required: boolean;
155
157
  };
156
158
 
157
159
  /**
@@ -216,6 +218,7 @@ export const resolveLayoutField = (schema: Schema.Schema<any>, name: string): Re
216
218
  leafName: segments[segments.length - 1],
217
219
  title,
218
220
  labelType: labelled ? labelType : undefined,
221
+ required: !prop.isOptional,
219
222
  };
220
223
  };
221
224
 
@@ -232,6 +235,8 @@ type LabelFieldProps = {
232
235
  * presentation of regular fields.
233
236
  */
234
237
  const LabelField = ({ schema, label, path, layout }: LabelFieldProps) => {
238
+ const { variant = 'default' } = useFormContext(FORM_LAYOUT_NAME);
239
+ const styles = formTheme.styles({ variant });
235
240
  const { getValue } = useFormFieldState(FORM_LAYOUT_NAME, path);
236
241
  const value = getValue();
237
242
  const text = value == null ? undefined : Annotation.getLabelWithSchema(schema, value);
@@ -239,14 +244,19 @@ const LabelField = ({ schema, label, path, layout }: LabelFieldProps) => {
239
244
  return null;
240
245
  }
241
246
 
247
+ const presentation = presentationFor(layout);
242
248
  return (
243
- <div className='contents'>
244
- <Input.Root>
245
- {layout !== 'inline' && <FormFieldLabel readonly label={label} path={SchemaEx.createJsonPath(path)} />}
246
- <p className='truncate min-w-0' title={text}>
247
- {text}
248
- </p>
249
- </Input.Root>
250
- </div>
249
+ <Input.Root>
250
+ <div className={styles.field()}>
251
+ {presentation.showLabel && (
252
+ <FormFieldLabel variant={variant} readonly label={label} path={SchemaEx.createJsonPath(path)} />
253
+ )}
254
+ <div className={styles.fieldControl()}>
255
+ <p className='truncate min-w-0' title={text}>
256
+ {text}
257
+ </p>
258
+ </div>
259
+ </div>
260
+ </Input.Root>
251
261
  );
252
262
  };
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export { FormLayout, type FormLayoutProps } from './FormLayout';
6
- export { LayoutParseError, parseLayout, type LayoutNode } from './parser';
6
+ export { type LayoutNode, LayoutParseError, parseLayout } from './parser';
@@ -11,7 +11,7 @@ import { Obj, Ref, Tag } from '@dxos/echo';
11
11
  import { META_TAGS_KEY, withMetaTags } from './meta-tags';
12
12
 
13
13
  // A feed-host schema whose own `tags` field is a `TagIndex` record (tagId -> objectId[]), not an
14
- // array — mirrors plugin-commerce `Search`, plugin-inbox `Mailbox`, plugin-feed `Subscription`.
14
+ // array — mirrors plugin-commerce `Search`, plugin-inbox `Mailbox`, plugin-magazine `Subscription`.
15
15
  const TagIndexHost = Schema.Struct({
16
16
  name: Schema.String.pipe(Schema.optional),
17
17
  tags: Schema.Record({ key: Schema.String, value: Schema.Array(Obj.ID) }).pipe(Schema.optional),
@@ -99,12 +99,12 @@ export const ObjectForm = ({ object, type }: ObjectFormProps) => {
99
99
  };
100
100
 
101
101
  const createFieldMap: FormFieldMap = {
102
- hue: ({ type, label, layout, getValue, onValueChange }) => {
102
+ hue: ({ type, label, presentation, getValue, onValueChange }) => {
103
103
  const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
104
104
  const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
105
105
  return (
106
106
  <>
107
- {layout !== 'inline' && <Form.Label label={label} />}
107
+ {presentation !== 'inline' && <Form.Label label={label} />}
108
108
  <HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
109
109
  </>
110
110
  );
@@ -129,6 +129,7 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
129
129
  key={option.id}
130
130
  value={option.id}
131
131
  label={option.label}
132
+ description={option.description}
132
133
  checked={selectedIds?.includes(option.id)}
133
134
  onSelect={() => onSelect?.(option.id)}
134
135
  classNames='flex items-center gap-2'
@@ -7,7 +7,7 @@ import * as Schema from 'effect/Schema';
7
7
  import React, { useEffect } from 'react';
8
8
  import { expect, userEvent, within } from 'storybook/test';
9
9
 
10
- import { DXN, Annotation, Filter, Obj, Ref, Tag, Type } from '@dxos/echo';
10
+ import { Annotation, DXN, Filter, Obj, Ref, Tag, Type } from '@dxos/echo';
11
11
  import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/Annotation';
12
12
  import { useQuery } from '@dxos/react-client/echo';
13
13
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';