@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
@@ -0,0 +1,86 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type VariantProps, mx, tv } from '@dxos/ui-theme';
6
+
7
+ /** Visual variants for {@link Form}. `settings` is the bordered two-column settings-panel look. */
8
+ export type FormVariant = NonNullable<VariantProps<typeof formStyles>['variant']>;
9
+
10
+ /** Non-className behavior flags, keyed by {@link FormVariant}. */
11
+ export type FormBehavior = { showDescription: boolean };
12
+
13
+ const formStyles = tv({
14
+ slots: {
15
+ viewport: '',
16
+ content: '',
17
+ section: 'flex flex-col py-form-section-gap first:pt-0',
18
+ group: 'flex flex-col gap-3 p-trim-md border border-separator rounded-sm',
19
+ sectionHeader: '',
20
+ sectionTitle: 'text-lg',
21
+ sectionDescription: 'text-description',
22
+ fieldSet: '',
23
+ field: '',
24
+ fieldLabel: 'h-8 grid grid-cols-[1fr_auto_auto] items-center select-none',
25
+ fieldLabelText: '',
26
+ fieldDescription: 'text-description',
27
+ fieldControl: '',
28
+ fieldValidation: '',
29
+ // Action bar (cancel/save), equal-width columns flowing horizontally.
30
+ actions: 'grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding',
31
+ // Standalone submit row (full-width primary button).
32
+ submit: 'flex w-full pt-form-padding',
33
+ // Collapsible field-set body: indented column of sub-fields.
34
+ fieldSetBody: 'flex flex-col gap-2 px-2 pb-2',
35
+ // Bordered container wrapping a collapsible nested group, plus its top spacing.
36
+ fieldSetBox: 'border border-subdued-separator rounded-sm',
37
+ fieldSetBoxOuter: 'pt-trim-md',
38
+ },
39
+ variants: {
40
+ variant: {
41
+ default: {},
42
+ settings: {
43
+ content: 'dx-document',
44
+ // Gap on the section spaces its direct children — section title/description and, for action
45
+ // panels, the `Form.Row`s placed directly in the section (which have no `fieldSet` wrapper).
46
+ section: 'py-form-section-gap! gap-trim-md',
47
+ sectionHeader: 'pb-form-section-gap',
48
+ sectionTitle: 'px-trim-md text-xl',
49
+ sectionDescription: 'px-trim-md',
50
+ // No top padding: the section gap already separates the field set from the title above it.
51
+ fieldSet: 'flex flex-col gap-trim-md',
52
+ field: mx(
53
+ 'grid',
54
+ 'grid-cols-1 [grid-template-areas:"header""description""control""validation"]',
55
+ 'md:grid-cols-2 md:[grid-template-areas:"header_header""description_control""validation_validation"]',
56
+ 'gap-x-trim-lg gap-y-0 p-trim-md border border-input-separator rounded-md',
57
+ ),
58
+ fieldLabel: '[grid-area:header]',
59
+ fieldLabelText: 'text-base-fg text-lg',
60
+ fieldDescription: '[grid-area:description] pt-1 text-description',
61
+ fieldControl: '[grid-area:control] flex justify-end items-start md:text-right pt-3 md:pt-0',
62
+ fieldValidation: '[grid-area:validation]',
63
+ },
64
+ },
65
+ },
66
+ defaultVariants: {
67
+ variant: 'default',
68
+ },
69
+ });
70
+
71
+ /** {@link Form} theme template: a tailwind-variants recipe (`styles`) plus non-class `behavior`. */
72
+ export const formTheme = {
73
+ styles: formStyles,
74
+ behavior: {
75
+ default: {
76
+ showDescription: false,
77
+ },
78
+ settings: {
79
+ showDescription: true,
80
+ },
81
+ } satisfies Record<FormVariant, FormBehavior>,
82
+ };
83
+
84
+ /** Slot names of {@link formTheme.styles}, for `bridgeTv` registration. */
85
+ // Object.keys widens to string[]; the slot names are statically known.
86
+ export const formSlots = Object.keys(formTheme.styles()) as Array<keyof ReturnType<typeof formTheme.styles>>;
@@ -8,8 +8,10 @@ import {
8
8
  FormContent,
9
9
  type FormContentProps,
10
10
  FormError,
11
- FormFieldSet,
12
- FormLayout,
11
+ FormFieldSetController,
12
+ FormGroup,
13
+ type FormGroupProps,
14
+ FormLayoutController,
13
15
  type FormLayoutProps,
14
16
  FormRoot,
15
17
  type FormRootProps,
@@ -20,27 +22,31 @@ import {
20
22
  FormViewport,
21
23
  type FormViewportProps,
22
24
  } from './FormControls';
23
- import { FormFieldLabel } from './FormField';
25
+ import { FormFieldLabel, FormRow, type FormRowProps } from './FormField';
24
26
 
25
27
  export const Form = {
26
28
  Root: FormRoot,
27
29
  Viewport: FormViewport,
28
30
  Content: FormContent,
29
31
  Section: FormSection,
30
- FieldSet: FormFieldSet,
31
- Layout: FormLayout,
32
+ Group: FormGroup,
33
+ FieldSet: FormFieldSetController,
34
+ Layout: FormLayoutController,
32
35
  Label: FormFieldLabel,
36
+ Row: FormRow,
33
37
  Actions: FormActions,
34
38
  Submit: FormSubmit,
35
39
  Error: FormError,
36
40
  };
37
41
 
38
42
  export type {
39
- FormRootProps,
40
- FormViewportProps,
43
+ FormActionsProps,
41
44
  FormContentProps,
42
- FormSectionProps,
45
+ FormGroupProps,
43
46
  FormLayoutProps,
44
- FormActionsProps,
47
+ FormRootProps,
48
+ FormRowProps,
49
+ FormSectionProps,
45
50
  FormSubmitProps,
51
+ FormViewportProps,
46
52
  };
@@ -19,7 +19,9 @@ import {
19
19
  useTranslation,
20
20
  withColumn,
21
21
  } from '@dxos/react-ui';
22
+ import { MarkdownView } from '@dxos/react-ui-markdown';
22
23
  import { mx } from '@dxos/ui-theme';
24
+ import { type Merge } from '@dxos/util';
23
25
 
24
26
  import { translationKey } from '#translations';
25
27
 
@@ -32,18 +34,16 @@ import {
32
34
  useFormHandler,
33
35
  useKeyHandler,
34
36
  } from '../../hooks';
35
- import {
36
- FormFieldSet as NaturalFormFieldSet,
37
- type FormFieldSetProps as NaturalFormFieldSetProps,
38
- } from './FormFieldSet';
39
- import { FormLayout as NaturalFormLayout, type FormLayoutProps as NaturalFormLayoutProps } from './FormLayout';
37
+ import { formTheme } from './Form.theme';
38
+ import { FormFieldSet, type FormFieldSetProps as NaturalFormFieldSetProps } from './FormFieldSet';
39
+ import { FormLayout, type FormLayoutProps as NaturalFormLayoutProps } from './FormLayout';
40
40
 
41
41
  //
42
42
  // Root
43
43
  //
44
44
 
45
- export type FormRootProps<T extends AnyProperties = AnyProperties> = PropsWithChildren<
46
- {
45
+ export type FormRootProps<T extends AnyProperties = AnyProperties> = Merge<
46
+ PropsWithChildren<{
47
47
  /**
48
48
  * Called when the form is submitted and passes validation.
49
49
  */
@@ -53,11 +53,13 @@ export type FormRootProps<T extends AnyProperties = AnyProperties> = PropsWithCh
53
53
  * Called when the form is canceled to abandon/undo any pending changes.
54
54
  */
55
55
  onCancel?: () => void;
56
- } & Omit<FormContextValue<T>, 'form'> &
57
- Pick<FormHandlerProps<T>, 'schema' | 'autoSave' | 'values' | 'defaultValues' | 'onValidate' | 'onValuesChanged'> &
58
- Omit<NaturalFormFieldSetProps<T>, 'schema' | 'path'>
56
+ }>,
57
+ Omit<FormContextValue<T>, 'form'>,
58
+ Pick<FormHandlerProps<T>, 'schema' | 'autoSave' | 'values' | 'defaultValues' | 'onValidate' | 'onValuesChanged'>,
59
+ Omit<NaturalFormFieldSetProps<T>, 'schema' | 'path'>
59
60
  >;
60
61
 
62
+ // `variant` (from FormContextValue) flows through `...props` into the context for all parts to read.
61
63
  export const FormRoot = <T extends AnyProperties = AnyProperties>({
62
64
  children,
63
65
  schema,
@@ -83,19 +85,31 @@ FormRoot.displayName = 'Form.Root';
83
85
 
84
86
  const FORM_VIEWPORT_NAME = 'Form.Viewport';
85
87
 
86
- export type FormViewportProps = { scroll?: boolean; gutter?: ColumnRootProps['gutter'] };
88
+ export type FormViewportProps = {
89
+ scroll?: boolean;
90
+ gutter?: ColumnRootProps['gutter'];
91
+ };
87
92
 
88
93
  // The viewing window: owns the gutter Column (chrome/side-padding).
89
94
  // Content-height by default; `scroll` makes it fill its parent and scroll (the gutter then hosts the scrollbar).
90
95
  export const FormViewport = composable<HTMLDivElement, FormViewportProps>(
91
- ({ children, scroll, gutter = 'xs', ...props }, forwardedRef) => {
96
+ ({ children, scroll, gutter = 'sm', ...props }, forwardedRef) => {
97
+ const { variant = 'default' } = useFormContext(FORM_VIEWPORT_NAME);
98
+ const styles = formTheme.styles({ variant });
92
99
  // Span the full width when nested inside another Column grid (e.g. Card.Root)
93
100
  // instead of landing in a single narrow track.
94
101
  const span = '[.dx-column-root_&]:col-span-full';
95
102
  if (scroll) {
96
103
  return (
97
104
  <Column.Root gutter={gutter} classNames={['dx-expander', span]}>
98
- <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
105
+ <ScrollArea.Root
106
+ {...composableProps(props, { classNames: styles.viewport() })}
107
+ orientation='vertical'
108
+ centered
109
+ padding
110
+ thin
111
+ ref={forwardedRef}
112
+ >
99
113
  <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
100
114
  </ScrollArea.Root>
101
115
  </Column.Root>
@@ -103,7 +117,11 @@ export const FormViewport = composable<HTMLDivElement, FormViewportProps>(
103
117
  }
104
118
 
105
119
  return (
106
- <Column.Root {...composableProps(props)} gutter={gutter} classNames={['w-full min-w-0', span]} ref={forwardedRef}>
120
+ <Column.Root
121
+ {...composableProps(props, { classNames: ['w-full min-w-0', span, styles.viewport()] })}
122
+ gutter={gutter}
123
+ ref={forwardedRef}
124
+ >
107
125
  {children}
108
126
  </Column.Root>
109
127
  );
@@ -122,7 +140,8 @@ export type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
122
140
 
123
141
  // The viewed body: centered in the viewport's gutter. Pure body — the gutter Column is owned by `Form.Viewport`.
124
142
  export const FormContent = composable<HTMLDivElement, FormContentProps>(({ children, ...props }, forwardedRef) => {
125
- const { form, testId } = useFormContext(FORM_CONTENT_NAME);
143
+ const { form, testId, variant = 'default' } = useFormContext(FORM_CONTENT_NAME);
144
+ const styles = formTheme.styles({ variant });
126
145
  const localRef = useRef<HTMLDivElement>(null);
127
146
  const mergedRef = useMergeRefs([forwardedRef, localRef]);
128
147
  useKeyHandler(localRef, form);
@@ -131,7 +150,7 @@ export const FormContent = composable<HTMLDivElement, FormContentProps>(({ child
131
150
  <div
132
151
  {...composableProps(props, {
133
152
  role: 'form',
134
- classNames: mx(withColumn.center(), 'flex flex-col w-full'),
153
+ classNames: mx(withColumn.center(), 'flex flex-col w-full dx-document', styles.content()),
135
154
  })}
136
155
  data-testid={testId}
137
156
  ref={mergedRef}
@@ -149,15 +168,23 @@ FormContent.displayName = FORM_CONTENT_NAME;
149
168
 
150
169
  const FORM_FIELDSET_NAME = 'Form.FieldSet';
151
170
 
152
- export type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
153
-
154
- export const FormFieldSet = (props: FormFieldSetProps) => {
155
- const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
171
+ export type FormFieldSetControllerProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
156
172
 
157
- return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
173
+ /** Context-reading binding for `Form.FieldSet`: pulls the schema + field context off the form and delegates to {@link FormFieldSet}. */
174
+ export const FormFieldSetController = ({ classNames, ...props }: FormFieldSetControllerProps) => {
175
+ const { form, variant = 'default', ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
176
+ const styles = formTheme.styles({ variant });
177
+ return (
178
+ <FormFieldSet
179
+ schema={form.schema}
180
+ classNames={styles.fieldSet({ class: classNames })}
181
+ {...contextProps}
182
+ {...props}
183
+ />
184
+ );
158
185
  };
159
186
 
160
- FormFieldSet.displayName = FORM_FIELDSET_NAME;
187
+ FormFieldSetController.displayName = FORM_FIELDSET_NAME;
161
188
 
162
189
  //
163
190
  // Layout
@@ -167,17 +194,18 @@ const FORM_LAYOUT_NAME = 'Form.Layout';
167
194
 
168
195
  export type FormLayoutProps = Omit<NaturalFormLayoutProps, 'schema'> & { schema?: NaturalFormLayoutProps['schema'] };
169
196
 
170
- export const FormLayout = ({ schema, ...props }: FormLayoutProps) => {
197
+ /** Context-reading binding for `Form.Layout`: resolves the schema (prop or form) and delegates to {@link FormLayout}. */
198
+ export const FormLayoutController = ({ schema, ...props }: FormLayoutProps) => {
171
199
  const { form, ...contextProps } = useFormContext(FORM_LAYOUT_NAME);
172
200
  const resolvedSchema = schema ?? form.schema;
173
201
  if (!resolvedSchema) {
174
202
  return null;
175
203
  }
176
204
 
177
- return <NaturalFormLayout schema={resolvedSchema} {...contextProps} {...props} />;
205
+ return <FormLayout schema={resolvedSchema} {...contextProps} {...props} />;
178
206
  };
179
207
 
180
- FormLayout.displayName = FORM_LAYOUT_NAME;
208
+ FormLayoutController.displayName = FORM_LAYOUT_NAME;
181
209
 
182
210
  //
183
211
  // Actions
@@ -203,9 +231,7 @@ export const FormActions = ({ classNames }: FormActionsProps) => {
203
231
  // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
204
232
 
205
233
  return (
206
- <div
207
- className={mx(withColumn.center(), 'grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}
208
- >
234
+ <div className={mx(withColumn.center(), formTheme.styles().actions(), classNames)}>
209
235
  {onCancel && (
210
236
  <IconButton
211
237
  icon='ph--x--regular'
@@ -239,17 +265,23 @@ FormActions.displayName = FORM_ACTIONS_NAME;
239
265
 
240
266
  const FORM_SECTION_NAME = 'Form.Section';
241
267
 
242
- export type FormSectionProps = ThemedClassName<{ label?: string; description?: string }>;
268
+ export type FormSectionProps = ThemedClassName<{
269
+ title?: string;
270
+ description?: string;
271
+ }>;
243
272
 
244
273
  export const FormSection = composable<HTMLDivElement, FormSectionProps>(
245
- ({ children, label, description, ...props }, forwardedRef) => {
274
+ ({ children, title, description, ...props }, forwardedRef) => {
275
+ const { variant = 'default' } = useFormContext(FORM_SECTION_NAME);
276
+ const styles = formTheme.styles({ variant });
246
277
  return (
247
- <div
248
- {...composableProps(props, { classNames: 'flex flex-col pt-form-section-gap first:pt-0' })}
249
- ref={forwardedRef}
250
- >
251
- {label && <h2 className='text-lg'>{label}</h2>}
252
- {description && <p className='text-description'>{description}</p>}
278
+ <div {...composableProps(props, { classNames: styles.section() })} ref={forwardedRef}>
279
+ {(title || description) && (
280
+ <div className={styles.sectionHeader()}>
281
+ {title && <h2 className={styles.sectionTitle()}>{title}</h2>}
282
+ {description && <MarkdownView classNames={styles.sectionDescription()} content={description} />}
283
+ </div>
284
+ )}
253
285
  {children}
254
286
  </div>
255
287
  );
@@ -258,6 +290,31 @@ export const FormSection = composable<HTMLDivElement, FormSectionProps>(
258
290
 
259
291
  FormSection.displayName = FORM_SECTION_NAME;
260
292
 
293
+ //
294
+ // Group
295
+ //
296
+
297
+ const FORM_GROUP_NAME = 'Form.Group';
298
+
299
+ export type FormGroupProps = ThemedClassName<PropsWithChildren>;
300
+
301
+ /**
302
+ * A bordered card grouping related rows/controls (e.g. an entity card in a list).
303
+ * Layout-only and context-free — unlike `Form.FieldSet` it is not schema-driven.
304
+ */
305
+ export const FormGroup = composable<HTMLDivElement, FormGroupProps>(({ children, ...props }, forwardedRef) => {
306
+ const { variant = 'default' } = useFormContext(FORM_SECTION_NAME);
307
+ const styles = formTheme.styles({ variant });
308
+
309
+ return (
310
+ <div {...composableProps(props, { classNames: styles.group() })} ref={forwardedRef}>
311
+ {children}
312
+ </div>
313
+ );
314
+ });
315
+
316
+ FormGroup.displayName = FORM_GROUP_NAME;
317
+
261
318
  //
262
319
  // Submit
263
320
  //
@@ -279,7 +336,7 @@ export const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProp
279
336
  }
280
337
 
281
338
  return (
282
- <div className={mx('flex w-full pt-form-padding', classNames)}>
339
+ <div className={mx(formTheme.styles().submit(), classNames)}>
283
340
  <IconButton
284
341
  classNames='w-full'
285
342
  type='submit'
@@ -11,28 +11,27 @@ import React, { useMemo } from 'react';
11
11
 
12
12
  import { Annotation, Format } from '@dxos/echo';
13
13
  import { SchemaEx } from '@dxos/effect';
14
- import { IconBlock, IconButton, IconButtonProps, useTranslation } from '@dxos/react-ui';
14
+ import { IconButton, IconButtonProps, useTranslation } from '@dxos/react-ui';
15
15
 
16
16
  import { translationKey } from '#translations';
17
- import {
18
- type CreateOptions,
19
- type FormFieldOptions,
20
- type FormFieldRenderer,
21
- type FormFieldRendererProps,
22
- type RefFieldDataProps,
23
- } from '#types';
17
+ import { type FieldContext, type FormFieldRenderer, type FormFieldRendererProps } from '#types';
24
18
 
19
+ import { AutofillAnnotation, OptionsLookupAnnotation } from '../../../annotations';
25
20
  import { useFormFieldState } from '../../../hooks';
26
21
  import { getRefProps } from '../../../util';
27
22
  import { FormFieldSet } from '../FormFieldSet';
28
23
  import {
29
24
  ArrayField,
25
+ AsyncSelectField,
26
+ AutofillField,
30
27
  BooleanField,
28
+ ComboboxField,
31
29
  DateField,
32
30
  GeoPointField,
33
31
  InlineRefField,
34
32
  MarkdownField,
35
33
  NumberField,
34
+ PasswordField,
36
35
  RefField,
37
36
  SelectField,
38
37
  TextAreaField,
@@ -55,20 +54,35 @@ export type FormFieldProps = {
55
54
  */
56
55
  name: string | null;
57
56
 
57
+ /**
58
+ * Explicit label, overriding the `title ?? capitalize(name)` derivation. Used by `ArrayField` to
59
+ * give scalar/ref items the array's resolved title (e.g. `Tags`) rather than re-capitalizing the
60
+ * raw array property name (e.g. `_tags`), since the element type carries no title of its own.
61
+ */
62
+ label?: string;
63
+
58
64
  /**
59
65
  * Path to the current object from the root. Used with nested forms.
60
66
  */
61
67
  path?: (string | number)[];
62
68
  autoFocus?: boolean;
63
- } & FormFieldOptions &
64
- Pick<RefFieldDataProps, 'getOptions' | 'onCreate' | 'useType'> &
65
- CreateOptions;
69
+ /** Whether the field is required (non-optional in the schema). Drives the label asterisk. */
70
+ required?: boolean;
71
+ /**
72
+ * Force a `Ref` field to render its target inline (a nested form) instead of the picker.
73
+ * Set by `ArrayField` for owned-ref arrays (`FormCreateAnnotation`); equivalent to
74
+ * `FormInlineAnnotation` but driven by the parent array rather than the element's own AST.
75
+ */
76
+ refInline?: boolean;
77
+ } & FieldContext;
66
78
 
67
79
  export const FormField = (props: FormFieldProps) => {
68
80
  const {
69
81
  type,
70
82
  name,
83
+ label: labelProp,
71
84
  path,
85
+ required,
72
86
  projection,
73
87
  fieldMap,
74
88
  fieldProvider,
@@ -85,22 +99,27 @@ export const FormField = (props: FormFieldProps) => {
85
99
  useType,
86
100
  getOptions,
87
101
  onCreate,
102
+ resolveCreateEntry,
103
+ refInline,
88
104
  } = props;
89
105
  const { t } = useTranslation(translationKey);
90
106
  const title = SchemaEx.getAnnotation<string>(SchemaAST.TitleAnnotationId)(type);
91
107
  const description = SchemaEx.getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
92
108
  const examples = SchemaEx.getAnnotation<string[]>(SchemaAST.ExamplesAnnotationId)(type);
93
109
 
94
- // `name === null` means "no header" -- collapse to an empty string so
95
- // downstream consumers keep their `label: string` types, and the falsy
96
- // value lets `FormFieldSet`'s `label && <FormFieldLabel ...>` guard skip
97
- // the header.
98
- const label = useMemo(() => title ?? (name == null ? '' : String.capitalize(name)), [title, name]);
110
+ const label = useMemo(
111
+ () => labelProp ?? title ?? (name == null ? '' : String.capitalize(name)),
112
+ [labelProp, title, name],
113
+ );
99
114
  const placeholder = useMemo(
100
115
  () => (examples?.length ? `${t('example.placeholder')}: ${examples[0]}` : (description ?? label)),
101
- [examples, description, label],
116
+ [examples, description, label, t],
102
117
  );
103
118
 
119
+ // Build the schema for `fieldProvider` only when one is registered, memoized by `type` (the AST) so
120
+ // we don't reconstruct it on every render.
121
+ const providerSchema = useMemo(() => (fieldProvider ? Schema.make(type) : undefined), [fieldProvider, type]);
122
+
104
123
  const fieldState = useFormFieldState(FormField.displayName, path);
105
124
  const jsonPath = SchemaEx.createJsonPath(path ?? []);
106
125
  const fieldProps: FormFieldRendererProps = {
@@ -108,16 +127,18 @@ export const FormField = (props: FormFieldProps) => {
108
127
  format: Format.FormatAnnotation.getFromAst(type).pipe((annotation) => Option.getOrUndefined(annotation)),
109
128
  readonly,
110
129
  label,
130
+ description,
111
131
  jsonPath,
112
132
  placeholder,
113
- layout,
133
+ presentation: layout,
134
+ required,
114
135
  db,
115
136
  ...fieldState,
116
137
  };
117
138
 
118
139
  // Omit empty fields entirely in read-only mode -- an empty value has nothing
119
140
  // to display, so a labelled row with a blank input is just noise. This
120
- // mirrors what `FormFieldWrapper` already does for `layout === 'static'`, but
141
+ // mirrors what `FormRow` already does for `presentation === 'static'`, but
121
142
  // covers every field type (including those that bypass the wrapper:
122
143
  // RefField, SelectField, MarkdownField, ...). Container fields
123
144
  // (`ArrayField`, nested-struct -> `FormFieldSet`) keep their own
@@ -136,10 +157,29 @@ export const FormField = (props: FormFieldProps) => {
136
157
  return <CustomField {...fieldProps} />;
137
158
  }
138
159
 
139
- // TODO(burdon): Expensive to create schema each time; pass AST?
140
- const component = fieldProvider?.({ schema: Schema.make(type), prop: name ?? '', fieldProps });
141
- if (component) {
142
- return component;
160
+ if (fieldProvider && providerSchema) {
161
+ const component = fieldProvider({ schema: providerSchema, prop: name ?? '', fieldProps });
162
+ if (component) {
163
+ return component;
164
+ }
165
+ }
166
+
167
+ //
168
+ // Dynamic, value-driven fields (options/value/validation loaded via a self-contained Effect annotation).
169
+ //
170
+
171
+ const optionsLookup = Option.getOrUndefined(OptionsLookupAnnotation.getFromAst(type));
172
+ if (optionsLookup) {
173
+ return optionsLookup.combobox ? (
174
+ <ComboboxField {...fieldProps} lookup={optionsLookup} />
175
+ ) : (
176
+ <AsyncSelectField {...fieldProps} lookup={optionsLookup} />
177
+ );
178
+ }
179
+
180
+ const autofill = Option.getOrUndefined(AutofillAnnotation.getFromAst(type));
181
+ if (autofill) {
182
+ return <AutofillField {...fieldProps} autofill={autofill} />;
143
183
  }
144
184
 
145
185
  //
@@ -190,10 +230,21 @@ export const FormField = (props: FormFieldProps) => {
190
230
 
191
231
  const refProps = getRefProps(type);
192
232
  if (refProps) {
193
- // Inline a single referenced object's own fields (nested form) instead of a picker.
194
- const inline = Annotation.FormInlineAnnotation.getFromAst(refProps.ast).pipe(Option.getOrElse(() => false));
233
+ // Inline a single referenced object's own fields (nested form) instead of a picker. `refInline` lets a
234
+ // parent (e.g. an owned-ref `ArrayField`) force this for elements whose own AST carries no annotation.
235
+ const inline =
236
+ refInline || Annotation.FormInlineAnnotation.getFromAst(refProps.ast).pipe(Option.getOrElse(() => false));
195
237
  if (inline && !refProps.isArray) {
196
- return <InlineRefField {...fieldProps} {...refProps} db={db} useType={useType} onCreate={onCreate} />;
238
+ return (
239
+ <InlineRefField
240
+ {...fieldProps}
241
+ {...refProps}
242
+ db={db}
243
+ useType={useType}
244
+ onCreate={onCreate}
245
+ resolveCreateEntry={resolveCreateEntry}
246
+ />
247
+ );
197
248
  }
198
249
 
199
250
  const isCreateTarget = !createTypename || refProps.typename === createTypename;
@@ -209,6 +260,7 @@ export const FormField = (props: FormFieldProps) => {
209
260
  useType={useType}
210
261
  getOptions={getOptions}
211
262
  onCreate={onCreate}
263
+ resolveCreateEntry={resolveCreateEntry}
212
264
  />
213
265
  );
214
266
  }
@@ -243,6 +295,7 @@ export const FormField = (props: FormFieldProps) => {
243
295
  useType={useType}
244
296
  getOptions={getOptions}
245
297
  onCreate={onCreate}
298
+ resolveCreateEntry={resolveCreateEntry}
246
299
  />
247
300
  );
248
301
  }
@@ -258,12 +311,7 @@ FormField.displayName = 'Form.FormField';
258
311
  //
259
312
 
260
313
  export const CompactIconButton = (props: IconButtonProps) => {
261
- return (
262
- // IconBlock defaults to aria-hidden (decorative slot); the button is interactive, so un-hide it.
263
- <IconBlock aria-hidden={false} classNames='my-[1px]'>
264
- <IconButton variant='ghost' density='xs' square iconOnly {...props} />
265
- </IconBlock>
266
- );
314
+ return <IconButton variant='ghost' iconOnly {...props} />;
267
315
  };
268
316
 
269
317
  /**
@@ -288,6 +336,7 @@ const getFormField = ({
288
336
  Match.when(Format.TypeFormat.DateTime, () => DateField),
289
337
  Match.when(Format.TypeFormat.GeoPoint, () => GeoPointField),
290
338
  Match.when(Format.TypeFormat.Markdown, () => MarkdownField),
339
+ Match.when(Format.TypeFormat.Password, () => PasswordField),
291
340
  Match.when(Format.TypeFormat.Text, () => TextAreaField),
292
341
  Match.when(Format.TypeFormat.Time, () => DateField),
293
342
  Match.orElse(() => undefined),
@@ -310,6 +359,8 @@ const getFormField = ({
310
359
  case 'BooleanKeyword':
311
360
  return BooleanField;
312
361
  }
362
+
363
+ return undefined;
313
364
  };
314
365
 
315
366
  const getSelectOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {